/* site.css: fastlift.app HOLD THE LINE. Values from design/2026-09-10/VISUAL-SYSTEM.md. No em dashes. */
:root{
  --g0:#0B0B0C;--g1:#101114;--frame:#111214;
  --ink-1:#F4F5F2;--ink-2:#A6A9B3;--ink-3:#7E828C;
  --lime:#C6F432;--lime-deep:#9BC417;--lime-10:rgba(198,244,50,.10);--lime-16:rgba(198,244,50,.16);--lime-24:rgba(198,244,50,.24);--lime-45:rgba(198,244,50,.45);
  --hair:rgba(255,255,255,.08);--hair-hi:rgba(255,255,255,.14);--glass:rgba(255,255,255,.04);--glass-top:rgba(255,255,255,.06);--track:rgba(255,255,255,.10);--mark:rgba(255,255,255,.28);
  --ease-out:cubic-bezier(.22,1,.36,1);--ease-standard:cubic-bezier(.33,0,.20,1);--ease-odometer:cubic-bezier(.33,0,.15,1);--ease-loop:cubic-bezier(.65,0,.35,1);
  --d90:90ms;--d180:180ms;--d320:320ms;--d500:500ms;--d700:700ms;--d900:900ms;--d1400:1400ms;
}
@font-face{font-family:"Hanken Grotesk";src:url(fonts/HankenGrotesk-400.woff2) format("woff2");font-weight:400;font-display:swap}
@font-face{font-family:"Hanken Grotesk";src:url(fonts/HankenGrotesk-600.woff2) format("woff2");font-weight:600;font-display:swap}
@font-face{font-family:"Hanken Fallback";src:local("Arial");size-adjust:100%;ascent-override:92%;descent-override:24%;line-gap-override:0%}
/* overscroll-behavior-x:none (2026-09-12): the document is never wider than the viewport (measured
   360 of 360 in Chrome device mode), but a sideways touch drag there still stretches the page and
   snaps it back, which reads as a horizontal scroll. Nothing on the page scrolls sideways, so the
   horizontal overscroll effect has nothing to offer and is switched off. */
html{background:var(--g0);color:var(--ink-1);overflow-x:clip;overscroll-behavior-x:none;-webkit-text-size-adjust:100%}
/* Safe-area insets on the sides (post-launch 5, 2026-09-11): viewport-fit=cover is set in the head,
   and only the store bar honoured an inset, so on a notched phone in landscape the nav and every
   headline ran under the notch. Zero everywhere else, so nothing changes on a plain screen. */
body{margin:0;padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right);font-family:"Hanken Grotesk","Hanken Fallback",Arial,sans-serif;font-size:16px;line-height:1.55;-webkit-font-smoothing:antialiased}
a{color:var(--lime)}a:hover{color:var(--lime-deep)}
/* height:auto (snag 05, 2026-09-11): every screenshot carries width="712" height="1444", and max-width
   alone clamps the width to its container while the height attribute still resolves to 1444px, so
   the fallback shots rendered at 240x1444, a 3x vertical stretch (the ring became a tall ellipse and
   the document grew to 14,750px). height:auto keeps the declared aspect ratio at any width. */
img{display:block;max-width:100%;height:auto}
/* focus: visible on every interactive element, never removed (task 10) */
a:focus-visible,button:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--lime);outline-offset:2px}
.skip{position:absolute;left:-9999px;top:16px;background:var(--lime);color:var(--g0);padding:12px 16px;border-radius:999px;z-index:50}
.skip:focus{left:16px}
/* type roles */
.eyebrow{font-size:11px;line-height:1;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--lime)}
h2.eyebrow{margin:0}
.display-xl{font-size:clamp(48px,6vw + 25.5px,112px);line-height:1.02;letter-spacing:-.025em;font-weight:600;margin:0}
.display-l{font-size:36px;line-height:1.05;letter-spacing:-.02em;font-weight:600;margin:0}
.lead{font-size:17px;line-height:1.5;color:var(--ink-2);margin:0}
.caption{font-size:13px;line-height:1.45;color:var(--ink-3);margin:0}
@media(min-width:1024px){.display-l{font-size:72px;line-height:1.02;letter-spacing:-.025em}.lead{font-size:22px;line-height:1.45}.caption{font-size:14px}.eyebrow{font-size:12px}}
/* Display type follows its COLUMN, not only the viewport (snag 02 and 03, 2026-09-11). The hero and
   story copy tracks now shrink before the phone does (see the two grids below), so between 900 and
   about 1300px the copy column can be far narrower than the viewport suggests, and a viewport-sized
   headline in a narrow column wraps one word to a row. Where container units exist the size is also
   capped by the column (14cqw keeps "Lose fat" on one row of a 322px column at 48px); engines without
   them keep the viewport clamp above, which fits because the column can never meet the phone. */
