
  @font-face{font-family:'Syne';font-weight:700;font-display:swap;src:url(/assets/fonts/Syne-700.woff2) format('woff2')}
  @font-face{font-family:'Syne';font-weight:800;font-display:swap;src:url(/assets/fonts/Syne-800.woff2) format('woff2')}
  /* ── Palette ───────────────────────────────────────────────────────────────
     LIGHT IS THE DEFAULT and there is no prefers-color-scheme block. That is
     deliberate on two counts: light is the brand's primary mode, and the previous
     file declared the light palette twice and the dark palette twice — so a colour
     fix applied in one place silently worked in only one of the two paths, which is
     how the dark-mode contrast bugs survived. One light block, one dark block, dark
     reached only by explicit choice. Nothing to keep in sync.

     The rule that shapes everything: lilac is 1.91:1 on paper. It CANNOT hold text.
     A deep violet does the work; lilac does the trim — rules, borders, dividers.
     Measured ratios are in brand/board/. If you change a value, re-measure. */
  :root{
    --paper:#EFEDF4;      /* cool ground — violet reads muddy on warm stock */
    --ink:#171320;        /* 15.72:1 — body text */
    --sub:#5A5468;        /*  6.23:1 — secondary text */
    --accent:#6D28D9;     /*  6.12:1 on paper, white on it 7.10:1 — links, buttons */
    --lilac:#C8A2C8;      /*  1.91:1 — TRIM ONLY. Never text, never a label. */
    --line:#C8A2C8;       /* hairlines are the lilac's main job */
    --card:#F3EDF7;       /* surface wash */
    --ok:#0F7A5A; --warn:#8A5A00; --bad:#B3123C; --info:#0E6E82;
    /* Surfaces that INVERT (a card painted --ink, a button painted --accent) need
       their own foregrounds, or they break the moment the theme flips. Hardcoding
       #fff and #c9cbe8 here is exactly what made the Growth tier 1.18:1 in dark. */
    --on-ink:#EFEDF4; --on-ink-sub:#B3ABC4; --on-accent:#FFFFFF;
    --mark-accent:#C4B5FD;  /* the logo's short bar, read against an --ink tile */
    --disp:'Syne',system-ui,sans-serif; --body:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  }
  :root[data-theme="dark"]{
    --paper:#141018; --ink:#EDE9F3; --sub:#9D95AD;
    --accent:#C4B5FD;     /* 10.18:1 on the dark ground; #8f97ff failed at 2.61:1 */
    --lilac:#6E5A80; --line:#3A3348; --card:#1C1726;
    --ok:#4ADE9E; --warn:#F0C674; --bad:#FF8FA8; --info:#67D3E8;
    --on-ink:#141018; --on-ink-sub:#4C4459; --on-accent:#141018;
    --mark-accent:#6D28D9;  /* tile is now paper, so the bar goes deep violet */
  }
  *{box-sizing:border-box}
  body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--body);font-size:17px;line-height:1.6;-webkit-font-smoothing:antialiased}
  .wrap{max-width:1080px;margin:0 auto;padding:0 clamp(20px,5vw,64px)}
  a{color:inherit}
  .disp{font-family:var(--disp);font-weight:800;letter-spacing:-.03em;line-height:.9}
  .eyebrow{font-size:12px;font-weight:700;letter-spacing:.22em;text-transform:uppercase}
  hr.rule{border:0;border-top:1.5px solid var(--line);margin:0;opacity:.9}

  /* nav */
  nav{display:flex;align-items:center;gap:20px;padding:22px 0}
  nav .mark{font-family:var(--disp);font-weight:800;font-size:26px;letter-spacing:-.04em}
  nav .mark .d{color:var(--accent)}

  /* ── the lockup ──
     The mark carries no colour of its own: the tile takes --ink and the bars take
     --paper, so it inverts itself in dark mode with no second asset and no JS. The one
     thing that cannot follow that rule is the short bar, which must read as the accent
     AGAINST the tile — and the tile is the opposite colour to the page. Hence its own
     token, which is deliberately the other theme's accent. */
  .brand{display:inline-flex;align-items:center;gap:11px;text-decoration:none;color:inherit}
  .logomark{width:30px;height:30px;flex:none;display:block}
  .logomark .tile{fill:var(--ink)}
  .logomark .b1,.logomark .b2{fill:var(--paper)}
  .logomark .b3{fill:var(--mark-accent)}
  nav .sp{flex:1}
  nav a.link{text-decoration:none;color:var(--sub);font-size:14px;font-weight:600}
  /* `nav a.link` — an ELEMENT selector — missed the theme toggle, which is a
     <button class="link">. So below 640px every nav link vanished except that one, and it
     kept the bar 9px wider than the viewport: the whole site scrolled sideways on a 320px
     phone. Invisible in a screenshot at 390px, which is why it survived. Match on the
     class, not the tag, and give mobile the theme control back in the panel. */
  @media(max-width:640px){nav .link{display:none}}
  .btn{display:inline-flex;align-items:center;gap:8px;background:var(--ink);color:var(--paper);
    text-decoration:none;font-weight:700;font-size:15px;padding:11px 18px;border-radius:2px;border:1.5px solid var(--ink)}
  .btn.ghost{background:transparent;color:var(--ink)}
  .btn.accent{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}

  /* hero */
  header.hero{padding:clamp(24px,6vw,80px) 0 clamp(40px,7vw,96px)}
  .hero .eyebrow{color:var(--accent)}
  /* `overflow-wrap:break-word` here allowed a break INSIDE "handled." — at ~1440px the
     final full stop landed alone on a fourth line and read as a stray black block under
     the headline. The headline is two deliberate lines (there is a <br>); it should never
     break a word. 138px is the largest size at which both lines fit the 952px measure at
     1440px, which is where it was overflowing. */
  /* The size was 11.4vw capped at 138px, and at that size the longest line — "Your
     inbox," — was 1113px of text inside a 952px column at every width above 1280. THAT
     is the root cause of the orphaned full stop site/README.md has listed as open since
     Wave 0C: the line could not fit, so it wrapped, and the stop was left stranded on a
     fourth line. It was never really a wrapping bug to be tuned out at 1440px.
     10.7vw capped at 116px is the largest that fits the column at every width from
     320px up, measured rather than guessed, with the 320px gutter as the binding case. */
  h1{font-family:var(--disp);font-weight:800;letter-spacing:-.045em;line-height:.86;
    font-size:clamp(32px,10.7vw,116px);margin:.18em 0 .1em;
    overflow-wrap:normal}
  /* Each headline line is its own unbreakable block, so the browser has no opportunity
     to break one badly. With the size above it never needs to. */
  .hero h1 .l{display:block;white-space:nowrap}
  .tag{font-family:var(--disp);font-weight:700;font-size:clamp(20px,3.6vw,34px);letter-spacing:-.02em;color:var(--ink);margin:0}
  .lead{font-size:clamp(18px,2.2vw,22px);color:var(--sub);max-width:50ch;margin:20px 0 28px}
  .herocta{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
  .note{font-size:13px;color:var(--sub)}

  section{padding:clamp(46px,7vw,92px) 0}
  .kick{display:flex;gap:14px;align-items:baseline;margin-bottom:clamp(22px,3vw,40px)}
  .kick .no{font-family:var(--disp);font-weight:800;color:var(--accent);font-size:14px}
  /* At 320px the 32px floor cannot fit "automatically." in a 280px measure, and the page
     scrolled sideways. Hyphenation only engages when a word genuinely will not fit — so
     nothing changes at 390px and up — and a hyphenated break reads as typography where a
     horizontally scrolling page reads as broken. */
  h2{font-family:var(--disp);font-weight:800;letter-spacing:-.03em;line-height:.98;font-size:clamp(32px,6vw,64px);margin:0;text-wrap:balance;
    overflow-wrap:break-word;hyphens:auto}
  p.big{font-size:clamp(19px,2.4vw,26px);color:var(--sub);max-width:34ch}

  /* value rows — the money-first list, plain, no cards */
  .val{display:grid;gap:0;margin-top:8px}
  .v{display:grid;grid-template-columns:auto 1fr;gap:20px;padding:26px 0;border-top:1.5px solid var(--line);align-items:start}
  .v .n{font-family:var(--disp);font-weight:800;font-size:clamp(28px,4vw,44px);color:var(--accent);letter-spacing:-.03em;font-variant-numeric:tabular-nums}
  .v h3{font-family:var(--disp);font-weight:700;font-size:clamp(20px,2.6vw,27px);letter-spacing:-.02em;margin:.1em 0 .3em}
  .v p{margin:0;color:var(--sub);max-width:60ch}
  .v:last-child{border-bottom:1.5px solid var(--line)}

  .cols{display:grid;gap:0}
  @media(min-width:760px){.cols{grid-template-columns:repeat(3,1fr)}}
  .col{padding:24px 0}
  @media(min-width:760px){.col{padding:6px 34px 6px 0;border-right:1.5px solid var(--line)}.col:last-child{border-right:0}}
  .col h4{font-family:var(--disp);font-weight:700;font-size:21px;letter-spacing:-.02em;margin:0 0 8px}
  .col p{color:var(--sub);font-size:16px;margin:0}

  .willwont{display:grid;gap:0}
  @media(min-width:720px){.willwont{grid-template-columns:1fr 1fr}}
  .wc{padding-top:10px}
  @media(min-width:720px){.wc.a{padding-right:36px;border-right:1.5px solid var(--line)}.wc.b{padding-left:36px}}
  .wc .hd{font-family:var(--disp);font-weight:800;font-size:clamp(30px,5vw,52px);letter-spacing:-.03em}
  .wc.b .hd{color:var(--accent)}
  .li{padding:12px 0;border-top:1.5px solid var(--line);font-size:15.5px}

  /* pricing */
  .price{display:grid;gap:16px;margin-top:12px}
  @media(min-width:820px){.price{grid-template-columns:repeat(3,1fr)}}
  .tier{border:1.5px solid var(--line);border-radius:3px;padding:22px}
  .tier.feat{background:var(--ink);color:var(--on-ink)}
  .tier .nm{font-family:var(--disp);font-weight:700;font-size:20px;letter-spacing:-.02em}
  .tier .pr{font-family:var(--disp);font-weight:800;font-size:38px;letter-spacing:-.03em;margin:10px 0 4px}
  .tier .pr small{font-size:15px;font-weight:600;color:var(--sub)}
  .tier.feat .pr small{color:var(--on-ink-sub)}
  .tier ul{list-style:none;padding:0;margin:12px 0 0;font-size:14.5px;color:var(--sub)}
  .tier.feat ul{color:var(--on-ink-sub)}
  .tier li{padding:5px 0;border-top:1px solid rgba(120,120,120,.25)}

  /* faq */
  .qa{border-top:1.5px solid var(--line);padding:22px 0}
  .qa:last-of-type{border-bottom:1.5px solid var(--line)}
  @media(min-width:820px){.qa{display:grid;grid-template-columns:minmax(220px,300px) 1fr;gap:32px}}
  .qa .q{font-family:var(--disp);font-weight:700;font-size:clamp(19px,2.2vw,24px);letter-spacing:-.02em}
  .qa .a{color:var(--sub);max-width:58ch}

  /* waitlist */
  .cta{background:var(--ink);color:var(--on-ink);border-radius:4px;padding:clamp(28px,5vw,56px);margin:24px 0}
  .cta h2{color:var(--on-ink)}
  .form{display:flex;gap:10px;flex-wrap:wrap;margin-top:20px;max-width:520px}
  .form input{flex:1;min-width:220px;background:transparent;border:1.5px solid var(--on-ink);color:var(--on-ink);
    border-radius:2px;padding:13px 14px;font:inherit;font-size:16px}
  .form input::placeholder{color:var(--on-ink-sub)}
  .form button{background:var(--accent);color:var(--on-accent);border:0;border-radius:2px;padding:13px 20px;font:inherit;font-weight:700;cursor:pointer}
  #msg{margin-top:12px;font-size:14px;color:var(--on-ink-sub);min-height:1em}
  footer{padding:40px 0 70px;color:var(--sub);font-size:13.5px;border-top:1.5px solid var(--line);display:flex;gap:16px;flex-wrap:wrap;align-items:baseline}
  footer .sp{flex:1}

  /* ══ MOTION ═══════════════════════════════════════════════════════════════
     Rules this obeys, so that later edits keep obeying them:

     1. Compositor-only properties — opacity, transform, clip-path. Never a shadow,
        never a filter. Animating a shadow IS glossiness; it is banned structurally
        rather than by taste, so there is nothing to argue about later.
     2. Entrance travel is 10-16px. Never 40. Long travel is the clearest tell of a
        template.
     3. Two easings only. Entrances cubic-bezier(.2,.7,0,1); state changes
        cubic-bezier(.4,0,.2,1). 160-220ms interaction, 420-700ms entrance.
     4. No library. No GSAP, no Framer, and emphatically no smooth-scroll library —
        they fight prefers-reduced-motion and break iOS momentum scrolling.
     5. The hidden state is applied only under .js, which script adds. If the script
        fails, is blocked, or is slow, the page renders complete rather than blank.
     6. The vocabulary is the page's own: type being set and rules being ruled. The
        signature move is a hairline drawing itself from the left, not a card floating.
     ════════════════════════════════════════════════════════════════════════ */

  /* ── motion tokens ──
     Durations are priced against the distance travelled, which is the whole trick. A
     "short movement" is a 150-250ms event; 600ms is what a large expansion costs. The
     hairline rules cross the page, so they get the longest duration on the page, and the
     26px body reveal gets less than half of it. Travel is sized at roughly 0.9 of the
     element's line-height — the unit that actually governs whether a reveal is visible. */
  :root{
    --e-in:cubic-bezier(.2,.7,0,1);--e-state:cubic-bezier(.4,0,.2,1);
    --e-out:cubic-bezier(.23,1,.32,1);--e-inout:cubic-bezier(.77,0,.175,1);
    --travel-body:26px;--dur-body:420ms;
    --travel-head:40px;--dur-head:460ms;
    --dur-rule:900ms;--dur-hero:560ms;--stagger:70ms;
  }

  /* ── interaction: there were ZERO :hover and ZERO :focus rules before this ── */
  /* A real focus ring everywhere. Not negotiable, and previously absent entirely. */
  a:focus-visible,button:focus-visible,summary:focus-visible,input:focus-visible{
    outline:2px solid var(--accent);outline-offset:3px;border-radius:1px}

  nav a.link{background-image:linear-gradient(currentColor,currentColor);
    background-size:0% 1.5px;background-position:0 100%;background-repeat:no-repeat}
  /* NB: linear-gradient here is a 1.5px solid rule, not a gradient *look*. It is the
     standard way to animate an underline's width. Do not "fix" it. */
  .btn .arw{display:inline-block}
  .qa summary::marker,.qa summary::-webkit-details-marker{content:"";display:none}

  @media(hover:hover){
    nav a.link:hover{background-size:100% 1.5px;color:var(--ink)}
    .btn.ghost:hover{background:var(--ink);color:var(--paper)}
    /* 3px was below the threshold where a hover response registers as deliberate — it
       read as a rendering wobble. Same lesson as the scroll reveal, smaller scale. */
    .btn.accent:hover .arw{transform:translateX(6px)}
    .tier:hover{border-color:var(--accent)}
    .v:hover .n{--nx:4px;transform:translateX(var(--nx))}
  }
  /* A hover state stuck "on" after a tap is the commonest mobile animation bug, so
     touch devices get a press response instead and no hover transforms at all. */
  @media(hover:none){
    .btn:active{opacity:.75}
    .form button:active{opacity:.75}
  }

  /* Cross-document navigation gets a real transition where the browser has one: Chrome
     126+ and Safari 18.2+. Firefox has not shipped cross-document view transitions in
     stable, where this rule is simply ignored and navigation is an ordinary page load.
     Nothing is built on top of it, which is the only condition on which it is worth
     having at all. The UA respects prefers-reduced-motion for this automatically. */
  @view-transition{navigation:auto}

  @media(prefers-reduced-motion:no-preference){
    html{scroll-behavior:smooth}

    nav a.link,.btn,.btn .arw,.tier,.v .n,.form button,.form input{
      transition:background-size .22s var(--e-state),background-color .16s var(--e-state),
                 color .16s var(--e-state),border-color .18s var(--e-state),
                 transform .18s var(--e-state),opacity .18s var(--e-state)}

    /* ── scroll reveal ──
       One shared IntersectionObserver, threshold 0 with a negative bottom margin.
       Threshold 0 matters on mobile specifically: a tall row may never reach 25%
       visibility inside a short viewport, and would then never reveal.

       Distance and duration are a matched pair, and getting that wrong is what made
       the first version of this invisible. It travelled 14px over 620ms: with an
       ease-out, ~80% of the distance lands in the first 150ms and the remaining ~2.5px
       crawls out over 450ms at about 5px/s. That reads as a snap followed by nothing.
       The rule of thumb that actually works is to size travel against LINE-HEIGHT, not
       in absolute pixels — 14px was half a line of body copy, and a tenth of a line of
       the headline, which is why it vanished at the top of the page. */
    .js .reveal{opacity:0;transform:translateY(var(--travel-body));
      transition:opacity var(--dur-body) var(--e-out),transform var(--dur-body) var(--e-out);
      transition-delay:calc(var(--i,0) * var(--stagger))}
    .js .reveal.in{opacity:1;transform:none}
    .js h2.reveal,.js .kick.reveal{--travel-body:var(--travel-head);--dur-body:var(--dur-head)}

    /* The signature move: hairline rules draw themselves from the left. These cross most
       of the page, so by the same distance/duration rule they must be the SLOWEST thing
       here — not the same 600ms as a 26px nudge. When everything moves at one speed
       nothing has weight, and weight is what makes motion read as an event. */
    .js hr.rule{transform:scaleX(0);transform-origin:left;
      transition:transform var(--dur-rule) var(--e-inout)}
    .js hr.rule.in{transform:scaleX(1)}

    /* Hero. Runs as a plain CSS animation at parse time — deliberately NOT through the
       observer, which would leave the headline sitting at its start state until JS runs.

       On animating the h1 at all: it is the LCP element, and the rule is narrower than
       "don't touch it". Chromium has ignored zero-opacity paints for LCP since 2020, so
       `opacity:0` really would break it — a faded-in element never becomes a candidate
       unless something incidentally repaints it. But a TRANSFORM does not disqualify
       anything (the spec applies transforms when computing the rect), and neither does
       starting at a non-zero opacity. So the headline moves, starting at .12.
       Never `opacity:0`, never clipped, and no lingering will-change — w3c/
       largest-contentful-paint#147 has that suppressing LCP registration outright.
       Caveat worth knowing: issue #148 shows Firefox and Chrome disagree here and the
       spec is silent, so this is Chromium behaviour we are leaning on, not a standard. */
    .js .hero h1{opacity:.12;transform:translateY(24px);
      animation:heroIn var(--dur-hero) var(--e-out) forwards}
    /* When motion.js is live it staggers the two lines individually, so the block-level
       animation above must stand down or both run and the headline moves twice. `.mo` is
       added by motion.js itself, which means this only ever applies when there is
       something to hand over to. */
    .mo .hero h1{opacity:1;transform:none;animation:none}
    .mo .hero h1 .l{opacity:.12}
    .js .hero .eyebrow,.js .hero .tag,.js .hero .lead,
    .js .hero .herocta,.js .hero .note{
      opacity:0;transform:translateY(var(--travel-body));
      animation:heroIn var(--dur-body) var(--e-out) forwards}
    .js .hero .eyebrow{animation-delay:.04s}
    .js .hero .tag    {animation-delay:calc(var(--dur-hero) - .12s)}
    .js .hero .lead   {animation-delay:calc(var(--dur-hero) - .05s)}
    .js .hero .herocta{animation-delay:calc(var(--dur-hero) + .02s)}
    .js .hero .note   {animation-delay:calc(var(--dur-hero) + .09s)}
    @keyframes heroIn{to{opacity:1;transform:none}}

    /* ── the mark sorts itself ──
       Three bars arrive out of line and settle into the stepped order, shortest and
       lilac last. It is the product's whole job performed by its own logo, and it costs
       four rules. Runs once on load, never on scroll — a logo that re-animates every
       time you pass it stops being a logo and starts being a distraction. */
    .js .logomark .b{transform-box:fill-box;animation:sort .5s var(--e-out) both}
    .js .logomark .b1{transform:translateX(-14px);animation-delay:.12s}
    .js .logomark .b2{transform:translateX(-30px);animation-delay:.24s}
    .js .logomark .b3{transform:translateX(-44px);animation-delay:.36s}
    @keyframes sort{from{opacity:0}to{opacity:1;transform:none}}

    .qa[open] .marker{transform:rotate(45deg)}
    .marker{transition:transform .2s var(--e-state)}
    .navpanel{transition:clip-path .24s var(--e-in)}
    .navpanel .themebtn{background:none;border:0;font:inherit;text-align:left;cursor:pointer;padding:0;color:inherit}
  .navpanel a{opacity:0;transform:translateY(10px);
      transition:opacity .3s var(--e-in),transform .3s var(--e-in);
      transition-delay:calc(var(--i,0) * 40ms)}
    .navpanel.open a{opacity:1;transform:none}
    #msg{transition:opacity .2s var(--e-state)}
  }

  /* ── FAQ accordion ──
     Real <details>/<summary> for free keyboard support and semantics. The panel
     animates grid-template-rows 0fr -> 1fr; the max-height:999px hack eases visibly
     wrongly and is not used. */
  .qa{list-style:none}
  .qa summary{list-style:none;cursor:pointer;display:flex;gap:16px;align-items:baseline}
  .qa summary::-webkit-details-marker{display:none}
  .qa .marker{font-family:var(--disp);font-weight:700;color:var(--accent);
    font-size:22px;line-height:1;margin-left:auto;flex:none}
  .qa .panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows .28s var(--e-state)}
  .qa[open] .panel{grid-template-rows:1fr}


  /* ── tap targets ──
     Measured on a real 390px viewport: the "All plans" back link was 14px tall and the
     footer links 15px, against Apple's 44pt and Google's 48dp minimums. They were
     perfectly visible and nearly unhittable with a thumb — the kind of thing that never
     shows up in a screenshot and shows up constantly in use.

     Padding rather than a bigger font, so the type keeps its proportion; negative margin
     so the enlarged hit area does not push the layout around. Only where a pointer is
     coarse, so desktop spacing is untouched. */
  @media(pointer:coarse){
    .note a,.eyebrow a,footer a{display:inline-block;padding:13px 6px;margin:-13px -6px}
    .navtoggle{min-height:44px;min-width:44px}
    nav .brand{min-height:44px}
    .qa summary{min-height:44px;align-items:center}
  }


  @media(prefers-reduced-motion:no-preference){
    /* ── more motion, still no gloss ──
       Everything here is geometry or type. The rule remains: nothing that looks like
       light — no gradient, no glow, no shadow — because the moment one element shines
       the whole page reads as a different, cheaper brand.

       1. Section numbers count themselves in. The kicker digits (01, 02...) slide up
          behind their own clip as the section arrives, so each one lands like a chapter
          mark rather than simply being present. */
    .js .kick .no{display:inline-block;overflow:clip}
    .js .kick.reveal .no{opacity:0;transform:translateY(100%);
      transition:opacity .34s var(--e-out),transform .44s var(--e-out)}
    .js .kick.reveal.in .no{opacity:1;transform:none}

    /* 2. The value rows draw their own rule as they arrive, left to right, instead of
          the border simply existing. Uses scaleX on a pseudo-element so nothing in the
          layout moves. */
    .js .v{position:relative}
    .js .v::before{content:"";position:absolute;top:-1.5px;left:0;right:0;height:1.5px;
      background:var(--accent);transform:scaleX(0);transform-origin:left;
      transition:transform .55s var(--e-inout)}
    .js .v.in::before{transform:scaleX(1)}

    /* 3. The big number in each row arrives fractionally ahead of its text.
          This WAS a font-weight settle, and it was dead code on two counts: it
          transitioned "wght" 800 to "wght" 800 (identical from- and to-states, so it
          animated nothing), and Syne ships here as two STATIC woff2 subsets — 700 and
          800 — so there is no variable axis present to animate even if the values had
          differed. Replaced with a transform, which is compositor-only and, unlike a
          weight change on a numeral, cannot reflow the row it sits in. */
          Both the reveal and the hover want to move this numeral, and the reveal's
          selector is the more specific of the two — so they go through one custom
          property rather than both setting `transform` and the hover silently losing. */
    .js .v .n{--nx:-6px;transform:translateX(var(--nx));opacity:.4;
      transition:transform .42s var(--e-out) .06s,opacity .42s var(--e-out) .06s}
    .js .v.in .n{--nx:0px;opacity:1}

    /* 4. Buttons acknowledge a press. 60ms, barely perceptible consciously, and the
          difference between a button that feels connected and one that feels like an
          image of a button. */
    .btn:active{transform:translateY(1px)}
    .btn{transition:transform .06s var(--e-state),background-color .16s var(--e-state),
                    color .16s var(--e-state),border-color .18s var(--e-state)}

    /* 5. The FAQ marker rotates AND the answer eases open from its own height rather
          than a guessed max-height, which is what makes the easing look wrong. */
    .qa .panel > div{opacity:0;transition:opacity .3s var(--e-out) .06s}
    .qa[open] .panel > div{opacity:1}

    /* 6. Anchor jumps land softly rather than teleporting. */
    html{scroll-padding-top:24px}
  }

  /* ── the pricing staircase ──
     The tiers escalate visually as well as in price: Starter is calm, Growth has more
     going on, Scale has the most. The point is that the ladder is legible before anyone
     reads a word — you can see which one is the big one.

     Driven by ONE variable per card, `--step` (1, 2, 3), which scales travel, duration
     and stagger together. That keeps it a single system rather than three hand-tuned
     animations that drift apart the first time someone edits one. Everything below still
     resolves through the shared motion tokens, so changing --dur-body changes all three.

     Restraint still applies: no gradients, no glow, no shadow. The escalation is in how
     much MOVES, not in how shiny anything is. */
  .plan{--step:1;border:1.5px solid var(--line);border-radius:3px;padding:26px 24px;
    position:relative;display:flex;flex-direction:column}
  .plan.s2{--step:2}
  .plan.s3{--step:3}
  .plan.s2{background:var(--ink);color:var(--on-ink);border-color:var(--ink)}
  .plan .nm{font-family:var(--disp);font-weight:700;font-size:20px;letter-spacing:-.02em}
  .plan .pr{font-family:var(--disp);font-weight:800;font-size:clamp(34px,5vw,46px);
    letter-spacing:-.03em;margin:10px 0 2px;font-variant-numeric:tabular-nums}
  .plan .pr small{font-size:15px;font-weight:600;color:var(--sub)}
  .plan.s2 .pr small{color:var(--on-ink-sub)}
  .plan .who{font-size:14px;color:var(--sub);margin:.3em 0 0}
  .plan.s2 .who{color:var(--on-ink-sub)}
  .plan ul{list-style:none;padding:0;margin:16px 0 0;font-size:14.5px;color:var(--sub);flex:1}
  .plan.s2 ul{color:var(--on-ink-sub)}
  .plan li{padding:7px 0;border-top:1px solid rgba(120,120,120,.25);display:flex;gap:10px}
  .plan li::before{content:"";flex:none;width:7px;height:7px;margin-top:7px;
    background:var(--accent);border-radius:1px}
  .plan.s2 li::before{background:var(--on-ink)}
  .plan .go{margin-top:20px}
  .plan .tag{position:absolute;top:-11px;left:22px;background:var(--accent);
    color:var(--on-accent);font-size:11px;font-weight:700;letter-spacing:.1em;
    text-transform:uppercase;padding:3px 9px;border-radius:2px}
  .plans{display:grid;gap:20px;grid-template-columns:1fr;margin-top:34px}
  @media(min-width:900px){.plans{grid-template-columns:repeat(3,1fr);align-items:stretch}}

  /* the free pilot, stated where it cannot be missed */
  .pilotbar{border:1.5px solid var(--accent);border-radius:3px;padding:18px 22px;
    margin-top:26px;display:flex;gap:16px;flex-wrap:wrap;align-items:baseline}
  .pilotbar strong{font-family:var(--disp);font-weight:800;font-size:19px;
    letter-spacing:-.02em;color:var(--accent)}
  .pilotbar span{color:var(--sub);font-size:15px;flex:1;min-width:15ch}

  /* comparison table — scrolls in its own box rather than the page */
  .cmp{overflow-x:auto;margin-top:22px;-webkit-overflow-scrolling:touch}
  .cmp table{border-collapse:collapse;width:100%;min-width:560px;font-size:14.5px}
  .cmp th,.cmp td{text-align:left;padding:12px 14px;border-bottom:1px solid var(--line)}
  .cmp th{font-family:var(--disp);font-weight:700;font-size:15px}
  .cmp td:not(:first-child),.cmp th:not(:first-child){text-align:center;width:19%}
  .cmp .y{color:var(--accent);font-weight:700}
  .cmp .n{color:var(--sub);opacity:.5}
  .cmp tr td:first-child{color:var(--sub)}
  /* The plan grid above is a yes/no matrix, so its cells are narrow and centred. The
     pricing-model comparison is prose in the last column and needs the opposite: left
     aligned, and free to be as wide as it needs. */
  .cmp.prose td:not(:first-child),.cmp.prose th:not(:first-child){text-align:left;width:auto}
  .cmp.prose td:first-child{color:var(--ink);font-family:var(--disp);font-weight:700;
    white-space:nowrap}
  .cmp.prose td:nth-child(2){color:var(--sub);font-variant-numeric:tabular-nums}
  .cmp.prose td:last-child{color:var(--sub);min-width:30ch}
  .cmp.prose tr:last-child td{color:var(--ink);border-bottom:0}

  @media(prefers-reduced-motion:no-preference){
    /* The escalation itself. Travel, duration and stagger all scale with --step, so the
       higher tier genuinely moves more rather than merely being described as doing so. */
    .js .plan{opacity:0;transform:translateY(calc(var(--travel-body) * var(--step) * .6));
      transition:opacity calc(var(--dur-body) + var(--step) * 90ms) var(--e-out),
                 transform calc(var(--dur-body) + var(--step) * 90ms) var(--e-out);
      transition-delay:calc(var(--step) * 110ms)}
    .js .plans.in .plan{opacity:1;transform:none}

    /* Each feature line arrives in turn, and the higher the tier the longer the run —
       so the expensive one visibly has more in it. */
    .js .plan li{opacity:0;transform:translateY(10px);
      transition:opacity .34s var(--e-out),transform .34s var(--e-out);
      transition-delay:calc(var(--step) * 110ms + var(--i,0) * calc(var(--step) * 45ms) + .2s)}
    .js .plans.in .plan li{opacity:1;transform:none}

    /* Only the top tier gets a moving edge: a hairline travelling the border. Geometry,
       not gloss — no glow, no gradient, nothing that would look like a different brand. */
    .js .plan.s3::after{content:"";position:absolute;left:0;top:-1.5px;height:1.5px;
      width:34%;background:var(--accent);
      animation:edge 5.5s var(--e-inout) infinite;animation-play-state:paused}
    .js .plans.in .plan.s3::after{animation-play-state:running}
    body.tabhidden .plan.s3::after{animation-play-state:paused}
    @keyframes edge{0%{transform:translateX(-100%)}55%,100%{transform:translateX(300%)}}
  }

  /* ── the funnel demonstration ──
     Deliberately built from the same vocabulary as the rest of the page — hairlines,
     rounded rectangles, one accent — rather than a screenshot. A screenshot would date
     the moment the panel changes and would need a raster at three densities; this is
     ~2KB of markup that is sharp at any size and re-colours itself in dark mode.

     It is authored in its RESTING state: inbox, wires, lanes and labels are all real
     markup. The only animated things are the chips, which are invisible by default. So
     a reader with reduced motion, or no CSS animation at all, still gets a legible
     diagram rather than a half-built one waiting for a keyframe that will never run. */
  /* Inside .wrap already, so it inherits the page gutter — no width of its own. */
  .demo{margin:56px auto 0;padding:0}
  .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
    clip-path:inset(50%);white-space:nowrap;border:0}
  /* ── the live triage ──
     Replaces the SVG funnel, which showed the shape of the idea but never the product.
     This is the product: mail arriving, being judged, and — twice — being refused.

     Everything here is laid out at its FINAL size before anything animates. The list has
     a fixed min-height and each row a settled height, so rows arriving one at a time
     cannot push the page around underneath the reader. A demonstration that reflows the
     viewport while you read it is worse than no demonstration. */
  .tri{display:grid;gap:20px;grid-template-columns:1fr;margin-top:8px}
  @media(min-width:820px){.tri{grid-template-columns:1.55fr 1fr;gap:28px;align-items:start}}

  .tri-main{border:1.5px solid var(--line);border-radius:12px;padding:18px 20px 20px}
  .tri-hd{display:flex;align-items:baseline;gap:12px;padding-bottom:14px;
    border-bottom:1.5px solid var(--line)}
  .tri-ttl{font-family:var(--disp);font-weight:700;font-size:15px;color:var(--ink)}
  .tri-status{flex:1;font-size:13px;color:var(--sub);font-variant-numeric:tabular-nums}
  .tri-stop{font:inherit;font-size:12px;font-weight:700;letter-spacing:.06em;
    text-transform:uppercase;background:none;color:var(--sub);cursor:pointer;
    border:1.5px solid var(--line);border-radius:2px;padding:5px 11px;opacity:.45;
    pointer-events:none}
  /* The button is only live while something is actually counting down. An always-clickable
     Stop that stops nothing is a worse lie than not having one. */
  .triage.is-sending .tri-stop{opacity:1;pointer-events:auto;color:var(--bad);
    border-color:var(--bad)}
  .triage.is-stopped .tri-status{color:var(--bad)}

  .tri-list{list-style:none;margin:0;padding:0;min-height:262px}
  .tmail{padding:11px 0;border-bottom:1px solid var(--line)}
  .tmail:last-child{border-bottom:0}
  .tmail-hd{display:flex;align-items:baseline;gap:10px}
  .tmail-from{font-family:var(--disp);font-weight:700;font-size:14px;color:var(--ink);flex:1}
  .tmail-subj{font-size:13.5px;color:var(--sub);margin-top:2px}
  .tmail-verdict{display:inline-flex;align-items:baseline;gap:7px;font-size:11px;
    font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--sub)}
  .tconf{font-variant-numeric:tabular-nums;letter-spacing:0;opacity:.7}
  /* The reason line is the "show your working" beat. It stays hidden until the verdict
     lands, because a reason that arrives before the decision reads as a script. */
  .tmail-why{font-size:12.5px;color:var(--sub);margin-top:5px;display:none}
  .tmail.judged .tmail-why{display:block}
  .tmail[data-lane="held"].judged .tmail-verdict{color:var(--accent)}
  .tmail[data-lane="held"].judged .tmail-why{color:var(--accent)}
  .tmail[data-lane="held"].judged{border-left:2px solid var(--accent);
    margin-left:-14px;padding-left:12px}

  .tri-lanes{display:grid;gap:12px}
  @media(max-width:819px){.tri-lanes{grid-template-columns:repeat(3,1fr)}}
  .tlane{border:1.5px solid var(--line);border-radius:3px;padding:14px 16px}
  /* 700, not the 800 used elsewhere for display figures. Syne ExtraBold's numerals are
     idiosyncratic enough that a LONE digit reads as an abstract mark rather than a number
     — "£149" is fine because the surrounding figures give it context, a solitary "1" is
     not. Bold keeps the same shapes at a legible weight. */
  .tlane-n{display:block;font-family:var(--disp);font-weight:700;
    font-size:clamp(26px,4vw,34px);line-height:1;color:var(--ink);
    font-variant-numeric:tabular-nums;transform-origin:left center}
  .tlane-l{display:block;font-family:var(--disp);font-weight:700;font-size:15px;
    color:var(--ink);margin-top:8px}
  .tlane-s{display:block;font-size:12.5px;color:var(--sub);margin-top:2px}
  .tlane[data-lane="held"]{border-color:var(--accent)}
  .tlane[data-lane="held"] .tlane-n,.tlane[data-lane="held"] .tlane-l{color:var(--accent)}

  .demonote{max-width:52ch;margin:18px auto 0;text-align:center}

  /* ── /tech ──
     The technical page. Same vocabulary as everything else — hairlines, Syne for
     display, no fills — so it reads as the same publication rather than a docs site
     bolted on. The one addition is a monospace, used only for identifiers that are
     literally strings in the source. */
  .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.92em}

  .pipe{width:100%;height:auto;display:block;overflow:visible;margin-top:30px}
  .pipe .pipe-wires .draw{stroke:var(--line);stroke-width:1.5}
  .pipe rect{fill:none;stroke:var(--line);stroke-width:1.5}
  .pipe .pipe-out rect{stroke:var(--accent)}
  .pipe text{fill:var(--ink);font-family:var(--disp);font-weight:700;font-size:15px;
    text-anchor:middle}
  .pipe .pipe-note text{fill:var(--sub);font-family:var(--body);font-weight:400;
    font-size:12.5px;text-anchor:start}
  @media(max-width:640px){
    .pipe text{font-size:19px}
    .pipe .pipe-note text{font-size:15px}
  }

  /* A pull quote, used once, for the guard.py comment. Deliberately not a "callout box":
     it is a rule and an indent, which is what the rest of the site does. */
  .pull{margin:30px 0 0;padding:0 0 0 22px;border-left:2px solid var(--accent);max-width:60ch}
  .pull p{font-family:var(--disp);font-weight:700;font-size:clamp(19px,2.4vw,25px);
    line-height:1.4;color:var(--ink);margin:0;letter-spacing:-.015em}
  .pull cite{display:block;margin-top:14px;font-style:normal;font-size:13px;color:var(--sub)}

  .stats{display:grid;gap:0;grid-template-columns:1fr;margin-top:32px;
    border-top:1.5px solid var(--line)}
  @media(min-width:700px){.stats{grid-template-columns:1fr 1fr}}
  @media(min-width:1000px){.stats{grid-template-columns:1fr 1fr 1fr}}
  .stat{padding:20px 0;border-bottom:1.5px solid var(--line)}
  @media(min-width:700px){.stat{padding:20px 22px 20px 0}}
  /* 700, not 800 — an isolated figure in Syne ExtraBold reads as a mark rather than a
     number. Same reason the triage counters use 700. */
  .stat-n{display:block;font-family:var(--disp);font-weight:700;
    font-size:clamp(30px,4.6vw,42px);line-height:1;color:var(--accent);
    font-variant-numeric:tabular-nums}
  .stat-l{display:block;font-family:var(--disp);font-weight:700;font-size:15px;
    color:var(--ink);margin-top:9px}
  .stat-s{display:block;font-size:13px;color:var(--sub);margin-top:4px;max-width:34ch}

  /* ── the reply demonstration ──
     The funnel shows the sorting; this shows the thing being sold, which is the writing.
     Deliberately an awkward enquiry: it asks a question the agent must NOT answer. The
     draft books the visit and refuses to guess at a price, because "knows when to stop"
     is the claim that is hardest to believe and easiest to demonstrate. */
  .swap{display:grid;gap:18px;margin-top:44px;grid-template-columns:1fr}
  @media(min-width:820px){.swap{grid-template-columns:1fr 1fr;gap:24px}}
  .swap .card{border:1.5px solid var(--line);border-radius:12px;padding:22px 24px}
  .swap .cardlab{font-size:12px;letter-spacing:.14em;text-transform:uppercase;
    color:var(--sub);margin-bottom:14px}
  .swap .from{font-family:var(--disp);font-weight:700;color:var(--ink);margin:0 0 8px}
  .swap p{margin:0;color:var(--sub);line-height:1.7}
  .swap .out-msg{border-color:var(--accent)}
  .swap .reply{color:var(--ink)}
  .swap .held{margin-top:16px;padding-top:14px;border-top:1px solid var(--line);
    font-size:13px;color:var(--accent)}

  @media(prefers-reduced-motion:no-preference){
    /* The draft wipes in from the left, so it reads as being written rather than
       appearing. clip-path is compositor-accelerated and, unlike animating width or
       height, cannot shift the layout around it. */
    .js .swap .out-msg{clip-path:inset(0 100% 0 0);
      transition:clip-path .7s var(--e-inout) .25s}
    .js .swap.in .out-msg{clip-path:inset(0 0 0 0)}
    /* With motion.js live the reply types itself, which is the same gesture done better.
       Running both means the card wipe clips the typing for its first second and the two
       read as one muddy movement, so the card stands down and the typing is the event. */
    .mo .swap .out-msg,.mo .swap.in .out-msg{clip-path:none;transition:none}
  }
  @media(max-width:640px){
    .triage{margin-top:40px}
    .tri-list{min-height:0}
    .tlane{padding:11px 12px}
    .tlane-s{display:none}
  }

  /* ── the reply typing itself ──
     A mask, not a character loop. `--type` runs 0→1 and drives a clip-path, so the text
     is laid out at full size from the first frame and the container never resizes. The
     naive version — appending characters to textContent — reflows the card on every
     frame, and this is the one block on the page people actually stop and read.

     @property is what makes a custom property animatable at all; without the registration
     the browser treats --type as a string and jumps 0→1 with no interpolation. Where it
     is unsupported the reply simply appears, which is the pre-existing behaviour. */
  @property --type{syntax:'<number>';inherits:false;initial-value:1}
  @media(prefers-reduced-motion:no-preference){
    .mo .swap .reply.typing{clip-path:inset(0 calc((1 - var(--type)) * 100%) 0 0)}
  }
  /* The held badge is hidden by motion.js in JS, not here, and only at the moment it
     commits to animating it back in. Hiding it from the stylesheet would mean that a
     motion.js which loaded and then threw before reaching this block would leave the
     single most important sentence on the page invisible forever. */
  .qa .panel > div{overflow:hidden}
  @media(prefers-reduced-motion:reduce){.qa .panel{transition:none}}
  /* Desktop reads as an editorial page, so every answer is open and the disclosure
     control is hidden. Mobile collapses, because scrolling past four open answers to
     reach the pricing is worse than one tap. */
  @media(min-width:820px){
    .qa .panel{grid-template-rows:1fr}
    .qa .marker{display:none}
    .qa summary{cursor:default}
  }

  /* ── mobile navigation ──
     Previously `nav a.link{display:none}` below 640px with NO replacement: the mobile
     site had no navigation at all. The control is a word, not a hamburger, because an
     editorial design navigates with type. */
  .navtoggle{display:none;background:none;border:0;padding:6px 0;margin-left:auto;
    font:inherit;font-weight:700;font-size:14px;color:var(--ink);cursor:pointer;
    font-family:var(--disp);letter-spacing:-.01em}
  .navpanel{position:fixed;inset:0;background:var(--paper);z-index:60;
    padding:78px clamp(20px,7vw,44px);display:flex;flex-direction:column;gap:4px;
    clip-path:inset(0 0 100% 0)}
  .navpanel.open{clip-path:inset(0 0 0 0)}
  /* `display:flex` above beats the `hidden` attribute, which left the closed menu in
     the accessibility tree — a screen reader announced every nav link twice, and the
     links were reachable by Tab from behind a panel nobody could see. */
  .navpanel[hidden]{display:none}
  .navpanel a{font-family:var(--disp);font-weight:800;letter-spacing:-.03em;
    font-size:clamp(30px,8.5vw,52px);text-decoration:none;padding:8px 0;color:var(--ink)}
  .navpanel .close{position:absolute;top:22px;right:clamp(20px,7vw,44px);
    background:none;border:0;font:inherit;font-family:var(--disp);font-weight:700;
    font-size:14px;color:var(--ink);cursor:pointer;padding:6px 0}
  @media(max-width:640px){
    .navtoggle{display:block}
    nav .btn.ghost{display:none}
  }
  @media(min-width:641px){.navpanel{display:none}}
  body.navopen{overflow:hidden}


  /* ── form states ── */
  .form button[disabled]{opacity:.6;cursor:progress}
  .form.err input{border-color:var(--accent)}
  .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}
  .skip{position:absolute;left:-9999px;top:0;background:var(--ink);color:var(--paper);
    padding:10px 16px;z-index:80}
  .skip:focus{left:8px;top:8px}

  /* ── legal pages ──────────────────────────────────────────────────────────
     Same editorial language as the marketing page: hairline rules, Syne for
     headings, one accent. A privacy policy nobody can read is a dark pattern
     with extra steps, so the measure is capped and the type is the body size,
     not the fine-print size. */
  .legal{max-width:72ch;padding:clamp(28px,5vw,56px) 0 clamp(56px,8vw,96px)}
  .legal h1{font-family:var(--disp);font-weight:800;letter-spacing:-.03em;line-height:.95;
    font-size:clamp(34px,6vw,64px);margin:0 0 10px}
  .legal .meta{color:var(--sub);font-size:14px;margin:0 0 28px}
  .legal h2{font-family:var(--disp);font-weight:700;letter-spacing:-.02em;
    font-size:clamp(20px,2.6vw,27px);margin:40px 0 10px;padding-top:20px;
    border-top:1.5px solid var(--line)}
  .legal h3{font-size:17px;font-weight:700;margin:24px 0 6px}
  .legal p,.legal li{color:var(--ink)}
  .legal li{margin:6px 0}
  .legal ul{padding-left:20px}
  .legal a{color:var(--accent);text-underline-offset:3px}
  .legal table{border-collapse:collapse;width:100%;margin:14px 0;font-size:15px;display:block;overflow-x:auto}
  .legal th,.legal td{text-align:left;padding:9px 12px 9px 0;border-bottom:1px solid var(--line);
    vertical-align:top}
  .legal th{font-weight:700;white-space:nowrap}
  .legal .callout{border-left:2px solid var(--accent);padding:2px 0 2px 16px;margin:18px 0;color:var(--sub)}
  .legal .toc{font-size:15px;columns:2;column-gap:32px;margin:0 0 8px;padding-left:18px}
  @media(max-width:640px){.legal .toc{columns:1}}

  /* footer legal links */
  footer .legal-links{display:flex;gap:18px;flex-wrap:wrap;margin-top:10px;font-size:14px}
  footer .legal-links a{color:var(--sub);text-decoration:none}

  /* ══════════════════════════════════════════════════════════════════════════════
     THE NIGHT SHIFT  —  the scroll film on the homepage
     ------------------------------------------------------------------------------
     A full-bleed dark scene you scroll through: emails land overnight and get handled,
     the two about money refuse to be answered, and the sky warms from night to dawn.

     This is a deliberate one-time theme device (dark scene between light sections), not
     a random mid-page inversion. The scene paints its OWN colours, hardcoded, because it
     is a photograph of a night and must read the same in either site theme. It resolves
     to --paper at the very end so the seam back into the page is invisible.

     Authored finished: every card visible, every verdict stamped, with no JS and no
     scroll-timeline support. film.js only cross-fades the sky, ticks the clock, counts
     the tally. Same rule the rest of the site follows — nothing here is load-bearing.
     Compositor-only: opacity and transform. No animated gradients, no filters. */
  .film{
    --f-ink:#ECE7F5; --f-sub:#B6ADC8; --f-dim:#8A8299;
    --f-card:#161020; --f-line:rgba(200,162,200,.20);
    --f-repl:#57D9A6; --f-file:#A6ACC0; --f-held:#FF93AC;
    position:relative; isolation:isolate; color:var(--f-ink);
    /* ONE tall vertical gradient: you descend through the night into dawn as you scroll.
       Seamless by construction — no sticky planes to un-stick, no seams, correct with no
       JS. The mail cards are self-contained dark panels, so they stay legible over any
       part of it; only the open beat (top, night) and the dawn beat (bottom, light) rely
       on the backdrop, and each sits where the gradient is the tone it needs. */
    background:linear-gradient(180deg,
      #0a0713 0%, #0b0817 12%, #120a22 26%, #1e1038 41%,
      #341c57 55%, #4d2f6f 66%, #6a4f86 76%, #9878a6 85%,
      #c6a2c2 92%, #e4d3e0 97%, #ece3ea 100%);
    overflow:clip;
  }
  /* a faint star field over the night portion only, masked to fade before dawn. Pure CSS. */
  .film::before{content:""; position:absolute; top:0; left:0; right:0; height:52%; z-index:0;
    pointer-events:none;
    background-image:
      radial-gradient(1.5px 1.5px at 18% 12%, rgba(255,255,255,.8), transparent),
      radial-gradient(1.2px 1.2px at 68% 7%, rgba(255,255,255,.6), transparent),
      radial-gradient(1px 1px at 42% 19%, rgba(255,255,255,.55), transparent),
      radial-gradient(1.4px 1.4px at 84% 24%, rgba(255,255,255,.65), transparent),
      radial-gradient(1px 1px at 30% 30%, rgba(255,255,255,.5), transparent),
      radial-gradient(1.2px 1.2px at 57% 38%, rgba(255,255,255,.5), transparent),
      radial-gradient(1px 1px at 9% 42%, rgba(255,255,255,.45), transparent);
    -webkit-mask:linear-gradient(180deg,#000 55%,transparent 90%);
    mask:linear-gradient(180deg,#000 55%,transparent 90%)}

  .film-hud{position:sticky; top:0; height:0; overflow:visible; z-index:6; pointer-events:none}
  .film-hud .hud-clock{position:absolute; right:clamp(20px,5vw,60px); top:clamp(18px,3vw,30px)}
  .film-hud .hud-note{position:absolute; right:clamp(20px,5vw,60px); top:clamp(52px,7vw,74px)}
  .hud-clock{font-family:var(--disp); font-weight:800; font-size:clamp(22px,3.4vw,34px);
    letter-spacing:-.02em; color:var(--f-ink); font-variant-numeric:tabular-nums}
  .hud-ap{font-size:.5em; font-weight:700; color:var(--f-sub); margin-left:.25em; letter-spacing:.04em}
  .hud-note{font-size:12px; font-weight:700; letter-spacing:.2em; text-transform:uppercase; color:var(--f-dim)}

  .film-rail{position:relative; z-index:2}
  .beat{min-height:100svh; min-height:100dvh; display:flex; flex-direction:column;
    align-items:center; justify-content:center; text-align:center;
    padding:12dvh clamp(20px,5vw,60px); gap:0}

  .beat-time{font-family:var(--disp); font-weight:700; font-size:14px; letter-spacing:.12em;
    text-transform:uppercase; color:var(--f-held); margin:0 0 18px}
  .beat-h{font-family:var(--disp); font-weight:800; letter-spacing:-.04em; line-height:.9;
    font-size:clamp(40px,8.5vw,104px); margin:0}
  .beat-h span{display:block}
  .beat-lead{font-size:clamp(17px,2.3vw,22px); color:var(--f-sub); max-width:28ch; margin:24px auto 0}
  .beat-open .beat-h{color:var(--f-ink)}

  /* the mail card */
  .mailcard{width:min(560px,100%); text-align:left; background:var(--f-card);
    border:1px solid var(--f-line); border-radius:6px; padding:clamp(20px,3vw,30px);
    box-shadow:0 30px 80px -40px rgba(0,0,0,.9)}
  .mc-top{display:flex; align-items:baseline; justify-content:space-between; gap:12px}
  .mc-from{font-weight:700; font-size:16px; color:var(--f-ink)}
  .mc-time{font-size:13px; color:var(--f-dim); font-variant-numeric:tabular-nums; flex:none}
  .mc-subj{font-family:var(--disp); font-weight:700; font-size:clamp(19px,2.6vw,25px);
    letter-spacing:-.02em; color:#fff; margin:12px 0 8px}
  .mc-body{font-size:15px; line-height:1.55; color:var(--f-sub); margin:0}
  .mc-verdict{display:flex; align-items:center; gap:14px; flex-wrap:wrap;
    margin-top:20px; padding-top:18px; border-top:1px solid var(--f-line)}
  .stamp{font-family:var(--disp); font-weight:800; font-size:13px; letter-spacing:.02em;
    padding:6px 12px; border-radius:3px; flex:none; border:1.5px solid currentColor}
  .stamp.s-replied{color:var(--f-repl)}
  .stamp.s-filed{color:var(--f-file)}
  .stamp.s-held{color:var(--f-held)}
  .mc-why{font-size:14px; color:var(--f-sub); line-height:1.4}

  /* the gate: the two money beats carry more weight than the routine ones */
  .beat.gate .mailcard{border-color:rgba(255,147,172,.42);
    box-shadow:0 0 0 1px rgba(255,147,172,.18), 0 30px 80px -40px rgba(0,0,0,.9)}
  .beat.gate .stamp.s-held{background:rgba(255,147,172,.12)}
  .beat.gate .mc-why{color:#ffc2cf}

  /* the morning tally. The dawn beat always sits at the light foot of the gradient, so
     dark ink is correct with or without JS — no runtime flip needed. */
  .beat-dawn .beat-h{color:#241436}
  .beat-dawn .beat-time{color:#9a5a86}
  .beat-dawn .beat-lead{color:#4a3a5c}
  .tally{display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(16px,4vw,48px);
    margin:38px auto 34px; max-width:640px; width:100%}
  .tally-cell{display:flex; flex-direction:column; gap:6px}
  .tally-n{font-family:var(--disp); font-weight:800; font-size:clamp(46px,9vw,88px);
    line-height:1; letter-spacing:-.04em; color:#3a1d63; font-variant-numeric:tabular-nums}
  .tally-cell:last-child .tally-n{color:#a12a55}
  .tally-l{font-size:13px; font-weight:600; color:#4a3a5c; letter-spacing:.01em}
  .beat-dawn .btn.accent{background:#3a1d63; border-color:#3a1d63; color:#fff}
  @media(max-width:560px){.tally{grid-template-columns:1fr; gap:20px; text-align:center}
    .tally-cell{align-items:center}}

  /* progressive enhancement: cards rise and verdicts land as they scroll through.
     Gated so the finished static scene is what shows without support / with reduced motion. */
  @media (prefers-reduced-motion: no-preference){
    @supports (animation-timeline: view()){
      .film .mailcard, .film .beat-open > *, .film .tally, .film .beat-dawn > .beat-time,
      .film .beat-dawn > .beat-h, .film .beat-dawn > .beat-lead, .film .beat-dawn > .btn{
        animation:film-rise linear both; animation-timeline:view();
        animation-range:entry 4% cover 40%}
      .film .mc-verdict{animation:film-stamp linear both; animation-timeline:view();
        animation-range:cover 26% cover 52%}
    }
  }
  @keyframes film-rise{from{opacity:.1; transform:translateY(46px) scale(.965)} to{opacity:1; transform:none}}
  @keyframes film-stamp{from{opacity:0; transform:translateX(-8px)} to{opacity:1; transform:none}}

  /* the kicker/eyebrow on the light sections below the film, when it stands alone */
  #reply .kick, #trust .kick, #pricing .kick, section > .kick{align-items:baseline}
