/* ==========================================================================
   Kepta — willingness-to-pay landing page
   Built mobile-first at 390px. Desktop is the enhancement, never the source.

   Palette is locked (brandkit/master/KEPTA-PALETTE-LOCK.md). Measured ratios:
     Ink   #34424A on Base    #FAF7F2 →  9.71:1  AAA
     Ink   #34424A on Surface #FFFFFF → 10.37:1  AAA
     Ink   #34424A on Peach   #FFD6A5 →  7.61:1  AAA
     Muted #5A6670 on Base            →  5.51:1  AA
     Muted #5A6670 on Surface         →  5.88:1  AA
     White #FFFFFF on Teal    #17706A →  5.90:1  AA
     Teal  #17706A as text on Base    →  5.52:1  AA

   Hard bans enforced below:
     · Calm Teal #4DB6AC never carries text or a button (2.44:1 — fails).
     · Soft Peach bands take Ink text only (Muted 4.32:1, Teal 4.33:1 — under AA).
     · Deep Teal #17706A appears ONLY on interactive elements.
   ========================================================================== */

/* --- Fonts: self-hosted. The CSP declares no font-src, so it falls back to
       'self' and Google Fonts would be blocked outright. ------------------- */
@font-face {
  font-family: 'Manrope';
  src: url('/assets/fonts/manrope-var-latin.woff2') format('woff2');
  font-weight: 200 800;
  font-display: swap;
  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;
}
@font-face {
  font-family: 'Inter';
  src: url('/assets/fonts/inter-var-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
  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 ------------------------------------------------------------- */
:root {
  --base: #FAF7F2;
  --surface: #FFFFFF;
  --ink: #34424A;
  --muted: #5A6670;
  --accent: #17706A;          /* interactive only */
  --accent-deep: #125A55;     /* pressed state — darker, still AA on white */
  --tint: #4DB6AC;            /* decoration only — never text, never buttons */
  --peach: #FFD6A5;
  --ivory-shade: #E7DFD3;

  --line: rgba(52, 66, 74, 0.10);
  --line-strong: rgba(52, 66, 74, 0.18);

  /* Spacing — 4px base, nothing off-scale */
  --s1: 4px;   --s2: 8px;   --s3: 12px;  --s4: 16px;  --s5: 24px;
  --s6: 32px;  --s7: 48px;  --s8: 64px;  --s9: 96px;  --s10: 128px; --s11: 160px;

  --r-sm: 8px; --r-md: 12px; --r-lg: 16px; --r-xl: 24px; --r-full: 999px;

  --dur-fast: 160ms;
  --dur-base: 240ms;
  --dur-slow: 420ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);

  /* Fluid type — 1.25 ratio on mobile easing to 1.333 on desktop */
  --t-display: clamp(2.125rem, 1.55rem + 2.95vw, 4rem);
  --t-h2:      clamp(1.625rem, 1.33rem + 1.5vw, 2.5rem);
  --t-h3:      clamp(1.125rem, 1.05rem + 0.38vw, 1.375rem);
  --t-lede:    clamp(1.0625rem, 1.0rem + 0.31vw, 1.25rem);
  --t-body:    clamp(1rem, 0.97rem + 0.16vw, 1.0625rem);
  --t-small:   0.9375rem;
  --t-fine:    0.875rem;
  --t-price:   clamp(2rem, 1.7rem + 1.5vw, 2.75rem);

  --measure: 62ch;
  --wrap: 1120px;
}

/* --- Reset -------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body, h1, h2, h3, p, figure, blockquote, ul, ol, dd { margin: 0; }
ul[class] { list-style: none; padding: 0; }
img, svg, video { display: block; max-width: 100%; height: auto; }
button, input, textarea, select { font: inherit; color: inherit; }

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

body {
  background: var(--base);
  color: var(--ink);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  font-size: var(--t-body);
  line-height: 1.6;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* --- Typography --------------------------------------------------------- */
h1, h2, h3, .display {
  font-family: 'Manrope', 'Inter', system-ui, sans-serif;
  color: var(--ink);
  text-wrap: balance;
}
h1 {
  font-size: var(--t-display);
  font-weight: 800;
  line-height: 1.06;
  letter-spacing: -0.028em;   /* large display needs negative tracking */
}
h2 {
  font-size: var(--t-h2);
  font-weight: 700;
  line-height: 1.14;
  letter-spacing: -0.02em;
  /* Display type wants a far shorter measure than body copy. Without this a
     section heading runs the full 1120px on desktop and stops scanning well. */
  max-width: 26ch;
}
h3 {
  font-size: var(--t-h3);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
}
p { max-width: var(--measure); }
.lede {
  font-size: var(--t-lede);
  line-height: 1.55;
  color: var(--muted);
}
.fine {
  font-size: var(--t-fine);
  line-height: 1.5;
  color: var(--muted);
}
strong { font-weight: 600; color: var(--ink); }

/* --- Layout ------------------------------------------------------------- */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--s5);
}
section { padding-block: var(--s8); }          /* 64px mobile */
@media (min-width: 768px) { section { padding-block: var(--s9); } }
@media (min-width: 1024px) { section { padding-block: var(--s10); } }

.band {
  background: var(--surface);
  border-block: 1px solid var(--line);
}
.band-peach { background: var(--peach); }
/* Peach carries Ink only — never Muted, never Deep Teal text. */
.band-peach, .band-peach p, .band-peach .lede, .band-peach .fine { color: var(--ink); }

/* --- Header ------------------------------------------------------------- */
.site-header {
  padding-block: var(--s5);
}
.site-header .wordmark {
  width: 104px;              /* Lockup Rules §3: 90px absolute minimum */
  height: auto;
}

/* The wordmark and the claim as one unit, so .site-header-row keeps the
   language switch pinned right no matter what the claim is doing. */
.site-lockup {
  display: flex;
  flex-direction: column;    /* stacked on a phone: 104px + the claim does not
                                fit on one line at 320px */
  align-items: flex-start;
  gap: 3px;
  min-width: 0;
}
/* Quiet on purpose — it sits over the CHAOS screen and must lose to the word
   every time. `nowrap` is load-bearing, not cosmetic: a second line makes the
   header taller, and the header's height feeds the sticky opener below it. At
   320px the widest of the two languages measures ~190px against 288px of
   available width, so it holds. */
.header-claim {
  margin: 0;
  font-size: 0.6875rem;      /* 11px */
  line-height: 1.2;
  color: var(--muted);
  white-space: nowrap;
}
/* STAYS STACKED AT EVERY WIDTH. This used to flip to a row at 560px, which put
   the claim on the same baseline as the wordmark on a computer and read as two
   competing marks rather than one lockup. Below the wordmark it reads as what
   it is — a line belonging to the name above it — and the header behaves
   identically on a phone and on a desktop. Only the size changes. */
@media (min-width: 560px) {
  .header-claim { font-size: var(--t-fine); }
}

/* --- Hero --------------------------------------------------------------- */
/* --- Hero: the second opening -------------------------------------------
 *
 * The Visual Poetry layout, taken properly this time: the type IS the
 * composition on the left, and on the right a single feature panel with the
 * subject inside it, a mark cut into its top-right corner and a rail of
 * circular controls cut into its lower-left edge.
 *
 * Everything that was here is still here. Nothing was invented to fill the
 * layout, which is the trap with a reference this graphic: the shapes want
 * content, and content made up to fill a shape is the thing a reader smells.
 */
.hero { position: relative; padding-top: var(--s6); padding-bottom: var(--s10); }
/* THE LIGHT DOES NOT STOP AT THE SECTION BOUNDARY. The opener's shaft fades
   out over its own last third; this picks up what is left of it and carries
   it a few hundred pixels into the hero before letting go. Deliberately not a
   scene — no visible edge, no second beam, just the room still being slightly
   lit on one side and slightly not on the other, resolving to flat ivory. */
.hero::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  width: 100vw;
  height: 300px;
  transform: translateX(-50%);
  z-index: 0;
  pointer-events: none;
  /* BOTH ENDS HAVE TO MEET AT ZERO. The first attempt started this at 0.10,
     which is darker than the opener is by the time it has faded out — so the
     hard line did not go away, it just changed direction. Everything here
     starts at nothing, swells a little below the boundary and dissolves. */
  background:
    radial-gradient(130% 70% at 74% 0%,
      rgba(255, 253, 248, 0) 0%, rgba(255, 253, 248, 0.5) 26%, rgba(255, 253, 248, 0) 72%),
    linear-gradient(to bottom,
      rgba(80, 61, 42, 0) 0%, rgba(80, 61, 42, 0.085) 32%, rgba(80, 61, 42, 0) 100%);
}
.hero > .wrap { position: relative; z-index: 1; }

.hero-poem { display: flex; flex-direction: column; gap: var(--s7); }
.hero-col { display: flex; flex-direction: column; gap: var(--s5); align-items: stretch; }

/* THE HEADLINE CARRIES THE PAGE. Nearly twice the old size, leading under 1,
   tight tracking, and a measure short enough to force the stack — a sentence
   this size laid out in one or two lines is just big text. */
.hero-title {
  font-size: clamp(2.6rem, 1.45rem + 5.4vw, 5.5rem);
  line-height: 0.94;
  letter-spacing: -0.045em;
  margin: 0;
  max-width: 13ch;
}
/* German runs about 40% longer for the same sentence — "Nie wieder überlegen,
   ob Sie es schon genommen haben." is 52 characters against 36. At the English
   size it stacks to seven lines on a phone and stops being a headline. */
html[lang="de"] .hero-title {
  font-size: clamp(2rem, 1.15rem + 4vw, 4.25rem);
  max-width: 15ch;
}

/* Set against the headline's right edge on desktop, the way the reference
   does it. NOT on a phone: a ragged left edge measurably slows reading, and
   this is the one sentence on the page that has to be understood by somebody
   deciding in four seconds whether the page is about them. */
.hero-what {
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--muted);
  max-width: 36ch;
  margin: 0;
  /* Set against the headline's right edge at EVERY width, not just on desktop.
     It costs a little reading speed — a ragged left edge always does — and it
     is two short sentences, which is the size where the composition is worth
     more than the milliseconds. */
  align-self: flex-end;
  text-align: right;
}
.hero-what strong { color: var(--ink); font-weight: 600; }

/* The sentence somebody repeats to a friend, so it is set one step up from the
   body and in the display face. */
.hero-line {
  margin: 0;
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: clamp(1.1875rem, 1.05rem + 0.6vw, 1.5rem);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.02em;
  color: var(--ink);
  /* The line breaks where the <br> says, and nowhere else. A 22ch measure on
     top of an explicit break put "Nichts mehr im Kopf." on a third line. */
  max-width: none;
  text-wrap: pretty;
}

/* The old fine print. Three promises that were one grey line nobody read.
   The rule running off to the right is the reference's device and it does a
   real job here: it stops three pills floating in space. */
.hero-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin: 0;
  padding: 0;
  list-style: none;
}
.hero-chips li {
  padding: 6px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  font-size: var(--t-fine);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}
/* THE RULE HAS TO KNOW WHETHER THE CHIPS WRAPPED.
   Inline, it ends up alongside whichever chip happens to land last on the
   second row — which in German is one chip on its own, with a hairline running
   off beside it looking like a mistake. A 100% basis forces it onto its own
   line, so it always sits under the whole group.

   It stays that way on desktop too. Keeping it inline there only works while
   all three chips fit one row, and in German they fit at 1280 and wrap at
   1024 — so the ugly state was not hypothetical, it was just off my screen.
   A full-width hairline under the group reads as deliberate at every width;
   a stub beside a lone chip never does. */
.hero-chips::after {
  content: '';
  flex: 1 0 100%;
  height: 1px;
  margin-top: var(--s2);
  background: var(--line);
}

.hero-actions { display: flex; flex-direction: column; gap: var(--s3); align-items: stretch; }

/* --- Keppy, back on the page's own ground -------------------------------
   The Soft Peach panel is gone. It made the section look like a card in a
   deck rather than the top of a page, and it boxed in the one character on
   the site whose whole job is to feel unboxed. What survives from the
   reference is the mark cut into the top right — that detail worked. */
.hero-stage { position: relative; display: flex; justify-content: center; }
/* OPTICALLY centred, not box-centred. His raised hand pushes the artwork's
   bounding box left of where his head reads, so a box centred on the column
   puts the face about seven pixels right of the middle — invisible in a
   screenshot and the first thing you see on a phone. */
.hero-keppy-wrap { position: relative; z-index: 1; transform: translateX(-3.5%); }
.hero-keppy-wrap .keppy-stack { width: clamp(160px, 45vw, 200px); }

/* Above his head, with air. It used to overlap by 14px, which read as a label
   stuck ON him rather than something he is saying. */
.hero-keppy-wrap .keppy-hint {
  bottom: calc(100% + 12px);
  white-space: normal;
  width: max-content;
  max-width: min(230px, 66vw);
  line-height: 1.3;
  text-align: center;
}
.hero-keppy-wrap .keppy-say { bottom: calc(100% + 12px); }

/* The mark. Deep Teal by request — noting for whoever reads this next that it
   is the one place on the site where that colour appears on something you
   cannot press, which is the rule it otherwise keeps everywhere. */
/* Just the mark. The circle around it made it look like a control that did
   nothing, and it was the one place on the site where Deep Teal sat on
   something you could not press. */