@supports(font-size:1cqw){
  .hero .copy,.beats{container-type:inline-size}
  .display-xl{font-size:min(clamp(48px,6vw + 25.5px,112px),clamp(48px,14cqw,112px))}
}
/* nav */
.nav{position:sticky;top:0;z-index:20;height:56px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;background:transparent}
.nav.is-scrolled{background:rgba(16,17,20,.92);backdrop-filter:blur(24px) saturate(140%);border-bottom:1px solid var(--hair)}
.nav .brand{display:flex;align-items:center;gap:8px;color:var(--ink-1);text-decoration:none;font-weight:600;font-size:17px}
.nav .links{display:none;gap:32px;font-size:14px}
.nav .links a{color:var(--ink-2);text-decoration:none}
.pill{display:inline-flex;align-items:center;height:36px;padding:0 16px;border-radius:999px;background:var(--lime);color:var(--g0);font-weight:600;font-size:14px;text-decoration:none;transition:background var(--d180) var(--ease-standard),color var(--d180) var(--ease-standard)}
.pill.is-ghost{background:transparent;color:var(--lime);box-shadow:inset 0 0 0 1px var(--lime-45)}
@media(min-width:900px){.nav{height:72px;padding:0 clamp(24px,4vw,80px)}.nav .links{display:flex;align-items:center}}
/* hero */
/* Registered so getComputedStyle() returns the resolved length (review of PR 23, item 8): site.js
   reads --phone-mt for the line and the handoff instead of mirroring the clamp with innerHeight,
   which disagrees with 100vh by the toolbar height on iPadOS Safari and Android Chrome. Engines
   without @property return the raw clamp text, and site.js falls back to the mirror there. */
@property --phone-mt{syntax:"<length>";inherits:true;initial-value:0px}
/* padding-bottom reserves the phone's slot (48px clearance + the 474px frame) so the hero keeps
   its height whether or not #device is currently in it. site.js moves the device out to .sticky-col when the story starts and back again on the way up; with the device in normal flow
   that move deleted 448px from above the reader mid-scroll (measured: a 100px scroll threw #story
   548px up the viewport and shrank the document by 448px, a CLS far over the 0.00 budget). Desktop
   already avoided this by positioning the device absolutely inside a min-height hero; this does the
   same below 900px, so the move is layout-neutral at every width. */
.hero{position:relative;padding:32px 16px 522px;overflow:clip}
.hero .copy{position:relative;z-index:2;max-width:800px}
.hero .lead{margin-top:16px;max-width:520px}
.badges{display:flex;gap:12px;align-items:center;margin-top:24px}
.badge-app img{height:48px;width:144px}
.badge-play img{height:76px;width:auto;margin:-14px -12px}   /* visible black block 48px, edges aligned with the App Store badge */
[data-platform="ios"] .badge-play{order:2}[data-platform="android"] .badge-app{order:2}
.trust{margin-top:16px}
.device{position:relative}          /* positioning context in both the hero and the sticky column */
/* Absolute, bottom-anchored, in the slot .hero's padding-bottom reserves above. Centred with
   left/right 0 plus margin auto rather than translateX(-50%): the reduced-motion block below kills
   every transform, which would have thrown a transform-centred phone off to the left. */
/* z-index:2 (2026-09-11, seen in the first screenshot anyone took of this hero): #line and #line-bloom
   are z-index:1 and the device was z-index:auto, so the lime rule painted straight across the phone's
   screen instead of passing behind it as the canvas draws it (SixteenHoursDesktop: "phone standing
   on the line"). The copy is z-index:2 as well and no longer shares a column with the phone. */
/* The whole phone, not a crop (founder, 2026-09-12, on the ring-only crop: "it looks shit. Make it
   look like a real phone or screenshot like the rest when you scroll down"). Same object as the beat
   shots below: 240 wide, the full capture, the same frame, so the hero phone and the story phones
   read as one device. CD-DIRECTIONS' "cropped at the ring" is retired. */
.hero .device{position:absolute;left:0;right:0;bottom:0;margin:0 auto;width:240px;height:474px;z-index:2}
@media(min-width:900px){
  /* Fluid tracks, not 800px 1fr (snag 02, 2026-09-11). With a fixed 800px copy track and the phone
     pinned to the viewport's right edge, the two converged as the window narrowed and 80px display
     type painted straight over the screenshot from 900 to about 1230px (measured overrun: 202px of
     headline ink at 902 wide). The copy track now shrinks first, minmax(0,800px), and the phone holds
     a 372px track of its own, so the headline can never reach it at any width. The device is back in
     flow for the same reason; the content-box min-height of 900px (1004px with the top padding, the
     same as before) exceeds the device's 816px (36 margin + 780 frame), so the hero keeps one height
     whether or not #device is currently in it, since site.js moves it to .sticky-col and back, which
     is exactly what the absolute positioning used to buy. Measured 2026-09-11: 1004px at 902, 1026,
     1154, 1282, 1366 and 1440 wide, before and after the move. The 20px right margin keeps the
     phone's right edge where it was (100px in from the viewport at the 80px padding). */
  /* One phone height for the whole page (phone placement fix, 2026-09-12). --ph is the frame's
     height in the hero, in the fixed handoff and in the story column, so the phone is the same
     size everywhere it appears. Two limits, the smaller wins: the frame ends 24px above the fold
     in the hero ((100vh - 200px) * .863, from the hero's 104px top padding and the phone's own
     124/780 top margin), and it clears the 72px nav by at least 29px top and bottom in the story
     column (100vh - 130px). Full 780px canvas size from a 1104px-tall window; 604px at 900; 420px
     at 586, which is a 1080p laptop at 150 per cent with the browser chrome, where the first cut
     (min 600px) still stood 14px taller than the whole viewport. Below 420 the frame is unreadable,
     so that is the floor and at such heights the hero phone may run under the fold; the column
     never does. Every dimension that used to be a 780-based constant is now a fraction of --ph.
     --sticky-top centres the frame in the space under the nav; the hero handoff (site.js,
     placeDevice) pins the phone at that same line, so nothing jumps. */
  /* Review of PR 23 (items 4 and 6): the 420px floor is now a floor only while the window can hold
     it. Below about 550px tall the column limit (100vh - 130px) takes over again, down to a 320px
     frame, so at 926x428 (a phone in landscape, which is wide enough to pin) the pinned phone sits
     between the nav and the fold instead of 32px under each. --sticky-top is floored 16px under the
     nav for the same reason. --pw is the frame's width, and every width that used to be the 372 or
     392 constant (hero and story tracks, column, story line, bezel, glow) derives from it. */
  :root{--ph:clamp(320px,min(calc(100vh - 130px),max(420px,calc((100vh - 200px) * .863))),780px);--pw:calc(var(--ph) * 372 / 780);--side-pad:clamp(24px,4vw,80px);--sticky-top:max(88px,calc(72px + (100vh - 72px - var(--ph)) / 2))}
  /* The phone's top margin in the hero (founder, 2026-09-12: "start the phone higher so it's in
     view fully"). The canvas value is 124/780 of the phone's height, and while --ph is tracking
     the window that is exactly 100vh - 200px - ph, so the two middle terms agree. Once --ph has hit
     its 420px floor the margin keeps shrinking instead, through zero and up to 72px above the copy,
     so the frame still ends 24px above the fold on windows down to about 530px tall (at 587 the
     phone starts 33px above the eyebrow and ends 24px clear of the bottom). Mirrored in site.js
     (phoneMarginTop), which needs the number where the device is not currently in the hero. */
  :root{--phone-mt:clamp(-72px,calc(100vh - 200px - var(--ph)),calc(var(--ph) * 124 / 780))}
  /* min-height is the phone's slot (its top margin plus its height, both fractions of --ph), so the
     hero ends exactly at the phone's bottom edge, as the canvas draws it (phone bottom 1080 on a
     1080 artboard), and keeps that height when site.js lifts the device out. The old fixed 900px
     left an 84px band under the phone at full size and made the hero 1004px tall on every window. */
  /* The phone track's minimum is the device's full box, frame plus its 20px right margin (review
     item 1): at 372 it was 20px narrower than the device, so lifting the device out for the fixed
     handoff let the track drop and the copy column widen 20px mid-scroll, resizing the headline. */
  .hero{padding:104px var(--side-pad) 0;min-height:calc(var(--ph) + var(--phone-mt));display:grid;grid-template-columns:minmax(0,800px) minmax(calc(var(--pw) + 20px),auto);column-gap:48px;justify-content:space-between;align-items:start}
  /* padding-bottom 88px is LINE_GAP + LINE_FLOOR (site.js): room for the line under the copy's last
     line and for the floor under the line, so the hero can never be shorter than copy plus line
     (review item 3: at 926x428 the hero ended 2px under the trust line and the line was clamped up
     through the store badges). No effect wherever the phone's slot is taller than the copy. */
  .hero .copy{padding-top:0;padding-bottom:88px}.hero .lead{margin-top:24px}.badges{margin-top:32px}.trust{margin-top:20px}
  /* --phone-mt puts the phone 228px below the hero's top at full size, where the canvas has it
     (SixteenHoursDesktop: phone top 300 under a 72px nav); it sat 88px too high. */
  .hero .device{position:relative;left:auto;right:auto;top:auto;bottom:auto;grid-column:2;justify-self:end;margin:var(--phone-mt) 20px 0 0;width:var(--pw);height:var(--ph);overflow:visible}
  /* The handoff (site.js placeDevice): once the hero phone's flow position has scrolled up to the
     sticky line, it holds there until the story column arrives at the same line and takes it. Same
     top as the column, same right edge as the hero (the side padding plus the 20px margin), so the
     phone never moves on screen while its parent changes. */
  /* No horizontal rule here on purpose (review of PR 23, item 5): a viewport-relative right offset
     cannot see body's safe-area padding, so the phone stepped sideways by the inset at both
     handoffs. placeDevice() sets left from the hero's own padding edge instead, which is where
     the in-flow device sits, so the two agree whatever the inset or scrollbar. */
  .device.is-fixed{position:fixed;top:var(--sticky-top);margin:0;z-index:2}
}
/* device frame. Below 900px it is the beat shots' frame exactly (2026-09-12): 240 wide, a 6px
   bezel (their 5px padding plus 1px border), 36 and 31px radii, and a 228x462 screen that holds the
   712x1444 capture at its own aspect, so nothing is cropped and the ring overlay's cover mapping
   (layoutOverlays, site.js) lands with zero side offset. */