.panel-mark {
  position: absolute;
  top: 0;
  right: 3%;
  z-index: 2;
  display: block;
  color: var(--accent);
}
.panel-mark svg { width: 25px; height: 35px; display: block; }

@media (min-width: 860px) {
  .hero-poem { flex-direction: row; align-items: center; gap: var(--s8); }
  .hero-col { flex: 1 1 56%; justify-content: center; gap: var(--s6); }
  .hero-stage { flex: 1 1 44%; }
  .hero-keppy-wrap .keppy-stack { width: clamp(210px, 21vw, 280px); }
  /* On a phone the CTA is full width and thumb-reachable. On desktop a
     full-width button reads as a banner, so let it size to its label. */
  .hero-actions { align-items: flex-start; }
  .hero-actions .scroll-cue { align-self: flex-start; padding-left: var(--s2); }
  .hero-actions .btn { padding-inline: var(--s7); }
}

/* --- Recognition -------------------------------------------------------- */
.questions {
  display: grid;
  gap: var(--s4);
  margin-top: var(--s6);
  padding: 0;
  list-style: none;
}
.questions li {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: clamp(1.375rem, 1.1rem + 1.4vw, 2rem);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--ink);
  padding-left: var(--s5);
  border-left: 2px solid var(--tint);   /* decoration only — carries no text */
}
/* The answer, revealed under the question it answers.
 *
 * The reader raises their hand and gets a reply in the same breath — the best
 * interaction slot left on the page, because the question is already theirs.
 *
 * grid-template-rows 0fr -> 1fr is the only way to animate to an unknown
 * height without measuring anything in script. Two things are load-bearing:
 * the inner element must carry `overflow: hidden` (or the text spills while
 * collapsed, and its padding with it), and it needs `min-height: 0` (or the
 * grid item refuses to shrink below its content and the row never closes).
 *
 * The answers inherit .questions li's 22-32px display type otherwise, which
 * would make each reply as loud as the question — so size and weight are reset
 * here deliberately. The answer is a reply, not a second headline. */
.q-answer-wrap {
  display: grid;
  grid-template-rows: 0fr;
  /* The gap above the answer lives HERE, on the container, and not on the
     answer itself. Measured: padding on the grid item survives the collapse —
     `overflow: hidden` clips the item's CONTENT, never its padding — so every
     question sat above a phantom 12px of nothing. On the container it is
     simply absent until the question is pressed, and it animates in step with
     the row rather than snapping. */
  padding-top: 0;
  transition: grid-template-rows var(--dur-base) var(--ease-out),
              padding-top var(--dur-base) var(--ease-out);
}
.q-btn[aria-pressed="true"] + .q-answer-wrap {
  grid-template-rows: 1fr;
  padding-top: var(--s3);
}
.q-answer {
  overflow: hidden;
  min-height: 0;
  margin: 0;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: var(--t-small);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0;
  color: var(--muted);
  max-width: 42ch;
}
@media (prefers-reduced-motion: reduce) {
  .q-answer-wrap { transition: none; }
}

.questions-after { margin-top: var(--s7); font-size: var(--t-lede); color: var(--muted); }

/* --- Steps -------------------------------------------------------------- */
.steps { display: grid; gap: var(--s5); margin-top: var(--s7); counter-reset: step; }
.step {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s5);
}
.step-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px; height: 28px;
  border-radius: var(--r-full);
  background: var(--base);
  border: 1px solid var(--line-strong);
  font-family: 'Manrope', sans-serif;
  font-size: var(--t-fine);
  font-weight: 700;
  color: var(--muted);
  margin-bottom: var(--s3);
}
.step h3 { margin-bottom: var(--s2); }
.step p { color: var(--muted); font-size: var(--t-small); }
.step-shot {
  margin-top: var(--s4);
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--base);
}
@media (min-width: 768px) { .steps { grid-template-columns: repeat(3, 1fr); gap: var(--s5); } }

/* --- Relief ------------------------------------------------------------- */
.ba p { max-width: 46ch; }

/* --- Trust -------------------------------------------------------------- */
.trust-list { display: grid; gap: var(--s5); margin-top: var(--s6); }
.trust-item h3 { margin-bottom: var(--s2); font-size: var(--t-h3); }
.trust-item p { color: var(--muted); font-size: var(--t-small); }
@media (min-width: 768px) { .trust-list { grid-template-columns: 1fr 1fr; gap: var(--s6) var(--s8); } }

/* --- The ask ------------------------------------------------------------ */
.ask { scroll-margin-top: var(--s5); }
/* The exchange, immediately under the headline and in one breath. Set at lede
   size in full ink rather than muted: it is the sentence that has to be
   believed for the rest of the block to work, so it is the second thing read,
   not the fifth. */
.ask-exchange {
  color: var(--ink);
  font-size: var(--t-lede);
  margin-top: var(--s4);
  margin-bottom: var(--s6);
  max-width: 34ch;
}

/* THE HEAD: the offer and its alternative, as two columns on a wide screen and
   a stack on a phone.
 *
 * The hierarchy has to survive both. On a phone the offer simply comes first
 * and the cost follows, which is the reading order that matters. On a desktop
 * they sit side by side, and the weighting is carried by size and colour
 * instead of by order: the offer keeps the display heading, the cost is body
 * copy in muted ink behind a hairline. Never two headlines competing — that
 * was the failure this block was rebuilt to fix. */
.ask-head {
  display: grid;
  gap: var(--s6);
  margin-bottom: var(--s7);
}
@media (min-width: 860px) {
  .ask-head {
    /* The offer takes the larger share. An even split would read as two equal
       claims, which is exactly what it must not be. */
    grid-template-columns: 1.25fr 1fr;
    gap: var(--s8);
    align-items: start;
  }
}

/* The cost of NOT taking part. Quieter than the offer at every width, and
   never a heading. The rule above it (left of it, on desktop) is the only
   decoration: it separates the two halves without boxing either one. */
.ask-anchor {
  margin: 0;
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
  max-width: 46ch;
}
.ask-anchor-title {
  margin: 0 0 var(--s4);
  color: var(--ink);
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-lede);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
}

/* THREE LINES, NOT A PARAGRAPH.
   On a phone this sits between the offer and the card, which is the right
   place — a reader who lands here from the sticky button meets the offer, then
   this, before the form. But as four muted sentences run together it was a
   wall, and a wall in that position is skipped by exactly the people it is
   written for. Split out, each line is one specific evening someone recognises.

   They arrive one after another rather than together: the stagger is what
   makes three lines read as three separate costs adding up, instead of as a
   list that was always there. Cost is a transition-delay and nothing else. */
.ask-cost-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--s3);
}
.ask-cost-list li {
  position: relative;
  padding-left: var(--s5);
  color: var(--muted);
  font-size: var(--t-small);
  line-height: 1.5;
}
/* A small tick-less mark. Calm Teal, because it is decoration and carries no
   meaning the text does not — Deep Teal would read as tappable. */
.ask-cost-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.58em;
  width: 10px;
  height: 1px;
  background: var(--tint);
}
.ask-cost-list li:nth-child(1) { transition-delay: 90ms; }
.ask-cost-list li:nth-child(2) { transition-delay: 200ms; }
.ask-cost-list li:nth-child(3) { transition-delay: 310ms; }
@media (min-width: 860px) {
  /* On a wide screen the rule turns and becomes the seam between the columns,
     which is what a vertical hairline is actually for. */
  .ask-anchor {
    padding-top: 0;
    padding-left: var(--s5);
    border-top: 0;
    border-left: 1px solid var(--line);
    /* Optically aligned to the headline's cap height rather than its box —
       the h2's line-height leaves dead space above the letterforms. */
    margin-top: 6px;
  }
}
.ask-card {
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xl);
  padding: var(--s6) var(--s5);
  box-shadow: 0 1px 2px rgba(52, 66, 74, 0.04), 0 8px 24px rgba(52, 66, 74, 0.05);
  max-width: 560px;
}
@media (min-width: 768px) { .ask-card { padding: var(--s7); } }

/* Heading-sized, not display-sized. At the old 48-72px this was the biggest
   thing on the page and read as an amount due — the section looked like a
   checkout, and people arrived at it braced to be asked for a card. It is a
   fact about the product now, so it is set like one. */
/* Says what the card is before the card says what it costs. */
.card-title {
  margin-bottom: var(--s2);
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-h3);
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--ink);            /* 10.37:1 on Surface */
}

.price {
  font-family: 'Manrope', sans-serif;
  font-size: var(--t-price);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--ink);
}
.price-unit { font-size: 0.5em; font-weight: 600; letter-spacing: -0.01em; color: var(--muted); }
.price-daily { margin-top: var(--s2); color: var(--muted); font-size: var(--t-small); }
/* The 18+ line and the two legal links. Sits under the disclosure block and
   above the form, so it is read before the card is filled in, not after.
   Muted on Surface is 5.88:1; the links take Deep Teal, which is interactive
   and therefore allowed to. */
.price-terms {
  margin-top: var(--s3);
  font-size: var(--t-fine);
  line-height: 1.55;
  color: var(--muted);
}
.price-terms a {
  color: var(--accent);
  /* Inline in a sentence, so no 44px block here — these are not the primary
     path and forcing a tap target would break the paragraph. Both pages are
     also linked from the footer, where they do get a full target. */
  text-underline-offset: 2px;
}

/* The reassurances under the price were always there and always read as fine
   print. Naming them is the whole change — a promise with a heading is a
   guarantee, the same words without one are a disclaimer. */
.guarantee-title {
  margin-top: var(--s5);
  font-weight: 700;
  font-size: var(--t-small);
  color: var(--ink);            /* 10.37:1 on Surface */
}
.price-note {
  margin-top: var(--s2);
  font-size: var(--t-small);
  color: var(--ink);          /* 10.37:1 on Surface */
  line-height: 1.55;
  max-width: none;
}
.price-note strong { color: var(--ink); }

/* --- The consent box ------------------------------------------------------
   A real 44px target (§8.2) built from a label wrapping the box and its text,
   so the whole sentence is tappable rather than just the 18px square — which
   matters most for exactly the people this site is for. */
/* Its own wrapper class rather than .field, which carries a `label { display:
   block }` rule that would flatten the row. */
.consent-field { margin-top: var(--s5); }
.consent {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  /* Negative margin plus padding so the touch target extends past the visible
     text without pushing the layout around. */
  margin: calc(var(--s2) * -1);
  padding: var(--s2);
  border-radius: var(--r-md);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.consent input[type="checkbox"] {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  margin: 0;
  /* Optically centred on the first line of text rather than its box. */
  margin-top: 0.1em;
  accent-color: var(--accent);
  cursor: pointer;
}
.consent input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.consent-text {
  font-size: var(--t-fine);
  line-height: 1.55;
  color: var(--muted);
}
.consent-text strong { color: var(--ink); }
.consent-text a {
  color: var(--accent);
  text-underline-offset: 2px;
}
/* The error sits directly beneath the box it belongs to. A summary at the top
   of the form makes somebody hunt for what they missed. */
.consent-field .field-error { margin-top: var(--s2); }
/* Same red as .field-error below, 5.6:1 on white. Not a token because it is the
   only error colour on the site and it exists in exactly these two places. */
.consent[data-invalid="true"] input[type="checkbox"] { outline: 2px solid #8E3A32; outline-offset: 3px; }

/* The value stack. Four things that were already true and already on the page,
   never counted up anywhere the eye could total them.

   The ticks are Calm Teal (#4DB6AC) — Palette Lock §1 allows it as DECORATION
   and forbids it on text or a button. That holds here: the tick carries no
   meaning the sentence does not, so its 2.44:1 contrast is not a failure, it is
   a mark beside legible Ink. Deep Teal would have been wrong for the opposite
   reason — on this site teal-on-a-thing means "tap this". */
.stack {
  margin-top: var(--s5);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
}
.stack-title {
  font-weight: 700;
  font-size: var(--t-small);
  color: var(--ink);            /* 10.37:1 on Surface */
}
.stack-list {
  margin-top: var(--s3);
  padding: 0;
  list-style: none;
}
.stack-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  margin-top: var(--s3);
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink);
}
.stack-list svg {
  flex: 0 0 auto;
  margin-top: 0.18em;           /* optical, so the tick sits on the x-height */
  color: var(--tint);
}
.stack-list strong { font-weight: 700; }

/* The facts end, the action begins. Without this the price and the form are
   one continuous block, which is exactly the shape of a checkout. */
#reserve-form {
  margin-top: var(--s6);
  padding-top: var(--s6);
  border-top: 1px solid var(--line);
}

/* The trade, sitting directly above the email field so it is read before the
   address is typed rather than sprung afterwards. Plain type, no box — it is a
   sentence, not a banner. */
.ask-trade {
  margin-bottom: var(--s4);
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--ink);           /* 10.37:1 on Surface */
}

/* The proof-of-concept note. Quiet by design: honest about the stage without
   making the stage the headline. */
.ask-poc {
  margin-top: var(--s4);
  font-size: var(--t-fine);
  line-height: 1.55;
  color: var(--muted);         /* 5.88:1 on Surface */
  text-align: center;
}

/* --- Language switch ------------------------------------------------------
   Two letters in the header. Deliberately plain: it is a utility, not a
   feature, and it competes with nothing. The current language is text rather
   than a link, so there is no control that does nothing when pressed. */