.frame{position:relative;width:240px;height:474px;border-radius:36px;background:var(--frame);border:1px solid var(--hair-hi);box-shadow:0 24px 48px -16px rgba(0,0,0,.70),0 0 0 1px rgba(0,0,0,.60);box-sizing:border-box}
.frame .screen{position:absolute;left:5px;top:5px;width:228px;height:462px;border-radius:31px;overflow:hidden;background:var(--g0)}
.frame .screen img{position:absolute;left:0;top:0;width:100%;height:100%;object-fit:cover;object-position:top;opacity:0;transition:opacity var(--d320) var(--ease-standard),transform var(--d320) var(--ease-out);transform:translateY(12px)}
.frame .screen img.is-active{opacity:1;transform:none}
.glow{position:absolute;width:480px;height:480px;border-radius:50%;background:radial-gradient(circle,var(--lime-16) 0%,rgba(198,244,50,.06) 35%,transparent 60%);filter:blur(32px);pointer-events:none;z-index:0}
@media(min-width:900px){.frame{width:var(--pw);height:var(--ph);border-radius:calc(var(--ph) * 52 / 780)}.frame .screen{left:calc(var(--ph) * 8 / 780);top:calc(var(--ph) * 8 / 780);width:calc(100% - var(--ph) * 16 / 780);height:calc(100% - var(--ph) * 16 / 780);border-radius:calc(var(--ph) * 44 / 780)}.glow{width:calc(var(--ph) * 720 / 780);height:calc(var(--ph) * 720 / 780);filter:blur(40px)}}
/* the line */
#line{position:absolute;left:0;height:1px;width:100%;background:linear-gradient(90deg,rgba(198,244,50,0),var(--lime) 12%,var(--lime));transform-origin:left center;transform:scaleX(0);z-index:1;pointer-events:none}
#line-bloom{position:absolute;left:0;height:40px;width:100%;margin-top:-20px;background:linear-gradient(90deg,rgba(198,244,50,0),var(--lime-24));filter:blur(40px);opacity:.25;transform-origin:left center;transform:scaleX(0);pointer-events:none;z-index:1}
/* ring overlay (spec section 5): sits over the capture's own ring, which #ring-mask hides */
#ring-mask{position:absolute;border-radius:50%;background:var(--g0)}
#ring{position:absolute;pointer-events:none;overflow:visible}
#ring .track{fill:none;stroke:var(--track);stroke-width:3}
#ring .mark{fill:var(--mark)}
#ring .casing{fill:none;stroke:var(--g0);stroke-width:13;stroke-linecap:round;stroke-dasharray:552.9;stroke-dashoffset:552.9}
#ring .arc{fill:none;stroke:var(--lime);stroke-width:10;stroke-linecap:round;stroke-dasharray:552.9;stroke-dashoffset:552.9;filter:drop-shadow(0 0 12px var(--lime-45))}
#ring .dot{fill:var(--lime);opacity:0}#ring .halo{fill:rgba(198,244,50,.25);opacity:0}
#ring.is-drawn .casing,#ring.is-drawn .arc{transition:stroke-dashoffset var(--d1400) var(--ease-out)}
#ring.is-drawn .dot,#ring.is-drawn .halo{opacity:1;transition:opacity var(--d180) var(--ease-standard) 200ms}
#ring.is-drawn .halo{animation:breathe 4000ms var(--ease-loop) 3000ms infinite}
#ring[hidden],#ring-mask[hidden]{display:none}
@keyframes breathe{0%,100%{opacity:.25}50%{opacity:.10}}
/* beat overlays (spec section 5): workout light sweep, coach rows, today rails. Masks position
   their spans in JS (layoutOverlays(), site.js) from data-box fractions of the natural 712x1444
   image, through the same cover mapping used for the ring, so nothing here sets left/top/width/height. */