.site-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
}
.lang-switch {
  display: flex;
  align-items: center;
  gap: var(--s1);
  font-size: var(--t-fine);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.lang-switch a,
.lang-current {
  /* 44px of target around two small letters (§8.2) without the header growing:
     the padding is vertical-centred inside a fixed line-height row. */
  display: grid;
  place-items: center;
  min-width: 44px;
  min-height: 44px;
  border-radius: var(--r-sm);
}
.lang-current { color: var(--ink); }
.lang-switch a { color: var(--muted); text-decoration: none; }
.lang-switch a:hover { color: var(--accent); background: rgba(23, 112, 106, 0.06); }
.lang-switch a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- Forms -------------------------------------------------------------- */
.field { margin-top: var(--s5); }
.field label {
  display: block;
  font-size: var(--t-fine);
  font-weight: 600;
  color: var(--ink);
  margin-bottom: var(--s2);
}
/* type="text" belongs here too. It was missing, which meant the survey's
   "Something else" box — the only text input on the site — rendered as a raw
   browser default: wrong height, wrong radius, no border of ours, and then a
   teal focus glow wrapped around a grey system border the moment it was
   tapped. Everything else in the dialog was styled and that one field was not. */
.field input[type="email"],
.field input[type="text"],
.field textarea {
  width: 100%;
  min-height: 48px;                    /* > 44px touch target */
  padding: var(--s3) var(--s4);
  font-size: 1rem;                     /* 16px — below this iOS auto-zooms */
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  transition: border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.field textarea { min-height: 96px; resize: vertical; line-height: 1.5; }
.field input::placeholder, .field textarea::placeholder { color: var(--muted); } /* 5.88:1 */
.field input:hover, .field textarea:hover { border-color: rgba(52, 66, 74, 0.30); }

:where(input, textarea, button, a, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
.field input:focus-visible, .field textarea:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(23, 112, 106, 0.16);
}
.field input[aria-invalid="true"] { border-color: #A5453C; }
/* Success: a validated, filled field settles to a calm confirmed border. */
.field input[aria-invalid="false"]:not(:placeholder-shown):not(:focus) {
  border-color: rgba(23, 112, 106, 0.45);
}
.field-error {
  display: block;
  margin-top: var(--s2);
  font-size: var(--t-fine);
  color: #8E3A32;                       /* 5.6:1 on white */
  min-height: 1.25em;
}

.turnstile-holder { margin-top: var(--s4); min-height: 66px; }
/* The widget has a fixed intrinsic width — scale it rather than let it push
   the page sideways on a 320px screen. */
@media (max-width: 359px) {
  .turnstile-holder { transform: scale(0.88); transform-origin: left top; }
}

/* --- Buttons ------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: 52px;
  padding: var(--s3) var(--s6);
  font-family: 'Manrope', sans-serif;
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-decoration: none;
  cursor: pointer;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  transition: background-color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
/* --- The primary button ---------------------------------------------------
 *
 * TWO TIERS, DELIBERATELY. The opener's button has to carry a whole screen on
 * its own — no headline, no paragraph, one thing to press. Every other button
 * on the site sits under copy that has already done the persuading. A button
 * that looks precious once and plain one screen later reads as a mistake, so
 * the difference is a hierarchy rather than an accident: the same Deep Teal
 * body and the same glass finish everywhere, and the light, the halo and the
 * cast shadow only where the button is the whole argument.
 *
 * ONE LIGHT SOURCE, upper right, shared with the opener's beam. The specular
 * sits on the top-right curve, the warm glow pools down-left, the shadow falls
 * down-left. Two light sources that disagree is the specific thing that makes
 * an otherwise expensive design read as amateur.
 *
 * WHY THERE ARE NO PSEUDO-ELEMENTS HERE ANY MORE. The glow used to be a
 * blurred ::before at z-index -1 inside `isolation: isolate`. That is not
 * behind the button: a negative-z-index child paints ON TOP of its parent's
 * own background and only below the parent's text. So the peach glow was
 * being drawn across the teal, which is the pale haze that looked like a
 * gradient smeared through the middle of the button. box-shadow cannot make
 * that mistake — it is defined to paint strictly outside the border box.
 */
.btn-primary {
  position: relative;
  background: var(--accent);            /* Deep Teal = interactive. Always. */
  color: #FFFFFF;                       /* 5.90:1 on the solid body */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),     /* specular, top edge */
    inset -1px 0 0 rgba(255, 255, 255, 0.10),    /* and along the lit right side */
    inset 0 -1px 0 rgba(0, 0, 0, 0.12),          /* the body turning away below */
    0 1px 2px rgba(18, 90, 85, 0.18),
    -1px 6px 14px -8px rgba(120, 82, 30, 0.34);  /* warm, never grey: grey on
                                                    Warm Ivory looks dirty */
}
.btn-primary:hover { background: var(--accent-deep); }
/* Pressed: the site-wide 0.985, so it reads as something physically pushed
   rather than a colour swap. */
.btn-primary:active { transform: scale(0.985); }
.btn-primary[disabled] { opacity: 0.55; cursor: not-allowed; transform: none; }

.btn-quiet {
  background: transparent;
  color: var(--accent);
  border-color: var(--line-strong);
}
.btn-quiet:hover { background: rgba(23, 112, 106, 0.06); border-color: var(--accent); }
.btn-quiet:active { transform: scale(0.985); }

.btn-full { width: 100%; }
.btn .spinner {
  width: 16px; height: 16px;
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-top-color: #FFFFFF;
  border-radius: var(--r-full);
  animation: spin 700ms linear infinite;
  display: none;
}
.btn.is-loading .spinner { display: block; }
.btn.is-loading { pointer-events: none; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Text links */
a { color: var(--accent); text-underline-offset: 0.18em; text-decoration-thickness: 1px; }
a:hover { color: var(--accent-deep); }

/* --- Status messages ---------------------------------------------------- */
.status-msg {
  margin-top: var(--s4);
  font-size: var(--t-small);
  min-height: 1.4em;
}
.status-msg[data-tone="error"] { color: #8E3A32; }
.status-msg[data-tone="ok"] { color: var(--ink); font-weight: 600; }

/* ==========================================================================
   THE OPENER — CHAOS, and the window that opens out of the A
   ==========================================================================

   ONE VARIABLE. page.js writes --open (0 to 1) on this section. Everything
   here derives from it, so a frame of animation costs one property write and
   touches neither layout nor paint — only transform, clip-path and opacity.

   THE GEOMETRY, because it is the part that is easy to break:

     --push       how far each half of the word travels from centre, and
                  simultaneously HALF the width of the open window. One value
                  for both is what keeps the letters exactly flush with the
                  window edge at every moment of the animation.

     --push-max   capped by the viewport, not chosen: the word must not slide
                  off screen. Half the word is about 1.7x its font-size in
                  Manrope ExtraBold, so the furthest it can travel is
                  50vw - half the word - a margin. On a 390px phone that lands
                  near 95px; on a desktop it hits the 310px ceiling instead,
                  which is what stops the window becoming a letterbox.

   At --open: 0 the two copies of the word sit exactly on top of one another
   and read as one solid CHAOS with a hairline of photograph down the A. */

/* The section is the RUNWAY. The inner sticks to the viewport and does the
   moving, so the word stays put while it opens instead of sliding off the top —
   which is what happened without it: measured, the window was still opening
   when the type had already left the screen.

   No overflow:hidden here. An overflow-hidden ancestor silently disables
   position:sticky, and the failure looks like the effect simply not working. */
.opener {
  position: relative;
  /* THE RUNWAY IS THE DIFFERENCE BETWEEN THESE TWO NUMBERS, and it is how far
     you can scroll while the opener stays pinned and the chaos keeps opening.
     It was 34vh — a third of a screen, over almost before it started. At 68vh
     the light has time to arrive, the word has time to come apart, and the
     hand-off into the page happens while the picture is still spreading, which
     is the part worth keeping. */
  min-height: 162vh;
  min-height: 162dvh;

  --open: 0;
  --word-size: clamp(2.9rem, 13.5vw, 6.5rem);

  /* THE WORD MAKES ROOM FOR THE PICTURE.
   *
   * The window can only ever be as wide as the gap the word opens, and the
   * word is as wide as it is — measured, not guessed: CHAOS renders 178.9px at
   * 390px, which left exactly 175px of window. A postage stamp. The two ways
   * to widen it are to let the outer letters run off the screen, which at
   * 320px eats most of the C and reads as a rendering fault, or to let the
   * word give up a little size as it opens. It does the second: 18% smaller by
   * the time the picture is fully out, which reads as the word yielding rather
   * than as the word being small.
   *
   * The estimator below is not a fudge factor. 1.7 x the font size is the
   * measured half-width of CHAOS in Manrope 800 at -0.035em, accurate to 0.1px
   * at 320, 390 and 430. */
  --word-end: 0.82;
  --word-now: calc(1 - (1 - var(--word-end)) * var(--open));
  --push-max: min(calc(50vw - (var(--word-size) * 1.7 * var(--word-end)) - 18px), 310px);
  /* FULLY CLOSED AT REST. This used to keep a 3px sliver of photograph showing
     in the A, on the theory that it hints there is something inside. On a real
     phone it just looked like a flaw in the type — you see the picture before
     you have done anything, and the word stops reading as a word. Nothing shows
     until you scroll. */
  --push: calc(var(--open) * var(--push-max));
  --frame-h: clamp(132px, 20vh, 300px);

  /* HOW FAR INTO THE PHOTOGRAPH THE WINDOW LOOKS.
   *
   * At zoom 1 the window shows the whole flat-lay, which on a phone means the
   * handwritten notes are 15px wide and their text is 6 device pixels tall —
   * present, and completely unreadable, which makes them worth nothing. The
   * note is the emotional half of the picture; the boxes are only the setup.
   *
   * 2.5x shows ~37% of the frame's width. That is still eleven or twelve
   * overlapping objects at every angle with no empty space, so it still reads
   * as disorder — the photograph was shot with even density across the frame
   * precisely so a centre crop would hold up — and "Did I take this one?" and
   * "2x daily?" both come up legible.
   *
   * It also costs nothing in sharpness: 37% of the 1600px master rendered into
   * a 3x phone window works out at almost exactly 1:1.
   *
   * FOCAL NUDGE. "Did I take this one?" sits at 45.6% / 51% of the master, so
   * the image is pushed right and up by a few percent to bring it to the
   * centre of the window. That matters more than it sounds: early in the
   * reveal the window is a narrow slit down the middle, so the note is the
   * first thing that appears, and the chaos spreads out around it. */
  --zoom: 2.5;
  --focal-x: 4.5%;
  --focal-y: -1.5%;
}

.opener-inner {
  position: sticky;
  top: 0;
  overflow: hidden;                 /* the outer rings must not make a scrollbar */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s6);
  /* Two declarations, not one: dvh is unsupported before Safari 15.4 and
     Chrome 108, and a first screen with no height on those browsers is blank.
     Same fallback pair the survey dialog needed. */
  min-height: 94vh;
  min-height: 94dvh;
  padding: var(--s6) var(--s4);
}

/* --- The light -----------------------------------------------------------
   One source, outside the frame past the top right corner, shared with the
   button below. It sits UNDER the stage, so it falls on the ivory and never on
   the photograph — that picture carries its own daylight, and a second
   lighting system on top of it is the fastest way to make a composition look
   wrong without being able to say why.

   A CONE, NOT A BLOB. This was a radial smudge in the corner, which reads as
   "something bright over there" rather than as light arriving from somewhere.
   A conic-gradient centred on a point outside the box IS an angular wedge —
   the endless triangle opening out from an unseen source — and a radial mask
   from the same point gives it the falloff with distance that a real beam has.
   Everything outside the wedge is dropped a few percent toward Ink, which is
   what makes the lit part read as lit rather than as merely pale.

   --beam-mid points the cone at the button. It has to be set per breakpoint:
   the source sits just off the top-right corner, so on a 390px phone the
   button is 24 degrees left of straight down and on a 1280px desktop it is
   55. One angle cannot serve both, and a beam that misses the thing it is
   supposed to be lighting is worse than no beam. */
.opener-light {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* WHERE THE SUN IS. Far enough out past the top-right corner that the shaft
     crosses the screen at a real diagonal — a source just outside the corner
     puts the button only 24 degrees off straight down on a phone, and a band
     that steep on a portrait screen reads as a gradient rather than as light
     coming through a window. */
  --beam-mid: 208deg;
  --beam-x: 135%;
  --beam-y: -25%;
  /* Warm, never grey. Reference 4 shades with cool grey because it sits on a
     grey surface; the same grey on Warm Ivory looks like dirt on the page. */
  --shade: rgba(80, 61, 42, 0.26);

  background:
    /* Where the shaft lands. A shaft that merely passes over the button lights
       the page; this pools on it, which is what makes it read as falling ON
       something. Sits above the shaft in the stack so it adds to it. */
    radial-gradient(46% 17% at 50% 62%,
      rgba(255, 253, 248, 0.92) 0%, rgba(255, 253, 248, 0) 74%),
    /* THE SHAFT. A conic gradient centred on a point outside the box IS an
       angular wedge — the endless triangle opening out from an unseen source.
       It has to be NARROWER than the box: seen from out there, a 390x700 phone
       spans about 79 degrees in total, so the first 80-degree version lit
       everything evenly and read as a faintly warm page.
       Seven degrees of transition either side is about 85px at the button's
       distance — a soft edge you can see the edge of, which is the difference
       between sunlight through a window and a glow. */
    conic-gradient(from 0deg at var(--beam-x) var(--beam-y),
      var(--shade) 0deg,
      var(--shade) calc(var(--beam-mid) - 13deg),
      rgba(255, 253, 248, 0.95) calc(var(--beam-mid) - 6deg),
      rgba(255, 253, 248, 1) calc(var(--beam-mid) + 6deg),
      var(--shade) calc(var(--beam-mid) + 13deg),
      var(--shade) 360deg);

  /* Distance falloff, from the same point. It has to still be strong where the
     button is: at 125%/110% the mask had already dropped the shaft to a fifth
     of its strength by the time it arrived, which is the same as not aiming it
     there at all. */
  /* Two masks, intersected. The first is the distance falloff from the source.
     The second is the reason the seam is gone: the light layer used to stop
     dead at the bottom of the opener, so the shade ended on a straight
     horizontal line across the page and the hero started as if someone had
     changed the wallpaper. It now dissolves over the last third of the
     section, and there is no edge to see. */
  /* THE BASE IS ONE MASK, AND IT IS DELIBERATELY THE WEAKER VERSION.
   *
   * This element is a full-bleed wash of warm shade. The masks are the only
   * thing making it look like light instead of like a stain over the page, so
   * what happens where they are not supported is not a cosmetic question.
   *
   * `mask-composite` has an initial value of `add`, which means a browser that
   * understands two mask layers but NOT the composite property takes their
   * UNION — and the union of these two is very nearly the whole box. The
   * result is the grey wash, on the first screen anyone sees. So the single
   * radial mask is the floor: supported everywhere that supports masking at
   * all, and correct on its own. The second layer is added back below, only
   * where it can be intersected. */
  -webkit-mask-image:
    radial-gradient(190% 175% at var(--beam-x) var(--beam-y),
      #000 0%, rgba(0, 0, 0, 0.95) 60%, rgba(0, 0, 0, 0.62) 88%, transparent 100%);
  mask-image:
    radial-gradient(190% 175% at var(--beam-x) var(--beam-y),
      #000 0%, rgba(0, 0, 0, 0.95) 60%, rgba(0, 0, 0, 0.62) 88%, transparent 100%);

  /* Arrives a beat after the word starts moving, and is fully up before the
     word finishes. One gesture, two events: the chaos opens, and then the room
     lights. Fading it in perfectly in step with the reveal made it feel like a
     property of the picture rather than something happening. */
  opacity: clamp(0, calc((var(--open) - 0.10) * 1.45), 1);
}

/* THE MACHINE COULD NOT AFFORD THE REVEAL.
   Set by the frame-budget guard in page.js when the measured median frame runs
   over about 28ms. --open is pinned to 1 by the same code, so the composition
   is already in its finished state; this only stops anything here from
   animating toward a value that is no longer going to change, and releases the
   compositor hint. The visitor gets the picture instantly instead of a
   stutter — the photograph is the content, the animation never was. */
.opener.is-degraded .opener-frame,
.opener.is-degraded .opener-word,
.opener.is-degraded .opener-light,
.opener.is-degraded .opener-rings span {
  transition: none !important;
  animation: none !important;
  will-change: auto;
}

/* NO MASKING AT ALL: NO LIGHT LAYER AT ALL.
   Without a mask this element is an unbroken wash of warm shade across the
   first screen. A missing atmospheric effect is invisible to somebody who has
   never seen the effect; a stain over the opening composition is not. So the
   layer only exists where it can be shaped. */
@supports not ((-webkit-mask-image: radial-gradient(#000, transparent)) or
               (mask-image: radial-gradient(#000, transparent))) {
  .opener-light { display: none; }
}

/* THE SECOND MASK, ADDED BACK ONLY WHERE IT CAN BE INTERSECTED.
   It dissolves the layer at BOTH ends of the section. The bottom fade has
   always been here. The top fade is newer and it fixes a real report: the
   light layer begins where the opener section begins, which is directly under
   the header, so its top edge is a hard horizontal line across the page right
   below the wordmark and the language switch. At full strength that line is a
   wide stripe — invisible in normal use only because --open is 0 at the top of
   the page and the layer is transparent there. The moment the opener is shown
   already-open at scroll 0, which is exactly what a slow machine gets, the
   stripe appears. Fading the first 11% costs a little intensity at the beam's
   source and removes the edge completely. Without it the
   shade stops dead on a horizontal line and the hero below starts as if
   somebody changed the wallpaper — a visible seam, but a seam is a far smaller
   failure than a wash, which is why this is the enhancement and not the base.
   The two blocks set different properties, so a browser matching both (any
   current Chrome) correctly gets both. */
@supports (mask-composite: intersect) {
  .opener-light {
    mask-image:
      radial-gradient(190% 175% at var(--beam-x) var(--beam-y),
        #000 0%, rgba(0, 0, 0, 0.95) 60%, rgba(0, 0, 0, 0.62) 88%, transparent 100%),
      linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.55) 4%, #000 11%, #000 74%, rgba(0, 0, 0, 0.5) 90%, transparent 100%);
    mask-composite: intersect;
  }
}
@supports (-webkit-mask-composite: source-in) {
  .opener-light {
    -webkit-mask-image:
      radial-gradient(190% 175% at var(--beam-x) var(--beam-y),
        #000 0%, rgba(0, 0, 0, 0.95) 60%, rgba(0, 0, 0, 0.62) 88%, transparent 100%),
      linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.55) 4%, #000 11%, #000 74%, rgba(0, 0, 0, 0.5) 90%, transparent 100%);
    -webkit-mask-composite: source-in;
  }
}

/* --- The rings -----------------------------------------------------------
   Calm order radiating out of the chaos, drawn in the brand's own stadium
   shape. Calm Teal is the correct token: the palette lock reserves it for
   decoration and forbids it as text or as a control, which is exactly this.

   TWO THINGS WERE WRONG, both visible on a phone and neither on a mock.

   1. WRONG CENTRE. These lived in .opener-inner, whose box is the stage PLUS
      the gap PLUS the button — so its centre sits roughly half a button-height
      below the photograph, and the rings appeared to radiate from a point just
      under the picture rather than out of it. They now live inside the stage,
      whose centre is exactly the frame's centre.

   2. WRONG SHAPE. Each ring grew by 12vw horizontally and 9vh vertically, which
      on a 390x745 phone is 47px against 67px — so every step stretched them
      taller than wider and the outer rings became portrait ovals around a
      landscape pill. A concentric outset of a stadium adds the SAME distance on
      every side, so there is one step now, used for both axes. */
.opener-rings {
  position: absolute;
  inset: 0;
  z-index: 0;                       /* below the frame and the type */
  pointer-events: none;
}
.opener-rings span {
  position: absolute;
  left: 50%;
  top: 50%;
  border: 1px solid rgba(77, 182, 172, 0.22);
  border-radius: 999px;
  /* One step, both axes — a true concentric outset of the window. */
  --step: clamp(24px, 6.5vmin, 58px);
  width: calc((var(--push-max) * 2) + (var(--ring) * 2 * var(--step)));
  height: calc(var(--frame-h) + (var(--ring) * 2 * var(--step)));
  opacity: calc(var(--open) * (1 - (var(--ring) - 1) * 0.15) * 0.55);
  /* Explicit centring rather than relying on a grid's static position for an
     absolutely positioned child, which is correct but far too subtle to leave
     load-bearing. */
  transform: translate(-50%, -50%) scale(calc(0.86 + var(--open) * 0.14));
}
.opener-rings span:nth-child(1) { --ring: 1; }
.opener-rings span:nth-child(2) { --ring: 2; }
.opener-rings span:nth-child(3) { --ring: 3; }
.opener-rings span:nth-child(4) { --ring: 4; }
.opener-rings span:nth-child(5) { --ring: 5; }
.opener-rings span:nth-child(6) { --ring: 6; }

/* --- The stage: the word and the window ---------------------------------- */
.opener-stage {
  position: relative;
  z-index: 1;                       /* above the light, so the photo stays unlit */
  width: 100%;
  height: calc(var(--frame-h) + var(--s5));
  display: grid;
  place-items: center;
}

.opener-frame {
  position: absolute;
  z-index: 1;                       /* above the rings, below the type */
  left: 50%;
  top: 50%;
  width: calc(var(--push-max) * 2);
  height: var(--frame-h);
  transform: translate(-50%, -50%);
  /* THE WHOLE SHAPE PROGRESSION, FOR FREE. `round 999px` caps at half the
     shorter side, so the window is a rounded hairline while it is narrow, a
     circle the moment its width equals its height, and a horizontal pill after
     that — the reference's exact arc with no maths and no image resampling.
     The photograph never moves or scales; the window over it opens. */
  clip-path: inset(0 calc(50% - var(--push)) 0 calc(50% - var(--push)) round 999px);
  /* The zoomed image is larger than this box. clip-path already contains it,
     but only while it is a clip-path — this survives anyone simplifying it. */
  overflow: hidden;
}
.opener-frame picture {
  display: block;
  width: 100%;
  height: 100%;
}
/* Laid out at the zoomed size rather than transform-scaled. A transform scales
   a raster the browser already chose a resolution for; changing the layout box
   makes the browser pick the right source for the size it will actually be
   drawn at, which is the whole point of the crop. */
.opener-frame img {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(100% * var(--zoom));
  height: calc(100% * var(--zoom));
  /* The global `img { max-width: 100% }` reset silently caps the width here and
     leaves the height zoomed, which crops vertically instead of magnifying and
     looks exactly like the zoom having no effect. */
  max-width: none;
  object-fit: cover;
  transform: translate(calc(-50% + var(--focal-x)), calc(-50% + var(--focal-y)));
}

.opener-word {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 2;
  font-family: 'Manrope', sans-serif;
  font-weight: 800;
  font-size: var(--word-size);
  line-height: 1;
  letter-spacing: -0.035em;
  color: var(--ink);
  white-space: nowrap;
  user-select: none;
}
/* Each copy is clipped to one side of its own centre — which, in a five-letter
   word, falls inside the A. That is the whole trick: two spans could never
   give you half a glyph. */
/* THE HALF-PIXEL OVERLAP IS THE POINT.
 *
 * Clipping one copy at exactly 50% and the other at exactly 50% leaves the two
 * halves meeting on a subpixel boundary, and a browser rounds that gap either
 * open or closed depending on the device pixel ratio and the current transform.
 * On a phone it rendered as a faint pale seam straight down the A — the word
 * looked slightly broken before anything had happened.
 *
 * Each copy now keeps half a pixel MORE than its half, so the two always
 * overlap rather than ever meeting exactly. Invisible when open (a letter
 * overhangs its window edge by half a pixel), and at rest the A is solid. */
/* scale() comes last, so it happens about each copy's own centre — which is
   its clip edge. That is why shrinking the word does not move the seam: the
   window edge stays at exactly +/- --push whatever the scale is. */
.opener-word-l {
  clip-path: inset(0 calc(50% - 0.5px) 0 0);
  transform: translate(calc(-50% - var(--push)), -50%) scale(var(--word-now));
}
.opener-word-r {
  clip-path: inset(0 0 0 calc(50% - 0.5px));
  transform: translate(calc(-50% + var(--push)), -50%) scale(var(--word-now));
}

/* --- The button ---------------------------------------------------------
   Rises in with the reveal, and reaches full strength at a third of the way
   through so one flick is enough to bring it in.

   visibility, not just opacity. A faded-out button that is still tappable is
   the ghost-target bug tests/ghosts.js exists to catch, and it would be
   sitting on the single most valuable screen on the site. */
.opener-cta {
  position: relative;
  z-index: 2;
  opacity: clamp(0, calc(var(--open) * 3), 1);
  transform: translateY(calc((1 - clamp(0, calc(var(--open) * 3), 1)) * 14px));
  visibility: hidden;
  pointer-events: none;

  /* GLASS, MADE OF THE TEAL ITSELF. Not a white sheen laid over the top — that
     is the thing that looked like a smear. The teal simply thins where the
     light enters, top right, and closes back to solid away from it, so the
     body reads as a tinted pane with light passing through its near edge.
     0.90 is the floor: white on 90% Deep Teal over white is 4.84:1, and the
     label sits where it is denser still. Anything thinner fails AA. */
  background: linear-gradient(206deg,
    rgba(23, 112, 106, 0.90) 0%,
    rgba(23, 112, 106, 0.97) 46%,
    rgba(23, 112, 106, 1) 100%);

  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.34),    /* specular along the lit curve */
    inset -1.5px 0 0 rgba(255, 255, 255, 0.18),
    inset 0 -1.5px 0 rgba(0, 0, 0, 0.14),
    /* THE HALO HAS TO STAY OFF THE SHADOW'S GROUND. box-shadows paint in
       source order, first on top — so a 26px pool of Soft Peach laid down
       before the cast shadow simply covers it, and the button ends up glowing
       on a page instead of standing in front of a lit wall. Reference 4 keeps
       its warm light tight against the outline and gives the shadow the room.
       Rim and bleed only; nothing peach reaches past about 8px. */
    0 0 0 1px rgba(255, 205, 145, 0.55),          /* the edge itself, shining */
    0 0 6px 0 rgba(255, 198, 128, 0.70),          /* hugging the outline */
    -2px 3px 8px -1px rgba(255, 214, 165, 0.78),  /* the sliver escaping beneath */
    /* THE SHADOW IT CASTS. Two: a tight one just under the lower edge that says
       how far off the page it is floating, and a long soft one thrown down-left
       away from the source. A shadow only reads as a shape if it is allowed out
       from under the thing casting it — negative spread pulls it back
       underneath, which is exactly where a shadow is invisible. */
    -7px 9px 11px -3px rgba(74, 54, 32, 0.46),
    -23px 35px 32px -6px rgba(74, 54, 32, 0.66);
}
.opener.is-open .opener-cta {
  visibility: visible;
  pointer-events: auto;
}
.opener-cta:hover {
  background: linear-gradient(206deg,
    rgba(18, 90, 85, 0.92) 0%, rgba(18, 90, 85, 0.98) 46%, rgba(18, 90, 85, 1) 100%);
}
/* Pressed, the halo flares. A one-off on tap, not a loop — the four-second
   breathe that used to live here was a second thing moving on a screen whose
   whole job is one gesture. */
.opener-cta:active {
  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, 0.34),
    inset -1.5px 0 0 rgba(255, 255, 255, 0.18),
    inset 0 -1.5px 0 rgba(0, 0, 0, 0.14),
    0 0 0 1px rgba(255, 214, 165, 0.75),
    0 0 9px 0 rgba(255, 210, 150, 0.95),
    -2px 3px 10px 0 rgba(255, 226, 186, 0.95),
    -3px 4px 6px -2px rgba(92, 70, 46, 0.28),
    -13px 20px 20px -6px rgba(92, 70, 46, 0.38);
}

/* --- The quieter second path --------------------------------------------
   The opener asks for one thing and offers no way to look first. The hero has
   had "Sound familiar?" under it since the beginning for exactly that reason,
   and this is the same affordance one screen earlier: press the button, or go
   and see what it is. It lags the button rather than arriving with it, so the
   eye lands on the ask first and finds the alternative a beat later. */
/* Not a link and not a button — a line of plain text under the button, on the
   lit ivory. It used to carry a chevron in a circle, which made two controls
   out of one choice. */
.opener-more {
  position: relative;
  z-index: 2;
  margin: calc(var(--s3) * -1) 0 0;
  font-size: var(--t-fine);
  font-weight: 600;
  color: var(--muted);
  opacity: clamp(0, calc((var(--open) - 0.30) * 3.5), 1);
  visibility: hidden;
}
.opener.is-open .opener-more { visibility: visible; }
.opener-more:hover { background: rgba(23, 112, 106, 0.06); }
.opener-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (min-width: 860px) {
  .opener-inner { gap: var(--s7); }
  .opener-cta { font-size: 1.125rem; padding: var(--s4) var(--s7); }
  /* A 620px window needs far less help than a 207px one — the notes are
     already 55px wide here. 1.3x is enough to make them comfortable and keeps
     the 1600px master above 2x on a retina desktop. */
  .opener { --zoom: 1.3; }
  /* The source stays just off the top-right corner, but a 1280px screen puts
     the button much further to its left — 55 degrees off straight down rather
     than 24. Same light, re-aimed. */
  .opener-light { --beam-mid: 240deg; }
}

/* Short viewports, which is most real phones once Safari's chrome is counted.
   The opener gives up height before the hero does, because the hero has a
   headline and a button to fit and this has a picture that can simply be a
   little smaller. */
@media (max-height: 820px) and (max-width: 860px) {
  /* The window grew wider with the word shrink, so it gets proportionally
     taller too — a stadium that gains width without height stops being a
     stadium and becomes a letterbox. 20vh keeps it near the 1.4 aspect the
     reveal's final shape wants at 745px tall, and the 126px floor stops a
     320x640 phone from squaring it off. */
  .opener { min-height: 152vh; min-height: 152dvh; --frame-h: clamp(126px, 20vh, 220px); }
  .opener-inner { min-height: 92vh; min-height: 92dvh; gap: var(--s5); }
}

@media (prefers-reduced-motion: reduce) {
  /* Fully open on load, and nothing moves. The photograph is the content; the
     animation is not. */
  .opener { --open: 1; }
  .opener-cta { transform: none; }
  /* Still centred — dropping the transform entirely would have knocked every
     ring half its own size down and to the right. */
  .opener-rings span { transform: translate(-50%, -50%); }
}

/* --- Sticky mobile CTA --------------------------------------------------
   §8.1 requires the primary action inside the natural thumb arc. A fixed bar
   is the only way to guarantee that at any scroll position. It hides once the
   real form is on screen so it never competes with it or fights the keyboard. */
.sticky-cta {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 20;
  padding: var(--s3) var(--s5);
  padding-bottom: calc(var(--s3) + env(safe-area-inset-bottom, 0px));
  /* Was rgba(...,0.92) plus a 12px backdrop-filter. Behind a bar that is
     already 92% opaque a blur is doing almost nothing you can see, and it
     costs a live composited layer on every scroll — on integrated graphics
     that is a real per-frame tax paid for an effect nobody can name. Raised to
     0.96 instead: visually indistinguishable, free. */
  background: rgba(250, 247, 242, 0.96);
  border-top: 1px solid var(--line);

  /* PARKED CLEAR OF THE FOLD, AND NOT PAINTED.
   *
   * This was translateY(110%), which is ten per cent of the bar's OWN height —
   * measured, that left its top edge eight pixels below the viewport. Eight
   * pixels is less than Safari's collapsing URL bar moves, less than a
   * rubber-band overscroll, and far less than the gap that opens when iOS
   * raises the keyboard while keeping the layout viewport at full height. Any
   * of those dragged a bar that was supposed to be hidden back into view,
   * which is exactly the reported "it floated in the middle of the screen".
   *
   * Two changes, and the second is the one that actually closes it: park it a
   * real distance away, and take it out of the paint and hit-testing tree
   * entirely while hidden. A bar that is `visibility: hidden` cannot appear
   * anywhere, however the viewport moves under it — and it cannot swallow a
   * tap aimed at whatever is behind it either.
   *
   * visibility is transitioned with a delay equal to the slide, so hiding
   * still animates out and only then stops rendering; showing is immediate. */
  transform: translateY(calc(100% + 48px + env(safe-area-inset-bottom, 0px)));
  visibility: hidden;
  pointer-events: none;
  transition: transform var(--dur-base) var(--ease-out),
              visibility 0s linear var(--dur-base);
}
.sticky-cta.is-visible {
  transform: translateY(0);
  visibility: visible;
  pointer-events: auto;
  transition: transform var(--dur-base) var(--ease-out), visibility 0s;
}
.sticky-cta .btn { width: 100%; }
@media (min-width: 860px) { .sticky-cta { display: none; } }
@media (prefers-reduced-transparency: reduce) {
  .sticky-cta { background: var(--base); backdrop-filter: none; -webkit-backdrop-filter: none; }
}

/* --- Skip link: hidden until focused, then a real, visible target ------- */
.skip-link {
  position: absolute;
  left: var(--s4);
  top: -100px;
  z-index: 30;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--s2) var(--s4);
  background: var(--accent);
  color: #FFFFFF;
  font-weight: 600;
  text-decoration: none;
  border-radius: var(--r-md);
  transition: top var(--dur-fast) var(--ease-out);
}
.skip-link:focus-visible { top: var(--s3); color: #FFFFFF; outline-offset: 3px; }

/* --- Footer ------------------------------------------------------------- */
.site-footer {
  padding-block: var(--s7);
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--t-fine);
}
.site-footer p { max-width: none; }
.site-footer p + p { margin-top: var(--s2); }
/* The medical disclaimer. Quiet, but it must stay legible — Muted on Base is
   5.51:1, so it holds AA at this size without shouting. */
.site-footer .footer-note {
  max-width: 62ch;
  margin-top: var(--s3);
  line-height: 1.55;
}
/* Inline footer links still need a 44px touch target (§8.2). */
.site-footer a { display: inline-block; padding-block: var(--s3); }
.site-footer .footer-links { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5); align-items: center; }
body.has-sticky .site-footer { padding-bottom: calc(var(--s7) + 72px); }
@media (min-width: 860px) { body.has-sticky .site-footer { padding-bottom: var(--s7); } }

/* --- Video -------------------------------------------------------------- */
.video-frame {
  margin-top: var(--s6);
  border-radius: var(--r-lg);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--surface);
  aspect-ratio: 16 / 10;
}
.video-frame video { width: 100%; height: 100%; object-fit: cover; }

/* --- Reveal on scroll: opacity + 16px, once, never on scroll-back ------- */
.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
}
.reveal.is-in { opacity: 1; transform: none; }

/* --- Reduced motion: cross-fade only, no travel, no elastic ------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .reveal { opacity: 1; transform: none; transition: none; }
  .sticky-cta { transition: none; }
  .btn-primary:active { transform: none; }
}

/* --- Confirmation page -------------------------------------------------- */
/* Flows from the top. Vertically centring this would look tidy on a tall
   window but pushes the top of the content off-screen on a phone, where the
   content is taller than the viewport. */
.confirm { padding-block: var(--s6) var(--s9); }
.confirm-keppy { width: 132px; margin-bottom: var(--s5); border-radius: var(--r-full); }
.confirm h1 { margin-bottom: var(--s4); }
.confirm .lede { margin-bottom: var(--s6); }
.confirm-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: var(--s6) var(--s5);
  max-width: 560px;
  margin-top: var(--s7);
}
@media (min-width: 768px) { .confirm-card { padding: var(--s7); } }
.choice-grid { display: grid; gap: var(--s2); margin-top: var(--s4); }
@media (min-width: 480px) { .choice-grid { grid-template-columns: 1fr 1fr; } }
.choice {
  display: flex;
  align-items: center;
  min-height: 48px;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  font-size: var(--t-small);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}
.choice:hover { border-color: var(--accent); background: rgba(23, 112, 106, 0.04); }
.choice input { position: absolute; opacity: 0; pointer-events: none; }
/* The selected state, said twice on purpose.
 *
 * The real input above is `opacity: 0`, so one of these rules is the ONLY thing
 * telling somebody which answers they have chosen. `:has()` is the clean way
 * and it is too new: Firefox only shipped it in 121 and Safari in 15.4. On
 * anything older this rule is dropped, and a visitor taps answers and sees
 * absolutely nothing happen — on the survey the whole page exists to collect.
 *
 * `.is-checked` is mirrored onto the label by reserve.js. The survey is
 * JavaScript-driven anyway, so it is always available where it is needed. */
.choice:has(input:checked),
.choice.is-checked {
  border-color: var(--accent);
  background: rgba(23, 112, 106, 0.08);
  font-weight: 600;
}
/* :focus-within is the fallback here — far older than :has() (Safari 10.1,
   Firefox 52) and close enough in behaviour for a visually hidden input. */
.choice:has(input:focus-visible),
.choice:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- Legal prose (privacy) ---------------------------------------------- */
.prose { padding-block: var(--s6) var(--s9); }
.prose h1 { margin-bottom: var(--s4); }
.prose h2.section { margin-top: var(--s7); margin-bottom: var(--s3); font-size: var(--t-h3); }
.prose p { margin-top: var(--s3); }
.prose .lede { margin-bottom: var(--s6); }
.prose ul { margin-top: var(--s3); padding-left: var(--s5); max-width: var(--measure); }
.prose li { margin-top: var(--s2); }
/* The standalone "Back" link at the end of a legal page. Inline links inside
   the prose are exempt from the 44px rule (WCAG 2.2 SC 2.5.8 exempts targets
   in a sentence), but this one is on its own and gets a real target. */
.prose > footer { margin-top: var(--s7); }
.prose > footer a { display: inline-block; padding: var(--s3) 0; min-height: 44px; }

/* --- Utilities ---------------------------------------------------------- */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* A form control inside a clipped wrapper still lays out at its natural width.
   Nothing renders and scrollWidth is unaffected — but it trips element-bounds
   overflow audits, so constrain it and keep the reports honest. */
.visually-hidden input { width: 1px; }
.stack-sm > * + * { margin-top: var(--s3); }
.center { text-align: center; }

/* ==========================================================================
   Currency — EUR by default; CHF, GBP and USD for visitors those apply to.
   Every version is in the markup and currency.js sets data-cur in <head>
   before first paint, so the most important number on the page is never
   rendered wrong and then corrected. A visible flip on the price would cost
   more trust than the localisation buys.

   Written as "hide all, then show the one that matches" rather than a chain of
   overrides — adding a fifth currency is then one line, not four.
   ========================================================================== */
.cur { display: none; }
:root:not([data-cur]) .cur-eur,
:root[data-cur="eur"] .cur-eur,
:root[data-cur="chf"] .cur-chf,
:root[data-cur="gbp"] .cur-gbp,
:root[data-cur="usd"] .cur-usd { display: inline; }

/* ==========================================================================
   Motion — Brand Identity §22: breathing, settling, gentle.
   Nothing bouncy, nothing flashy. Only transform and opacity, so every
   animation stays on the compositor. All of it is switched off wholesale by
   the prefers-reduced-motion block further up this file.
   ========================================================================== */

/* --- Hero entrance: one settling sequence, not a performance ------------- */
@keyframes settle-up {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
.hero h1,
.hero .lede,
.hero .hero-actions,
.hero-keppy-wrap {
  animation: settle-up var(--dur-slow) var(--ease-out) both;
}
.hero h1            { animation-delay: 40ms; }
.hero .lede         { animation-delay: 140ms; }
.hero .hero-actions { animation-delay: 240ms; }
.hero-keppy-wrap    { animation-delay: 180ms; }

/* Keppy's idle drift (§22) lives with the rest of his rules, further down under
   "Keppy's expressions". A second copy used to sit here and was dead code: the
   later rule shares this selector and this specificity, so it won outright. */

/* --- Staggered reveals: each group settles in sequence ------------------ */
.questions li:nth-child(1) { transition-delay: 0ms; }
.questions li:nth-child(2) { transition-delay: 70ms; }
.questions li:nth-child(3) { transition-delay: 140ms; }
.questions li:nth-child(4) { transition-delay: 210ms; }
.questions li:nth-child(5) { transition-delay: 280ms; }
.steps .step:nth-child(2)  { transition-delay: 90ms; }
.steps .step:nth-child(3)  { transition-delay: 180ms; }
.trust-item:nth-child(2)   { transition-delay: 60ms; }
.trust-item:nth-child(3)   { transition-delay: 120ms; }
.trust-item:nth-child(4)   { transition-delay: 180ms; }

/* --- Step cards lift very slightly on pointer devices ------------------- */
@media (hover: hover) and (pointer: fine) {
  .step {
    transition: transform var(--dur-base) var(--ease-out),
                border-color var(--dur-base) var(--ease-out),
                box-shadow var(--dur-base) var(--ease-out);
  }
  .step:hover {
    transform: translateY(-3px);
    border-color: var(--line-strong);
    box-shadow: 0 8px 24px rgba(52, 66, 74, 0.06);
  }
}

/* --- The illustrations come alive only once their card is on screen ----- */
@keyframes sweep {
  0%, 100% { transform: translateY(-16px); opacity: 0; }
  15%, 85% { opacity: 1; }
  50%      { transform: translateY(16px); }
}
.step.is-in .sc-scan { animation: sweep 3.2s var(--ease-inout) 400ms infinite; }
.sc-scan { opacity: 0; }

@keyframes corner-pulse {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1; }
}
.step.is-in .sc-corners { animation: corner-pulse 3.2s var(--ease-inout) 400ms infinite; }

@keyframes shutter-press {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(0.9); }
}
.step.is-in .sc-shutter { animation: shutter-press 3.2s var(--ease-inout) 400ms infinite; transform-origin: 120px 145px; }

/* Step 2 and 3 illustrations are animated further down, in the round-two block. */

/* --- The price settles in rather than simply appearing ------------------ */
.price-settle { opacity: 0; transform: scale(0.965); transform-origin: left center; }
.ask-card.is-in .price-settle {
  animation: price-in 560ms var(--ease-out) 120ms forwards;
}
@keyframes price-in {
  from { opacity: 0; transform: scale(0.965); }
  to   { opacity: 1; transform: none; }
}

/* --- Keppy easter egg (Brand Identity §25) ------------------------------ */
.hero-keppy-wrap {
  position: relative;
  justify-self: center;
  /* Clearance for the speech bubble. Without it the bubble covers the
     "Nothing is charged today · Launching October 2026" line, and that
     disclosure must never be obscured (§2.3). */
  margin-top: var(--s7);
}
@media (min-width: 860px) { .hero-keppy-wrap { justify-self: end; margin-top: 0; } }

/* Keppy has two voices — the invitation and the reply — and they share one
   surface, anchored to the same point above his head, so the chip reads as the
   bubble before it has anything to say. No hairline border: at this size a 1px
   line makes him look like a UI dialog instead of someone speaking. The soft
   layered shadow does the separating instead. */
.keppy-hint,
.keppy-say {
  position: absolute;
  left: 50%;
  bottom: calc(100% - 14px);
  margin: 0;
  background: var(--surface);
  text-align: center;
  pointer-events: none;
  box-shadow:
    0 1px 2px rgba(52, 66, 74, 0.05),
    0 10px 28px -6px rgba(52, 66, 74, 0.16);
}
/* The tail is the same surface rotated, so there is no seam and no second
   shadow to line up. Deliberately unshadowed — it sits against Keppy. */
.keppy-hint::after,
.keppy-say::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  background: var(--surface);
}

.keppy-hint {
  /* Both this and the button are positioned, and this comes first in the DOM,
     so without a z-index Keppy paints straight over it. */
  z-index: 3;
  transform: translate(-50%, 5px);
  padding: 7px 15px;
  border-radius: var(--r-full);
  font-size: var(--t-fine);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
  opacity: 0;
  /* IT WAITS AS LONG AS IT TAKES.
   *
   * This used to fade in at 2s, hold, and retire on its own at 21s. That was
   * already the second attempt — it started at 11s — and the timer was always
   * the wrong instrument, because it starts when the PAGE loads and the
   * invitation is for somebody who has not arrived yet.
   *
   * The opener made that unarguable. The hero is the second screen now: a
   * visitor spends time opening the chaos, presses nothing, scrolls down, and
   * finds a character with no indication that he does anything. The one moment
   * of play on the page, missed, because a clock they never saw ran out.
   *
   * So it fades in and stays. The only thing that retires it is the thing it
   * is asking for. */
  transition: opacity var(--dur-base) var(--ease-out);
  animation: hint-in 700ms var(--ease-out) 1.4s both;
}
.keppy-hint::after {
  width: 11px; height: 11px;
  border-radius: 0 0 3px 0;
  transform: translate(-50%, -7px) rotate(45deg);
}
@keyframes hint-in {
  from { opacity: 0; transform: translate(-50%, 5px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
/* Once they have actually tapped him, stop asking. */
.hero-keppy-wrap.is-tapped .keppy-hint { animation: none; opacity: 0; }

.keppy-btn {
  position: relative;
  display: block;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  border-radius: var(--r-xl);
  -webkit-tap-highlight-color: transparent;
}
.keppy-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 6px; }
.keppy-btn:active .keppy-stack { transform: scale(0.97); }

/* --- Keppy's expressions ------------------------------------------------
   Each overlay is the visor and nothing else, cut with the same mask as the
   base, so it lands exactly on his screen. Percentages come straight out of
   the generator: the visor sits at x180 y120, 389px wide, in a 656x920 base.
   Nothing here changes layout — only opacity — so a face change costs a
   composite and nothing more. */
.keppy-stack { position: relative; display: block; }
.keppy-face {
  position: absolute;
  left: 27.439%;
  top: 13.043%;
  width: 59.299%;
  height: auto;
  opacity: 0;
  pointer-events: none;
  transition: opacity 260ms var(--ease-inout);
}
.keppy-face.is-on { opacity: 1; }

/* The closed-eye frame is the blink, so it moves faster than the others. 150ms
   in, 600ms held, 150ms out. The laugh and the glance keep the softer 260ms:
   they are moods, and a mood that snaps on looks startled. */
.keppy-face[data-face="content"] { transition-duration: 150ms; }

/* The round head in "What changes" is a different crop, so the visor sits
   somewhere else inside it. */
.keppy-stack-head > img:first-child { border-radius: var(--r-full); }
.keppy-stack-head .keppy-face { left: 15.357%; top: 21.429%; width: 69.464%; }

/* Alive while nothing is happening. Three pixels over five seconds is below the
   threshold you would consciously notice and above the one that reads as a
   photograph. Transform only, so it never triggers layout.

   This is the ONLY breathe rule. There were briefly two on this exact selector
   — a 13px sway further up the file and this one — and because they had equal
   specificity the later simply switched the earlier off, silently. The sway is
   gone and its transform-origin and transition live here now. */
@keyframes keppy-breathe {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-3px); }
}
.hero-keppy-wrap .keppy-stack {
  animation: keppy-breathe 5s var(--ease-inout) infinite;
  transform-origin: 50% 90%;   /* settles from the feet, like shifting weight */
  transition: transform var(--dur-base) var(--ease-out);
  will-change: transform;
}
@media (hover: hover) and (pointer: fine) {
  /* Matched to a 3px idle. It used to lift 13px, which was in proportion to the
     old sway and is four times the movement of the one that actually ships. */
  .keppy-btn:hover .keppy-stack {
    animation-play-state: paused;
    transform: translateY(-4px) scale(1.02);
  }
}

/* No face-swapping and no drift if the visitor asked for less motion — he
   simply keeps his resting smile. This matters more here than on most sites:
   the people most likely to have it switched on, older users and people with
   neurological conditions, are disproportionately this audience.

   The one exception is Q6, further down: an expression change there is a direct
   answer to something the visitor just did, and reduced motion means less
   movement, not less feedback. */
@media (prefers-reduced-motion: reduce) {
  .keppy-face { display: none; }
  .hero-keppy-wrap .keppy-stack { animation: none; }
}

.keppy-say {
  z-index: 4;                   /* same stacking problem as the hint */
  width: max-content;
  max-width: 14.5rem;
  padding: var(--s3) var(--s4);
  border-radius: 18px;
  color: var(--ink);            /* never Deep Teal — Keppy is not a control */
  font-size: var(--t-small);
  line-height: 1.45;
  text-wrap: balance;
  opacity: 0;
  /* Grows out of the tail, so it reads as coming from him rather than
     fading in over him. */
  transform: translate(-50%, 8px) scale(0.96);
  transform-origin: 50% 100%;
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
.keppy-say::after {
  width: 14px; height: 14px;
  border-radius: 0 0 4px 0;
  transform: translate(-50%, -10px) rotate(45deg);
}
.keppy-say.is-shown { opacity: 1; transform: translate(-50%, 0) scale(1); }

/* ==========================================================================
   Round two — richness that stays calm.
   ========================================================================== */

/* --- The questions that don't switch off -------------------------------
   A soft highlight travels down the list on a loop. The section is about a
   thought that keeps coming back, so the motion says the same thing the words
   do. Hovering or focusing a line takes it over from the loop. */
.questions li {
  position: relative;
  border-left-color: var(--ivory-shade);
  color: var(--muted);
  transition: color var(--dur-base) var(--ease-out),
              border-left-color var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
.questions li.is-lit,
.questions li:hover,
.questions li:focus-within {
  color: var(--ink);
  border-left-color: var(--tint);
  transform: translateX(5px);
}
.questions-after {
  font-weight: 600;
  color: var(--ink);
}

/* --- Step 2: rows arrive, then confirm one after another ---------------- */
.cf-row { opacity: 0; }
.cf-dot { transition: none; }
.step.is-in .cf-row   { animation: row-in 4.8s var(--ease-out) infinite; }
.step.is-in .cf-row-2 { animation-delay: 260ms; }
.step.is-in .cf-row-3 { animation-delay: 520ms; }
@keyframes row-in {
  0%          { opacity: 0; transform: translateX(-10px); }
  12%, 88%    { opacity: 1; transform: none; }
  100%        { opacity: 1; transform: none; }
}
.step.is-in .cf-row .cf-dot  { animation: dot-confirm 4.8s var(--ease-out) infinite; }
.step.is-in .cf-row-2 .cf-dot { animation-delay: 260ms; }
.step.is-in .cf-row-3 .cf-dot { animation-delay: 520ms; }
@keyframes dot-confirm {
  0%, 22%   { fill: #E7DFD3; }
  30%, 100% { fill: #73AFAF; }
}
.sc-check { stroke-dasharray: 12; stroke-dashoffset: 12; }
.step.is-in .cf-row .sc-check { animation: draw-check-loop 4.8s var(--ease-out) infinite; }
.step.is-in .cf-row-2 .sc-check { animation-delay: 260ms; }
.step.is-in .cf-row-3 .sc-check { animation-delay: 520ms; }
@keyframes draw-check-loop {
  0%, 24%   { stroke-dashoffset: 12; }
  36%, 100% { stroke-dashoffset: 0; }
}

/* --- Step 3: the reminder lands, the stock runs down, the warning shows -- */
.rm-card { opacity: 0; }
.step.is-in .rm-card { animation: card-land 5.6s var(--ease-out) infinite; }
@keyframes card-land {
  0%       { opacity: 0; transform: translateY(-8px); }
  10%, 92% { opacity: 1; transform: none; }
  100%     { opacity: 1; transform: none; }
}
.step.is-in .rm-hand { animation: hand-tick 5.6s steps(12) infinite; transform-origin: 64px 52px; }
@keyframes hand-tick { to { transform: rotate(360deg); } }

.sc-bar { transform: scaleX(1); transform-origin: 58px 0; }
.step.is-in .sc-bar { animation: run-down 5.6s var(--ease-inout) infinite; }
@keyframes run-down {
  0%, 12%  { transform: scaleX(1); }
  60%      { transform: scaleX(0.24); }
  92%      { transform: scaleX(0.24); }
  100%     { transform: scaleX(1); }
}
.rm-alert { opacity: 0; transform-origin: 172px 114px; }
.step.is-in .rm-alert { animation: alert-in 5.6s var(--ease-out) infinite; }
@keyframes alert-in {
  0%, 58%   { opacity: 0; transform: scale(0.4); }
  66%, 90%  { opacity: 1; transform: scale(1); }
  100%      { opacity: 0; transform: scale(0.4); }
}

/* --- Before / After ------------------------------------------------------ */
.relief-pair {
  display: grid;
  gap: var(--s5);
  margin-top: var(--s7);
  align-items: center;
}
.ba { border-radius: var(--r-xl); padding: var(--s5); }
.ba-label {
  font-family: 'Manrope', sans-serif;
  font-size: var(--t-fine);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: var(--s3);
}
.ba-before {
  background: rgba(52, 66, 74, 0.05);
  color: var(--ink);
}
/* Ink only on peach (Palette Lock §2). No opacity on the label: at 0.7 it
   measured 3.48:1 against this panel, under AA, and 14px bold uppercase does
   not qualify as large text. The panel and the card carry the hierarchy. */
.ba-before .ba-label { color: var(--ink); }
.ba-before p { opacity: 0.88; }   /* 5.21:1 — quieter than After, still AA */
.ba-after {
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: 0 2px 10px rgba(52, 66, 74, 0.05);
}
.ba-after .ba-label { color: var(--ink); }   /* not Deep Teal — this is a label, not a control */
.ba-list { list-style: none; padding: 0; margin: 0 0 var(--s4); display: grid; gap: var(--s2); }
.ba-list li {
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-weight: 600;
  color: var(--ink);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
}
.ba-after.is-in .ba-list li { opacity: 1; transform: none; }
.ba-after.is-in .ba-list li:nth-child(1) { transition-delay: 120ms; }
.ba-after.is-in .ba-list li:nth-child(2) { transition-delay: 260ms; }
.ba-after.is-in .ba-list li:nth-child(3) { transition-delay: 400ms; }
.ba-after.is-in .ba-list li:nth-child(4) { transition-delay: 520ms; }
.ba-list li::before {
  content: '';
  flex: none;
  width: 20px; height: 20px;
  border-radius: var(--r-full);
  background: #73AFAF url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M6 10.4l2.6 2.6L14.4 7' fill='none' stroke='%23FAF7F2' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
}
.ba-keppy { justify-self: center; }
/* Size the stack, not every img inside it — the face overlay is positioned
   as a percentage of the stack and must not be given a width of its own. */
.ba-keppy .keppy-stack { width: 116px; }
.ba-keppy .keppy-stack > img:first-child { width: 100%; height: auto; }

@media (min-width: 820px) {
  .relief-pair { grid-template-columns: 1fr auto 1fr; gap: var(--s6); }
  .ba-keppy .keppy-stack { width: 132px; }
}

/* --- The ask, set apart from the trust band above it -------------------- */
.ask { border-top: 1px solid var(--line); }
/* The "Your price" eyebrow that used to live here is gone. It announced a
   price directly above a headline whose whole job is to say this is free. */

/* ==========================================================================
   The confirmation questions, as a dialog.
   A bottom sheet on a phone and a centred card on a desktop, because those are
   what each one already means to people. Native <dialog>, so focus trapping,
   Escape and inertness come from the browser rather than from code that has to
   be maintained and will eventually be wrong.
   ========================================================================== */
.q-dialog {
  padding: 0;
  border: 0;
  background: var(--surface);
  color: var(--ink);
  width: min(100%, 34rem);
  /* Two declarations, and the order matters.
   *
   * `dvh` is the right unit — it excludes the mobile browser's dynamic toolbar,
   * which `vh` does not. But it needs Safari 15.4+, Chrome 108+, Firefox 101+,
   * and on anything older the declaration is INVALID and simply dropped. That
   * left max-height at its initial value of `none`, so the dialog grew to the
   * full height of ten questions — measured at 3787px inside an 844px window.
   * The sheet is bottom-anchored, so the questions were pushed off the TOP of
   * the screen with no way to scroll back to them: reported in the wild as "the
   * questions just disappeared and there weren't ten of them".
   *
   * A browser that understands dvh takes the second line. One that does not
   * keeps the first, which is imperfect but bounded — and bounded is the whole
   * job here. This audience skews older, and older people run older phones. */
  max-height: 88vh;
  max-height: 88dvh;
  box-shadow: 0 -8px 40px rgba(52, 66, 74, 0.16);

  /* THE ONE LINE THAT WAS BREAKING THE SURVEY FOR EVERYBODY.
   *
   * Chrome's UA stylesheet ships `dialog { overflow: auto }`. This dialog is a
   * flex column — head, scrolling body, pinned footer — and the layout is
   * correct at rest: 52 + 566 + 86 = 704, the footer exactly where it belongs.
   * But the dialog ALSO reported scrollHeight 2861 against clientHeight 704,
   * which made it a scroll container in its own right, wrapped around one that
   * was already scrolling.
   *
   * So when you answered a question, the browser scrolled the radio into view
   * using the NEAREST scrollable ancestor — the dialog — and not the body.
   * Measured: on the very first answer dialogScrollTop went 0 -> 569 while
   * bodyScrollTop stayed at 0 and never moved again. The head scrolled off to
   * -521, and the footer carrying the submit button rode up and off the top of
   * the screen, ending 1438px above the dialog it lives in.
   *
   * That is every symptom two people reported, from one property: the submit
   * button "jumping up", a second scrollbar appearing, never reaching all ten
   * questions, and eventually a blank panel — which is what a scroll box looks
   * like once you have scrolled past everything in it.
   *
   * It fired on all nine viewports tested, phones included. It was reported on
   * desktop, which is where it was SEEN, not what caused it — assuming those
   * were the same thing would have sent the fix into media queries.
   *
   * `clip`, NOT `hidden`. This is the whole fix and the distinction is the
   * entire bug. An `overflow: hidden` box is still a SCROLL CONTAINER — it
   * just has no scrollbar. Script and `scrollIntoView()` scroll it happily,
   * silently, which is exactly what was happening. Measured with `hidden`
   * applied: dialogScrollTop still reached 2056 on desktop and 3291 on a
   * phone. `overflow: clip` is not a scroll container at all, so the browser
   * walks past it and scrolls .q-body — the box built to scroll.
   *
   * The root overflow is a Chrome quirk worth naming: the dialog's children
   * measure 52 + 566 + 86 = 704, exactly its clientHeight, yet its
   * scrollHeight reports 2861 — the height of the <form> inside .q-body,
   * leaking through a parent that is demonstrably clipping it (566 client
   * against 2990 scroll). `clip` makes the question moot rather than
   * depending on that quirk being fixed.
   *
   * `hidden` stays as the first declaration for Safari below 16, where `clip`
   * is unknown and the declaration is dropped. That leaves those browsers
   * exactly where they are today — no better, no worse — and reserve.js
   * carries a scrollTop guard that covers them. */
  overflow: hidden;
  overflow: clip;
}
.q-dialog::backdrop {
  background: rgba(52, 66, 74, 0.44);
  backdrop-filter: blur(2px);
}

/* Phone: a sheet that rises from the bottom edge. */
.q-dialog {
  margin: auto auto 0;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
}
@media (min-width: 700px) {
  .q-dialog {
    margin: auto;
    border-radius: var(--r-xl);
    box-shadow: 0 24px 64px rgba(52, 66, 74, 0.22);
  }
}

/* Only when open: a bare `display:flex` would override the closed state's
   `display:none` and leave the dialog permanently on screen. */
.q-dialog[open] {
  display: flex;
  flex-direction: column;
}

/* Its own strip, so the close control never floats over the questions. */
.q-head {
  flex: 0 0 auto;
  display: flex;
  justify-content: flex-end;
  padding: var(--s2) var(--s2) 0;
  background: var(--surface);
}

.q-body {
  /* Takes the leftover height so the footer below can stay put. min-height:0
     is what actually lets a flex child scroll — without it the body grows to
     its content and the footer is pushed off the bottom of the screen. */
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--s2) var(--s5) var(--s5);
  overflow-y: auto;
  overscroll-behavior: contain;   /* stop the page behind scrolling with it */
}
.q-body h2 {
  font-size: var(--t-h3);
}
.q-body:focus { outline: none; }   /* focused programmatically, not by tabbing */

/* The submit, parked outside the scroll area and wired to the form by id.
   Nine questions is a long scroll, and a button you only find at the bottom is
   a button a lot of people never find. */
.q-foot {
  flex: 0 0 auto;
  padding: var(--s4) var(--s5) calc(var(--s4) + env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: 1px solid var(--line);
}
.q-foot .status-msg:empty { display: none; }

/* --- The survey questions ------------------------------------------------ */
.survey-intro { margin-top: var(--s2); }

.q-set {
  border: 0;
  padding: 0;
  margin: var(--s6) 0 0;
}
.q-set:first-of-type { margin-top: var(--s5); }

.q-legend {
  padding: 0;
  font-weight: 600;
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink);           /* 10.37:1 on Surface */
}
.q-hint {
  margin-top: var(--s1);
  font-size: var(--t-fine);
  color: var(--muted);         /* 5.88:1 on Surface */
}
/* Longer option text reads badly in two narrow columns, so these stay stacked
   at every width. */
.choice-grid-1 { grid-template-columns: 1fr; }
@media (min-width: 480px) { .choice-grid-1 { grid-template-columns: 1fr; } }

/* Indented behind a soft rule so it reads as belonging to the option that
   revealed it, rather than as a stray field that appeared in the middle of a
   list of choices. Calm Teal is decoration here and carries no meaning the
   layout does not (Palette Lock §1). */
.q-other {
  margin-top: var(--s3);
  margin-left: var(--s2);
  padding-left: var(--s4);
  border-left: 2px solid var(--tint);
}
.q-other label { color: var(--muted); }

/* Keppy, shown rather than described — the question is unanswerable by anyone
   who scrolled past him. */
.q-keppy {
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin-top: var(--s3);
  /* Head-room for the cheer. He scales to 1.14 from his feet, which on an 82px
     stack is about 11px of growth upward — without this it clips the legend. */
  padding-top: 12px;
}
.q-keppy .keppy-stack {
  flex: 0 0 auto;
  width: 82px;
  transform-origin: 50% 100%;   /* grows and settles from the feet */
  transition: transform 420ms var(--ease-out);
}
/* Only the BASE image fills the stack. A blanket `.q-keppy img { width: 100% }`
   used to sit here, and because it out-specifies the `.keppy-face` rule that
   sets 59.299%, it blew every expression overlay up to full width — his visor
   rendered at nearly twice size, offset, covering his body and the hint text
   beside him. It went unnoticed because the reaction was a 1.8s flash and the
   test only ever asserted the class, never the geometry. */
.q-keppy .keppy-stack > img:first-child { width: 100%; height: auto; }
.q-keppy .keppy-face { width: 59.299%; }
.q-keppy .q-hint { margin-top: 0; }

/* --- He answers back ------------------------------------------------------
   Q6 asks how you feel about him, so he responds to what you say. It is the one
   place on the page where Keppy is on screen at the moment somebody does
   something, and the reaction is HELD rather than timed out: change your mind
   and he changes with you. A 1.8s timeout made it read as a blip. */

/* "I like him" — he grows and beams. Overshoots, comes back, settles a little
   larger than he started, and stays there. */
@keyframes keppy-cheer {
  0%   { transform: scale(1)    translateY(0)    rotate(0deg); }
  35%  { transform: scale(1.14) translateY(-7px) rotate(-3deg); }
  62%  { transform: scale(1.06) translateY(-2px) rotate(2deg); }
  100% { transform: scale(1.08) translateY(0)    rotate(0deg); }
}
.q-keppy .keppy-stack.is-happy {
  animation: keppy-cheer 620ms var(--ease-out) both;
  transform: scale(1.08);
}

/* "I would rather have just the plain app" — he takes it well. Eyes close into
   the resting curves, he settles a little smaller, and that is all. No slump,
   no sad face: the character lock forbids one (§1, §6), and looking wounded by
   an honest opinion would pressure people out of giving it — on the one question
   where the opinion is the entire point. */
.q-keppy .keppy-stack.is-nodding {
  transform: scale(0.96) translateY(3px);
}

@media (prefers-reduced-motion: reduce) {
  /* The expression still changes — it is an answer to a deliberate action, not
     ambient motion. Only the growing and settling is dropped. */
  .q-keppy .keppy-face.is-on { display: block; }
  .q-keppy .keppy-stack,
  .q-keppy .keppy-stack.is-happy,
  .q-keppy .keppy-stack.is-nodding {
    animation: none;
    transform: none;
    transition: none;
  }
}

/* One flex item, so the whitespace around the currency spans survives. */
.choice-text { display: inline; }

/* The close control. 44px of target around an 18px glyph (§8.2). */
.q-close {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--r-full);
  color: var(--muted);
  cursor: pointer;
}
.q-close:hover { color: var(--ink); background: var(--base); }
.q-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Entrance. Sheets slide, cards settle — matching what each shape implies. */
@keyframes q-sheet-in { from { transform: translateY(16px); opacity: 0; } to { transform: none; opacity: 1; } }
.q-dialog[open] { animation: q-sheet-in var(--dur-slow) var(--ease-out); }
@media (prefers-reduced-motion: reduce) {
  .q-dialog[open] { animation: none; }
}

/* Shown only once the dialog has been dismissed, so answering stays possible. */
.q-reopen-wrap { margin-top: var(--s6); text-align: center; }

/* ==========================================================================
   "Tap" or "Click" — whichever the device actually is. A phone told to click
   is the specific confusion that stopped people realising Keppy is pressable.
   ========================================================================== */
.hint-click { display: none; }
@media (hover: hover) and (pointer: fine) {
  .hint-tap { display: none; }
  .hint-click { display: inline; }
}

/* ==========================================================================
   Scroll cue.
   The hero measured 847px tall on an 844px viewport — it ended exactly at the
   fold, so the page looked complete and the only thing to do was press the
   CTA. People were arriving at the price having seen nothing but the headline.

   Quiet on purpose: it must read as "there is more below", not as a second
   button competing with the primary one. Deep Teal because it is interactive,
   which is the one thing that colour is allowed to mean.
   ========================================================================== */
/* --- The second option --------------------------------------------------
   Directly under the price button, because it is the other half of the same
   decision: reserve it, or go and look first. Below Keppy it was a screen and
   a half away from the button it is an alternative to, and the two were never
   on the same screen at once.

   Quiet on purpose. A second circle the size of the first under a full-width
   button reads as two buttons, and the choice stops looking like a choice. */
.scroll-cue {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  align-self: center;
  min-height: 44px;
  padding: 0 var(--s4);
  color: var(--accent);
  text-decoration: none;
  font-size: var(--t-small);
  font-weight: 600;
  border-radius: var(--r-full);
}
.scroll-cue:hover { background: rgba(23, 112, 106, 0.06); }
.scroll-cue:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.scroll-cue-arrow {
  display: grid;
  place-items: center;
  animation: cue-bob 2.6s var(--ease-inout) 2s infinite;
}
/* Two small falls and a rest — a continuous bounce becomes wallpaper and stops
   being read as a signal. */
@keyframes cue-bob {
  0%, 55%, 100% { transform: translateY(0); }
  12%           { transform: translateY(4px); }
  24%           { transform: translateY(0); }
  36%           { transform: translateY(4px); }
}
.scroll-cue:hover .scroll-cue-arrow { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .scroll-cue-arrow { animation: none; }
}
@media (min-width: 768px) { .hero { padding-bottom: var(--s7); } }

/* Short viewports — and that is most real phones, because Safari's chrome eats
   roughly 100px of an iPhone's 844. The scroll cue exists to say "there is
   more below"; below the fold it says nothing at all, so on a short screen
   everything else gives up room before the cue does. Keppy included: he is
   still the biggest thing on the page, just less enormous.

   RE-TIGHTENED when the hero gained the "what it is" sentence. That paragraph
   is real height, and German runs about a line longer than English, so the cue
   went under the fold on an iPhone 13 and — by a single pixel — on a Pro Max.
   Measured at the viewport heights Safari actually reports on load (roughly
   745 and 833), not at the device heights, which are about 100px larger and
   would have made this look fine while it was not. */
/* These were tuned for a hero whose whole job was fitting inside one screen,
   and they were still firing: `.hero-keppy-wrap .keppy-stack { width: 112px }`
   quietly beat the new clamp() at every phone height, so Keppy rendered at
   less than half the size the panel was built around and the panel looked
   empty. Half the selectors here — .lede, .hero-grid, .hero h1 — no longer
   match anything at all. What is left is the tightening that still makes
   sense: less air, same layout. */
@media (max-height: 820px) {
  .site-header { padding-block: var(--s4); }
  .hero { padding-top: var(--s4); }
  .hero-poem { gap: var(--s6); }
  .hero-col { gap: var(--s4); }
  .hero-actions { gap: var(--s2); }
}
/* An iPhone 13 reports 844 only once Safari's URL bar has collapsed, which
   happens after the first scroll — by which point the cue has done its job. But
   a page opened from a PWA or with the bar already hidden gets 844 on load, and
   German needs the same 30-odd pixels there. */
/* A short phone gives the panel less room, and Keppy has to stay inside it —
   overflow:hidden with align-items:flex-end crops the HEAD off a child taller
   than its box, which is the worst thing on this page to crop. */
@media (max-height: 700px) and (max-width: 860px) {
  .hero-panel { min-height: 320px; }
  .hero-keppy-wrap .keppy-stack { width: clamp(160px, 46vw, 200px); }
}

/* ==========================================================================
   Recognition questions — tappable.
   Pressing one is a small act of self-recognition, and it tells us which pain
   converts. Deliberately no score and no counter: a pressed question simply
   stays lit, reusing the highlight the loop already uses, so the interaction
   adds a meaning rather than a mechanic.
   ========================================================================== */
.questions-prompt { margin-top: var(--s3); }

.q-btn {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  padding: 10px 0;            /* clears 44px at the smallest clamped size */
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.q-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: var(--r-sm); }

/* A pressed question holds the lit state permanently — same treatment the
   travelling highlight uses, so nothing new has to be learned. */
.questions li:has(.q-btn[aria-pressed="true"]) {
  color: var(--ink);
  border-left-color: var(--tint);
  transform: translateX(5px);
}

.q-mark {
  flex: none;
  width: 22px; height: 22px;
  border-radius: var(--r-full);
  border: 2px solid var(--ivory-shade);
  opacity: 0;
  transform: scale(0.8);
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out),
              background-color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
/* Only shown once pressed — an empty checkbox next to every line would turn a
   moment of recognition into a form. */
.q-btn[aria-pressed="true"] .q-mark {
  opacity: 1;
  transform: none;
  border-color: #73AFAF;      /* the same tick teal used in "What changes" */
  background: #73AFAF url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M6 10.4l2.6 2.6L14.4 7' fill='none' stroke='%23FAF7F2' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/70% no-repeat;
}
@media (hover: hover) and (pointer: fine) {
  .q-btn:hover .q-mark { border-color: var(--tint); opacity: 1; transform: none; }
  .q-btn[aria-pressed="true"]:hover .q-mark { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .q-mark { transition: none; }
}

/* ==========================================================================
   FAQ.
   Progressive disclosure, so seven answers cost one screen instead of five.
   Built on the same disclosure language as the email fallback (.alt-path) so
   the page has one idea of what "expandable" looks like, not two.

   Summaries are Ink rather than Deep Teal: there are seven of them in a row,
   and seven teal lines would read as a link list and pull the eye away from
   the price that follows. The chevron carries the affordance instead.
   ========================================================================== */
.faq-intro { margin-top: var(--s3); color: var(--muted); }

.faq {
  margin-top: var(--s6);
  border-top: 1px solid var(--line);
}
.faq-item { border-bottom: 1px solid var(--line); }

.faq-item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  min-height: 60px;                /* comfortably past 44px (§8.2) */
  padding: var(--s3) 0;
  cursor: pointer;
  list-style: none;
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-h3);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary:hover { color: var(--accent); }
.faq-item summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-sm); }

/* The chevron is the affordance. Rotating rather than swapping glyphs means
   the state change is continuous, which reads as the panel opening rather
   than as two separate icons. */
.faq-item summary::after {
  content: '';
  flex: none;
  width: 12px; height: 12px;
  margin-right: 2px;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur-base) var(--ease-out);
}
.faq-item[open] summary::after { transform: rotate(-135deg) translate(-3px, -3px); }