.ov{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity var(--d320) var(--ease-standard)}
.ov.is-on{opacity:1}
.ov.masks{opacity:1}
.ov[hidden]{display:none}
.masks span{position:absolute;background:var(--g0);border-radius:12px;transition:opacity var(--d320) var(--ease-out);opacity:1}
.masks.is-on span{opacity:0}
.masks span:nth-child(2){transition-delay:80ms}.masks span:nth-child(3){transition-delay:160ms}
#ov-workout-light{background:linear-gradient(90deg,transparent 0%,var(--lime-24) 50%,transparent 100%);width:60%;transform:translateX(-100%);transition:transform var(--d900) linear,opacity var(--d320)}
#ov-workout-light.is-on{transform:translateX(200%)}
/* reveals */
/* Gated on can-reveal, which site.js adds to <html> only once the reveal observer exists. The
   arming class used to be .js alone, set unconditionally by the head snippet, so anything that
   stopped the observer being built (setupTour()'s no-pin bail, a throw above it, or site.js
   failing to load at all) left the facts rail, all twelve depth cards, the Why and Premium
   panels, the whole close band including both store badges, and the cheat sheet form at opacity 0
   for good. Nothing else on the page ever adds is-in below the hero. Hidden-until-armed is now
   impossible to reach without the code that unhides it. */
.js.can-reveal .reveal{opacity:0;transform:translateY(32px);transition:opacity var(--d500) var(--ease-out),transform var(--d500) var(--ease-out)}
/* Both selectors on purpose. Adding can-reveal to the arming rule above took it from two classes
   to three, which outranks a bare .reveal.is-in and would have left every reveal at opacity 0 for
   good (measured: 31 of 31). The four-class form wins back, and the bare one still covers the
   un-armed page, where nothing is hiding anything in the first place. */
.reveal.is-in,.js.can-reveal .reveal.is-in{opacity:1;transform:none}
.h1-line{display:block;transition:clip-path var(--d700) var(--ease-out),transform var(--d700) var(--ease-out)}
/* Armed by hero-armed, which heroLoad() sets as its FIRST statement, never by the bare .js the head
   snippet adds during parsing (snag 07, 2026-09-11). Keyed on .js alone, any failure of site.js (a
   404 from a partial deploy, a filtering proxy, an extension, Esc during load) left both spans clipped
   to nothing inside an empty 98px box for the whole visit: the page's only h1, invisible. Same shape
   as .js.can-reveal .reveal above: the class that hides is set by the code that shows. */
.js.hero-armed .h1-line{clip-path:inset(0 0 100% 0);transform:translateY(24px)}
.h1-line:nth-child(2){transition-delay:90ms}
.hero-armed.is-loaded .h1-line{clip-path:inset(0);transform:none}
/* story: fact rail, six beats, sticky phone, mid badges (UX-STRUCTURE 2.1 and P-3, P-4) */
.facts{margin:32px 16px 0;padding:14px 16px;border-top:1px solid var(--hair);border-bottom:1px solid var(--hair);text-align:center;font-size:13px;color:var(--ink-2)}
/* Desktop: the sentence keeps clear of the phone column, since the fixed phone stands over this
   band for most of the hero-to-story scroll (review item 5, second half: 125px of the sentence's
   right end sat under the capture at 920x800). The rules still run edge to edge behind the phone. */
@media(min-width:900px){.facts{padding-right:calc(var(--side-pad) + var(--pw) + 52px)}}
.story{position:relative;padding:0 16px}
.beat{min-height:60vh;display:flex;flex-direction:column;justify-content:center;gap:16px;padding:48px 0}
.beat .display-l{font-size:32px}
/* The beat shots are phones too (2026-09-12): the same frame, hairline and shadow as the hero
   device at 240px wide, and centred, where they were bare left-hugging screenshots under a centred
   framed phone. margin-left/right auto centres a fixed-width block in the beat's flex column. */
.inline-shot{box-sizing:border-box;width:240px;margin:24px auto 0;padding:5px;border-radius:36px;background:var(--frame);border:1px solid var(--hair-hi);box-shadow:0 24px 48px -16px rgba(0,0,0,.70),0 0 0 1px rgba(0,0,0,.60)}
.inline-shot img{border-radius:31px}
/* visually hidden, not display:none, while the pinned phone stands in for it (task 10): the beat's
   own screenshot stays in the accessibility tree with its descriptive alt even though the sighted
   layout shows the pinned device instead. */
/* Keyed on can-reveal, not bare .js (snag 07): setupTour() sets can-reveal as its last statement, once
   the pinned phone that stands in for these shots is actually being driven. If site.js never runs, or
   bails to no-pin, nothing hides them and the story keeps its imagery. */