.faq-item p {
  margin: 0;
  padding: 0 0 var(--s5);
  max-width: var(--measure);
  color: var(--muted);
}
.faq-item[open] p { animation: faq-open var(--dur-base) var(--ease-out); }
@keyframes faq-open {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .faq-item summary::after { transition: none; }
  .faq-item[open] p { animation: none; }
}

.faq-after { margin-top: var(--s6); color: var(--muted); font-size: var(--t-small); }
.faq-after a { color: var(--accent); }

/* ==========================================================================
   Trust band — lead promise, stated limits, founder note.

   With no credential and no personal story to point at, authority here is
   built from three things: a promise narrow enough to keep, limits named out
   loud, and a person willing to sign their name to both.
   ========================================================================== */

/* The medication-data promise.
   Deliberately NOT a callout box. A tinted panel with a coloured left border is
   the most recognisable tell of a generated interface, and worse, a callout
   signals "aside" — which is the opposite of what this sentence is. It leads
   the section instead, carried by type alone: Manrope at lede size, the
   promise itself in Ink at full weight. Ink on Surface is 10.37:1. */
.trust-lead {
  margin-top: var(--s5);
  max-width: 52ch;
}
.trust-lead p {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: clamp(1.125rem, 1.02rem + 0.5vw, 1.375rem);
  font-weight: 500;
  line-height: 1.45;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.trust-lead strong { font-weight: 800; }

/* Stated limits. Quieter than the promises above it — this is the small print
   said confidently, not a warning. */
.trust-limits { margin-top: var(--s7); }
.trust-limits h3 { margin-bottom: var(--s4); }
.trust-limits ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--s3);
  max-width: var(--measure);
}
.trust-limits li {
  position: relative;
  padding-left: var(--s5);
  color: var(--muted);
}
/* An en dash, not a tick. A tick would read as a feature list, and these are
   the opposite of features. */
.trust-limits li::before {
  content: '–';
  position: absolute;
  left: 0;
  color: var(--ivory-shade);
  font-weight: 700;
}

.founder-note {
  margin: var(--s7) 0 0;
  padding-left: var(--s5);
  border-left: 2px solid var(--ivory-shade);
  max-width: var(--measure);
}
.founder-note blockquote { margin: 0; }
.founder-note p {
  font-family: 'Manrope', system-ui, sans-serif;
  font-size: var(--t-lede);
  line-height: 1.5;
  color: var(--ink);
}
.founder-note figcaption {
  margin-top: var(--s4);
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--muted);
}
.founder-note figcaption a { color: var(--accent); }

/* ==========================================================================
   Social proof — the reservation count.
   Sits above the ask heading so the price is read in the light of "other
   people already did this", rather than being justified after the fact.
   Reserved height so its arrival never moves the price (CLS stays 0), and
   hidden entirely below the threshold rather than showing a small number.
   ========================================================================== */
.social-proof {
  min-height: 1.5em;              /* held whether or not a number arrives */
  margin-bottom: var(--s3);
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--muted);
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.social-proof.is-in { opacity: 1; }

/* ==========================================================================
   The price's two supporting lines.
   ========================================================================== */

/* The window. A real deadline, so it is stated plainly rather than dressed as
   an alarm — Ink on a soft peach panel (7.61:1), the same tint the page already
   uses for "something worth noticing", never red, never a countdown. */