.js.can-reveal .inline-shot{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.pills{display:flex;flex-wrap:wrap;gap:8px;list-style:none;padding:0;margin:0}
.pills li{padding:8px 12px;border-radius:999px;border:1px solid var(--hair-hi);color:var(--ink-2);font-size:13px;transition:color var(--d180) var(--ease-standard),border-color var(--d180) var(--ease-standard)}
.pills li.is-lit{color:var(--lime);border-color:var(--lime-45)}
.four-lines{display:grid;gap:18px;margin-top:24px}
.four-lines span{display:block;height:1px;background:var(--lime);transform-origin:left;transform:scaleX(0);transition:transform var(--d900) var(--ease-out)}
.beat.is-active .four-lines span{transform:scaleX(1)}
.story-line{position:absolute;left:8px;top:0;bottom:0;width:1px;background:var(--lime);transform-origin:top;transform:scaleY(0);opacity:.6}
/* Anchored to the story, not the whole document (post-launch 2, 2026-09-11): driven by scroll(root)
   the rule was only about half drawn when the six beats ended, because the page keeps going for
   another 4,000px. view() tracks this element's own passage through the viewport, and the element
   spans the story exactly (top:0 bottom:0), so cover 0% is the story entering at the bottom and
   cover 85% is its lower edge a little above the viewport's bottom, about where the last beat sits
   centred; the line is fully drawn by then. */
@supports(animation-timeline:view()){.story-line{animation:grow linear both;animation-timeline:view();animation-range:cover 0% cover 85%}@keyframes grow{to{transform:scaleY(1)}}}
@supports not (animation-timeline:view()){.story-line{transform:scaleY(1)}}
/* Phones never pin (founder, 2026-09-11, watching the page on his own phone: "the phone screen
   covers everything behind, that's not very good"). The pinned column was opaque and sat over the
   top 300px of every screen, so each section's copy slid under it the moment it had been read; the
   snag-01 fix reserved a band above the copy and measured clean at the centred instant, and the
   founder's eye still read the mechanic as covering. Below 900px each beat shows its own screenshot
   under its copy instead, the same fallback path short phones and no-JS readers already had, and
   nothing on the page ever paints over text. The tour with the crossfading screens is desktop only.
   The device stays in the hero on phones; placeDevice() (site.js) pins only at 900px and up. */
@media(max-width:899.98px){
  .sticky-col{display:none}
  .inline-shot,.js.can-reveal .inline-shot{position:static;width:240px;height:auto;padding:5px;margin:24px auto 0;overflow:visible;clip:auto;white-space:normal;border:1px solid var(--hair-hi);display:block}
}
/* desktop: two columns, sticky phone */
@media(min-width:900px){
  /* Fluid, like the hero (snag 03, 2026-09-11). A fixed 800px track started the phone column at
     x=968 at EVERY width, and html{overflow-x:clip} made the overflow unreachable rather than
     scrollable: 0px of phone visible at 902, 58 at 1026, 186 at 1154, the whole frame only from 1442,
     while the inline shots stayed at 1px because the code pins on width alone. The copy track shrinks
     first, the phone keeps its 372px, and the story rule is measured from the column the phone
     actually occupies rather than from a constant. */
  .story{--story-pad:var(--side-pad);--story-gap:clamp(32px,4vw,88px);display:grid;grid-template-columns:minmax(0,800px) minmax(calc(var(--pw) + 20px),auto);justify-content:space-between;padding:0 var(--story-pad);column-gap:var(--story-gap)}
  .story-line{left:auto;right:calc(var(--story-pad) + var(--pw) + 20px + var(--story-gap) / 2)}
  .beat{min-height:100vh;padding:0}.beat .display-l{font-size:72px}
  /* Column and phone share --ph and --sticky-top with the hero (see the hero block above). The old
     top:120px with a 780px frame needed a 900px-tall window; anything shorter cut the phone's
     bottom off for the whole story (120px gone at 780 tall). The column is the frame's width plus
     the hero's 20px right margin, and the device sits at its right edge, so the phone's right edge
     is the same in the hero, in the handoff and here: it used to step 20px right. */
  .sticky-col{position:sticky;top:var(--sticky-top);height:var(--ph);align-self:start;width:calc(var(--pw) + 20px)}
  .sticky-col .device{margin-left:auto;margin-right:20px;width:var(--pw)}
}
@supports(font-size:1cqw){@media(min-width:900px){.beat .display-l{font-size:clamp(36px,9cqw,72px)}}}
/* Browsers that cannot pin (no-pin, set by setupTour when sticky or IntersectionObserver is missing)
   and viewports placeDevice() decides not to pin (unpinned, set from the same predicate the CSS
   above uses, so a fractional width at the breakpoint can never land with neither a pinned phone
   nor a fallback shot: with JavaScript the class decides, without it the shots are never hidden). */
.no-pin .sticky-col,.unpinned .sticky-col{display:none}.no-pin .inline-shot,.js.can-reveal.no-pin .inline-shot,.js.can-reveal.unpinned .inline-shot{position:static;width:240px;height:auto;padding:5px;margin:24px auto 0;overflow:visible;clip:auto;white-space:normal;border:1px solid var(--hair-hi);display:block}
.mid{justify-content:center;flex-wrap:wrap;padding:48px 16px}.mid .caption{width:100%;text-align:center;margin-bottom:8px}
/* reduced motion: final states, 120ms crossfades only */
@media(prefers-reduced-motion:reduce){
  /* :not(.device) exempts the phone from the blanket (snag 04, 2026-09-11). The tall-phone column
     used to scale the device with a transform as LAYOUT, and the blanket stripped it: measured at
     375x812, a 300px device at left 30 inside a 200px overflow:hidden column. The previous attempt
     restated the scale under an "id-free but heavier" selector and could never win: :not() carries
     the specificity of its argument, so *:not(#ring-head) scores as an id, and no stack of classes
     beats an id whatever the order. The phone pin has since been removed below 900px (see the story
     rules above), so no layout transform remains on the device today; the exemption stays because
     it is the correct shape for any layout transform a later change puts on it, and nothing on the
     device animates, so it loses no reduced-motion protection by being exempt. */
  *:not(#ring-head):not(.device),*::before,*::after{animation:none!important;transition-duration:120ms!important;transition-delay:0ms!important;transition-property:opacity!important;transform:none!important;clip-path:none!important}
  /* .js.can-reveal .reveal, not .reveal: the plain class is (0,1,0) and lost to the arming rule
     above, media queries adding no specificity of their own, so this safety net computed to
     nothing and reduced-motion visitors still waited on the observer. Same selector, later in the
     file, therefore it wins. The two restatements that used to sit beside it (.h1-line clip-path,
     .sticky transform) lost to the blanket's !important and promised nothing; the blanket is the
     rule, and it already delivers both. */
  .js.can-reveal .reveal{opacity:1}
  #ring .casing,#ring .arc{stroke-dashoffset:var(--ring-target,276.45)}#ring .dot,#ring .halo{opacity:1}
  /* The workout light sweep does not run under reduced motion (site.js, BEATS beat-count), and
     with the blanket stripping its translateX(-100%) the 60 per cent wide gradient would park over
     the left of the phone; it stays invisible instead (post-launch 3, 2026-09-11). */
  #ov-workout-light{opacity:0!important}
}
/* print: the same "final state" reset, for the same reason. A printed page has no scrolling and
   no observer callbacks, so anything still waiting to be revealed would print blank. */
@media print{.js.can-reveal .reveal{opacity:1;transform:none}.js.hero-armed .h1-line{clip-path:none;transform:none}}
/* depth grid, why, premium, close band, cheat sheet, footer (task 7) */
.depth,.why,.premium,.close,.band{padding:64px 16px}
.grid{list-style:none;padding:0;margin:24px 0 0;display:grid;grid-template-columns:repeat(1,minmax(0,1fr));gap:12px}
.grid li{padding:16px;border-radius:24px;background:var(--glass);border:1px solid var(--hair);box-shadow:inset 0 1px 0 var(--glass-top);color:var(--ink-2)}
.grid li b{color:var(--ink-1);font-weight:600}
.compare,.two{display:grid;gap:12px;margin-top:24px}
.compare>div,.two>div{padding:24px;border-radius:24px;background:var(--glass);border:1px solid var(--hair);box-shadow:inset 0 1px 0 var(--glass-top)}
.compare h3,.two h3{margin:0 0 8px;font-size:20px;line-height:1.2;font-weight:600}
.compare p,.two p{margin:0;color:var(--ink-2)}
.close{text-align:center;display:flex;flex-direction:column;align-items:center;gap:16px}
.icon-ring{position:relative;width:160px;height:160px;display:grid;place-items:center}
.icon-ring img{border-radius:22px}
.close-ring{position:absolute;inset:0;transform:rotate(-90deg)}
.close-ring .arc{fill:none;stroke:var(--lime);stroke-width:6;stroke-dasharray:552.9;stroke-dashoffset:552.9;transition:stroke-dashoffset var(--d1400) var(--ease-out)}
.close.is-in .close-ring .arc{stroke-dashoffset:0}
/* The code is drawn dark on its own white tile (2026-09-11): light modules on the page's black
   read as an inverted code, which OpenCV could not decode at all and some phone scanners refuse;
   drawn the right way round it decodes to https://fastlift.app/get. */
.qr{display:none;border-radius:12px}
@media(min-width:900px){.depth,.why,.premium,.close,.band{padding:120px 80px}.grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.compare,.two{grid-template-columns:repeat(2,minmax(0,1fr))}.qr{display:block}}
/* cheat sheet band: dark glass panel, lime primary button, ink-2 helper text, copy and endpoint
   moved verbatim from archive/index-2026-08-28.html (form#wf, ids and field names unchanged) */
.band{background:var(--glass);border-top:1px solid var(--hair);border-bottom:1px solid var(--hair);box-shadow:inset 0 1px 0 var(--glass-top);text-align:center}
.band .lead,.band .caption{max-width:480px;margin-left:auto;margin-right:auto}
.band .caption{margin-top:16px}
.waitform{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;max-width:480px;margin:24px auto 0}
.waitform input[type=email]{flex:1;min-width:220px;background:var(--g1);border:1px solid var(--hair);color:var(--ink-1);border-radius:16px;padding:16px 18px;font-size:16px;outline:none}
.waitform input[type=email]:focus{border-color:var(--hair-hi);outline:2px solid var(--lime);outline-offset:2px}
.waitform input::placeholder{color:var(--ink-3)}
.btn{border:0;cursor:pointer;border-radius:16px;padding:0 20px;height:52px;font-size:16px;font-weight:600;font-family:inherit}
.btn.primary{background:var(--lime);color:var(--g0)}
.btn.primary:hover{background:var(--lime-deep)}
.btn[disabled]{opacity:.6;cursor:default}
.wmsg{font-size:14px;margin-top:14px;min-height:18px;color:#FF8A8A}
.wok{display:none;font-size:16px;color:var(--ink-1);margin-top:12px}
.wok .wpdf{display:inline-flex;align-items:center;margin:12px 0 6px;text-decoration:none;color:var(--g0)}
.wok .wnote{display:block;font-size:14px;color:var(--ink-2);margin-top:6px}
/* footer */
.footer{padding:48px 16px;border-top:1px solid var(--hair)}
.footer .frow{display:flex;flex-direction:column;gap:16px}
.footer .tagline{margin:0;color:var(--ink-1);font-weight:600}
.footer .copy{margin:4px 0 0;color:var(--ink-3);font-size:13px}
.footer .fl{display:flex;flex-wrap:wrap;gap:16px;font-size:13px;padding:0;margin:0;list-style:none}
.footer .fl a{text-decoration:none}
.footer .disc{margin:24px 0 0;font-size:12px;line-height:1.6;color:var(--ink-3)}
@media(min-width:900px){.footer{padding:64px 80px}.footer .frow{flex-direction:row;align-items:center;justify-content:space-between}}
/* sticky store bar (task 8) */
.sticky{position:fixed;left:0;right:0;bottom:0;z-index:30;height:calc(64px + env(safe-area-inset-bottom));padding:0 max(16px,env(safe-area-inset-right)) env(safe-area-inset-bottom) max(16px,env(safe-area-inset-left));display:flex;align-items:center;gap:12px;background:rgba(11,11,12,.92);border-top:1px solid var(--hair);font-size:15px;transform:translateY(100%);transition:transform var(--d180) var(--ease-standard)}
.sticky.is-shown{transform:none}
/* The author rule above sets display:flex, which beats the UA stylesheet's [hidden]{display:none}
   whatever the specificity, so bar.hidden did nothing: the store link stayed in the tab order
   while the bar was "hidden", and with reduced motion (which kills the translate that was the only
   thing holding the bar off screen) the bar sat over the hero from the first paint and could never
   leave. Same shape as #ring[hidden] and .ov[hidden] above; this element was the one that missed
   it. With motion on the transform still does the 200ms enter and exit. */
.sticky[hidden]{display:none}
.sticky span{flex:1}
.sticky .badge-app,.sticky .badge-play{display:none}
[data-platform="ios"] .sticky .badge-app,[data-platform="android"] .sticky .badge-play,[data-platform="desktop"] .sticky .badge-app{display:block}
.sticky .badge-play img{margin:-12px -10px}
@media(min-width:900px){.sticky{display:none}}
#depth,#why,#premium,#close,#cheatsheet{scroll-margin-bottom:72px}
/* scroll-margin-top clears the sticky nav (M9). Without it every in-page jump, the four nav links,
   the desktop nav pill and the skip link, landed with the target's top tucked underneath it. */
#hero-badges,#story,#beat-coach,#beat-food,#beat-groups,#depth,#why,#premium,#close,#cheatsheet{scroll-margin-top:56px}
@media(min-width:900px){#hero-badges,#story,#beat-coach,#beat-food,#beat-groups,#depth,#why,#premium,#close,#cheatsheet{scroll-margin-top:72px}}
@media(max-width:899px){.footer{padding-bottom:72px}}