.price-window {
  margin-top: var(--s4);
  padding: var(--s4);
  background: var(--peach);
  border-radius: var(--r-md);
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--ink);          /* Peach takes Ink only — Palette Lock §2 */
}
.price-window strong { color: var(--ink); }

/* ==========================================================================
   Product demo.
   The first thing on this page a visitor can verify rather than take on
   trust, so it is given room and treated as an object rather than an embed.

   The frame is deliberately NOT a photorealistic iPhone. Apple's own guidance
   is against inaccurate device art, the recording carries no status bar to sit
   behind a notch, and a fake handset would make the one honest asset on the
   page look staged. What it is instead: a plain dark bezel with a real
   shadow — enough to read as a held object, no costume.
   ========================================================================== */
.demo-lede {
  margin-top: var(--s3);
  font-size: var(--t-lede);
  color: var(--muted);
  max-width: 46ch;
}

.phone {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: var(--s7);
}

/* The honesty label. Deliberately type rather than a badge: a bordered pill or
   a peach chip here would read as a promotional tag, which is the opposite of
   what this sentence is doing. Ink on the word that has to land, Muted on the
   gloss — unmissable at a glance, still quiet. */
.demo-label {
  margin-bottom: var(--s5);
  max-width: 34ch;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--muted);          /* 5.88:1 on Surface, 5.51:1 on Base */
  text-align: center;
  text-wrap: balance;
}
.demo-label strong { color: var(--ink); font-weight: 700; }  /* 9.71:1 on Base */

/* Apple §12: a bigger surface should read as thicker. This is the largest
   floating object on the page, so it carries the deepest shadow — two layers,
   a tight contact shadow and a wide soft one, the way a real object sits. */
.phone-body {
  width: min(272px, 74vw);
  padding: 9px;
  background: var(--ink);
  border-radius: 42px;
  box-shadow:
    0 2px 6px rgba(52, 66, 74, 0.20),
    0 28px 64px -16px rgba(52, 66, 74, 0.42);
}
@media (min-width: 768px) { .phone-body { width: 300px; } }

.phone-screen {
  position: relative;
  overflow: hidden;
  border-radius: 34px;
  /* Locked to the recording's real ratio so nothing is letterboxed or cropped.
     .phone-video is object-fit: cover, so this is not cosmetic — a stale ratio
     here silently crops the demo rather than failing visibly. Updated with the
     August 2026 recording, which is 1180x2556 where the old one was 1080x2174. */
  aspect-ratio: 1180 / 2556;
  background: var(--base);
  isolation: isolate;
}
.phone-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The play control. Deep Teal because it is interactive, which is the only
   thing that colour is allowed to mean on this site. */
.phone-play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.phone-play-dot {
  display: grid;
  place-items: center;
  width: 68px; height: 68px;
  /* No padding nudge. The glyph carries its own optical centring in the path —
     correcting it here as well is what pushed it off-centre to the right. */
  border-radius: var(--r-full);
  background: var(--accent);
  color: #FFFFFF;                 /* 5.90:1 on Deep Teal */
  box-shadow: 0 6px 20px rgba(52, 66, 74, 0.28);
  transition: transform var(--dur-base) var(--ease-out),
              background-color var(--dur-base) var(--ease-out);
}
.phone-play:hover .phone-play-dot { transform: scale(1.06); background: var(--accent-deep); }
.phone-play:active .phone-play-dot { transform: scale(0.97); }
.phone-play:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 34px; }

/* Once it is playing the control gets out of the way entirely. */
.phone-screen.is-playing .phone-play { opacity: 0; pointer-events: none; }
.phone-play { transition: opacity var(--dur-base) var(--ease-out); }

.demo-note {
  margin-top: var(--s6);
  font-size: var(--t-small);
  color: var(--muted);
  text-align: center;
  /* Widened from 44ch: this is three sentences now, and centred text past four
     lines stops being readable and starts being a wall. */
  max-width: 52ch;
  margin-inline: auto;
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .phone-play-dot { transition: none; }
}
