/* ABOUTME: Styles for the public landing page at the site root
   ABOUTME: Stripped to a flat background, the beta signup form, the hero links, and the FAQ dialog */

/* ============ Supreme LL ============
   Same weight mapping the admin uses (admin.css): Book = 400, Jumbo = 900. Served from
   public/fonts rather than inlined as base64 so the files are cached across page loads. */
@font-face {
  font-family: 'Supreme LL';
  src: url(/fonts/SupremeLL-Book.otf) format('opentype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Supreme LL';
  src: url(/fonts/SupremeLL-Jumbo.otf) format('opentype');
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}

/* ============ tokens ============ */
:root{
  --ink:#1a1a1e;
  --ink-soft:rgba(26,26,30,.62);
  --ink-faint:rgba(26,26,30,.4);
  --brand:#B338C3;
  --brand-dark:#9C27AB;
  --glass-hi:rgba(255,255,255,.66);
  --glass-lo:rgba(255,255,255,.38);
  --glass-edge:rgba(255,255,255,.7);
  --shadow-soft:0 12px 32px rgba(30,30,36,.14);
  --ease-out:cubic-bezier(0.23,1,0.32,1);
  --ease-in-out:cubic-bezier(0.77,0,0.175,1);
  --runway:0px; /* phones only — see the corner-object note on body */
  --bleed:0px;  /* phones only — in-document height below the hero, see body::after */
}
*{box-sizing:border-box;margin:0;padding:0}
body{
  font-family:'Supreme LL',-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",system-ui,Roboto,sans-serif;
  font-weight:400;
  color:var(--ink);
  line-height:1.5;
  -webkit-font-smoothing:antialiased;

  /* ============ the edge objects ============
     All three slots are fixed <img> elements (layouts/landing.html.erb) so the drift system in
     landing.js can transform them — backgrounds can't transform. The top-cropped RIGHT slot is
     the historically risky one: in the pre-runway layout an element was guillotined at the
     status bar's bottom edge, because only the canvas — a background on body propagates there —
     paints behind the strip iOS reserves for the bar. The bet behind making it an element
     anyway: Safari 26 composites real page pixels behind the bar whenever scrollY is non-zero
     (the runway trick below), and elements should ride that compositing the same as a
     background layer did. VERIFY ON DEVICE — if the right object's head is cut at the bar,
     that slot goes back to a static background layer here.

     Safari 26's Liquid Glass behavior: at scrollY 0 it composites only the flat
     background-color behind the status bar, and real page pixels the moment the scroll is
     non-zero. So the page carries --runway of surplus height and landing.js parks the scroll
     exactly there on load — the two cancel visually, but Safari sees a scrolled page from the
     first frame. The fixed element slots need no runway correction at all: they measure from
     the viewport, not the document.
     https://1ar.io/updates/safari-26-liquid-glass-web/

     The slots render the plain -still.webp posters — frame one of each loop, so landing.js can
     swap in the animated loop from the bucket with no visual jump (same crop, same canvas). The
     per-slot tilt is CSS, in each slot's authored transform below: drift writes the independent
     `rotate` property, which composes with the transform instead of clobbering it, so the tilt
     survives the motion. The drop shadow is a live filter on .edge-obj below — unlike the old
     baked -corner files it re-runs on every loop frame (15x a second per object), which is the
     cost to watch on older phones; the -corner files are still in the tree if baking has to
     come back. Filters apply before transforms, so the shadow turns with the object — true of
     the baked copies under drift too.

     --obj-w carried the old corner files' canvas (the same ink on a larger rotated sheet —
     1579px wide where the pufferfish still is 1207); the values below are the old ones scaled
     by 1207/1579 so the ink renders at the size it always did. */
  /* One shared base width; each slot takes a fixed proportion of it, so both breakpoints and
     any future resize tune a single number. The bottom slot runs biggest per the mockup. */
  --obj-w:clamp(493px,55vw,1032px);
  --obj-l-w:var(--obj-w);
  --obj-b-w:calc(var(--obj-w) * 1.15);
  --obj-r-w:calc(var(--obj-w) * .94);
  background-color:#c6c7cc;
  /* Edge riders, not corners: one hanging off the LEFT edge with its middle below the vertical
     center, one half-sunk at the BOTTOM center, one off the RIGHT edge hanging down from above
     the top (its head crops out of the viewport, its foot reaches ~75% down). Every offset
     carries the file's transparent margin with it — for the reference pufferfish the ink starts
     7.9% of the file's width below its top edge, stops 5.3% short of its right one, starts 7.6%
     in from its left one, and 8.1% above its bottom one. The left slot's
     vertical `top 65%` is proportional alignment, not a length: it lines the image's 65% mark
     up with the box's 65% mark, which is what keeps it below center at every screen height. */
  /* The six position numbers, held as custom properties so the dev tuner (?tuner=1 mounts
     landing/_tuner) can drive them live and emit this block back for pasting. -xf/-yf are
     fractions of the slot's own width (negative = hanging off that edge); -x/-y are ordinary
     position percentages. The element rules below read these. */
  --obj-b-x:50%;  --obj-b-yf:-.52;
  --obj-l-xf:-.53;--obj-l-y:65%;
  --obj-r-xf:-.33;--obj-r-yf:-.10;
}
/* ============ liquid chrome ============
   The shimmer behind everything: two oversized metallic gradients and a specular glow, all
   fixed and one layer below the objects (z-index -2 to their -1). landing.js eases the foils
   around the pointer with a slow idle sine riding underneath, so the sheen keeps breathing on
   touch screens where the sine is all there is; the glow chases the cursor and rests
   upper-center without one. inset:-55% oversizes the foils so their travel never exposes an
   edge. Under reduced motion the loop never starts: the foils hold these authored positions
   as a static finish and the glow goes entirely. */
.foil-a,.foil-b,.glow{position:fixed;pointer-events:none;z-index:-2;will-change:transform}
.foil-a{
  inset:-55%;
  background:linear-gradient(112deg,
    #85868c 0%, #d7d8dd 10%, #ffffff 19%, #c2c3c9 30%, #8e8f96 42%,
    #e9eaee 54%, #ffffff 62%, #b3b4bb 74%, #83848a 86%, #cfd0d5 100%);
}
.foil-b{
  inset:-55%;
  background:linear-gradient(74deg,
    rgba(255,255,255,0) 0%, rgba(255,255,255,.55) 12%, rgba(120,121,128,.4) 26%,
    rgba(255,255,255,0) 38%, rgba(255,255,255,.65) 52%, rgba(110,111,118,.35) 66%,
    rgba(255,255,255,.5) 80%, rgba(255,255,255,0) 100%);
  mix-blend-mode:soft-light;
  opacity:.9;
}
.glow{
  left:0;top:0;width:110vmax;height:110vmax;
  margin-left:-55vmax;margin-top:-55vmax;
  background:radial-gradient(closest-side, rgba(255,255,255,.5), rgba(255,255,255,0) 60%);
}
@media (prefers-reduced-motion:reduce){
  .foil-a,.foil-b{transform:none !important}
  .glow{display:none}
}

/* ============ the drifting objects ============
   The left and bottom slots, fixed to the viewport at the same offsets their background layers
   used — the position vars above keep feeding them, so the tuner drives them unchanged. Drift
   (landing.js) writes the independent `translate` and `rotate` properties, so the authored
   left/top/bottom and the centering `transform` underneath are never touched by the motion —
   they stay the anchor drift wanders around, and what shows when motion is reduced and drift
   never starts. z-index:-1 sits them above the canvas (and the right slot's layer) but under
   everything real; DOM order paints the bottom object over the left one, matching the old
   layer order. max-width:none unclamps the base img rule — the bottom slot runs wider than the
   viewport on phones. */
/* The shadow approximates the old bake (30% black, sigma 38, +52px down at ~1200px file
   scale) at typical rendered size. CSS blur here is 2x the sigma it draws with. */
.edge-obj{
  position:fixed;z-index:-1;max-width:none;pointer-events:none;
  filter:drop-shadow(0 42px 60px rgba(0,0,0,.30));
}
/* Each slot's base tilt (varied ±7–11° so no two share one) rides here rather than in the
   files. Rotation composes after the centering translate, and a rotate following a pure
   translate is still a rotation about the element's own moved center — the anchors hold.
   --par-x/--par-y are the pointer parallax, written per element by landing.js — held as
   custom properties because JS writing style.transform would clobber the centering and tilt
   authored here, where a var slots into the same list. Drift's translate/rotate are separate
   independent properties and never touch any of this. */
#obj-l{
  width:var(--obj-l-w);
  left:calc(var(--obj-l-w) * var(--obj-l-xf));
  top:var(--obj-l-y);
  transform:translate(var(--par-x,0px),var(--par-y,0px)) translateY(calc(-1 * var(--obj-l-y))) rotate(-9deg);
}
#obj-b{
  width:var(--obj-b-w);
  left:var(--obj-b-x);
  bottom:calc(var(--obj-b-w) * var(--obj-b-yf));
  transform:translate(var(--par-x,0px),var(--par-y,0px)) translateX(-50%) rotate(7deg);
}
#obj-r{
  width:var(--obj-r-w);
  right:calc(var(--obj-r-w) * var(--obj-r-xf));
  top:calc(var(--obj-r-w) * var(--obj-r-yf));
  transform:translate(var(--par-x,0px),var(--par-y,0px)) rotate(11deg);
}
/* The runway itself: an empty strip above the hero for landing.js to park the scroll past.
   Zero-height above the phone breakpoint. */
body::before{content:"";display:block;height:var(--runway)}
/* The runway's bottom twin, and the bottom toolbar's price of admission: Safari 26 composites
   page pixels through the toolbar's glass only while the scroll rests BELOW its maximum —
   parked at maxScroll the bar paints flat root color and hard-cuts the page at its top edge
   (verified empirically, 2026-08). This strip of real document below the hero keeps the park
   mid-range. The mandatory snap keeps it unreachable as a resting position, same as the runway
   above. */
body::after{content:"";display:block;height:var(--bleed)}
img{display:block;max-width:100%;-webkit-user-drag:none;user-select:none}
a{color:inherit}
button{font:inherit;cursor:pointer;border:0;background:none;color:inherit}
:focus-visible{outline:2px solid var(--ink);outline-offset:3px;border-radius:6px}

/* ============ hero ============ */
.hero{
  /* Positioned for .hero-links, which anchors to its floor. */
  position:relative;
  /* The body::before runway above this box gives the page a scroll range of exactly the offset
     landing.js parks it at, so Safari 26 keeps painting the corner object behind the status bar
     (see the note on body). dvh rather than svh so that range survives the chrome collapsing —
     with a static unit the taller viewport would swallow it and clamp the scroll back to 0. */
  min-height:100dvh;
  display:flex;align-items:center;justify-content:center;
  /* The layout asks for viewport-fit=cover so the background reaches the screen edges, which
     means the notch and the home indicator are ours to avoid. Held in variables so a breakpoint
     can restate the padding without having to restate the insets with it. */
  --pad-t:80px; --pad-x:24px; --pad-b:96px;
  padding:
    calc(var(--pad-t) + env(safe-area-inset-top))
    calc(var(--pad-x) + env(safe-area-inset-right))
    calc(var(--pad-b) + env(safe-area-inset-bottom))
    calc(var(--pad-x) + env(safe-area-inset-left));
}
/* No width cap here: at desktop sizes the wordmark outgrows any fixed cap, and centered
   inline content that overflows its block spills out the END edge only (right, in LTR) —
   which shoved the title off the form's axis. The children cap their own widths instead. */
.hero-copy{position:relative;text-align:center}
/* No text-shadow — the in-app title renders flat, and this matches it. */
.hero h1{
  font-size:clamp(64px, 12.5vw, 168px);
  font-weight:900;line-height:.96;letter-spacing:-.03em;
  color:#fff;
  margin-bottom:14px;
}
.hero .sub{
  font-size:clamp(17px,2.4vw,21px);color:var(--ink-soft);
  margin:0 auto 36px;text-wrap:balance;
}

/* ============ hero links ============ */
.hero-links{
  position:absolute;left:0;right:0;
  bottom:calc(30px + env(safe-area-inset-bottom));
  display:flex;gap:4px;flex-wrap:wrap;justify-content:center;
}
.hero-link{
  text-decoration:none;font-size:12px;font-weight:900;letter-spacing:.05em;text-transform:uppercase;
  color:var(--brand);
  padding:8px 12px;border-radius:999px;
  transition:color 150ms ease, background-color 150ms ease;
}
.hero-link:hover{color:var(--brand-dark);background:rgba(255,255,255,.55)}

/* ============ signup ============ */
.signup{max-width:440px;margin:0 auto}
/* The homepage runs without a tagline while a new one is written, so the form follows the
   wordmark directly and carries the gap the tagline's margin used to provide. */
.hero h1 + .signup{margin-top:36px}
.signup-row{
  display:flex;gap:6px;padding:6px;border-radius:999px;
  background:linear-gradient(180deg,var(--glass-hi),var(--glass-lo));
  -webkit-backdrop-filter:blur(18px) saturate(170%);
  backdrop-filter:blur(18px) saturate(170%);
  border:1px solid rgba(255,255,255,.55);border-top-color:var(--glass-edge);
  box-shadow:var(--shadow-soft), inset 0 1px 0 rgba(255,255,255,.75);
}
.signup-row.shake{animation:shake 260ms var(--ease-in-out)}
@keyframes shake{20%{transform:translateX(-5px)}45%{transform:translateX(4px)}70%{transform:translateX(-3px)}90%{transform:translateX(2px)}}
.signup input{
  flex:1;min-width:0;border:0;background:transparent;outline:none;
  font:inherit;font-size:16px;color:var(--ink);padding:10px 12px 10px 16px;
}
.signup input::placeholder{color:var(--ink-faint)}
.signup .go{
  background:var(--brand);color:#fff;font-size:14px;font-weight:900;letter-spacing:.04em;text-transform:uppercase;
  padding:12px 20px;border-radius:999px;flex-shrink:0;
  transition:transform 120ms var(--ease-out), background-color 150ms ease;
}
.signup .go:hover{background:var(--brand-dark)}
.signup .go:active{transform:scale(.96)}
.signup .go[disabled]{cursor:progress}
.signup .error{font-size:13px;font-weight:900;color:#8c2f39;margin-top:10px;display:none}
.signup.is-error .error{display:block}
/* Marketing consent is captured by this line rather than a checkbox, so it has to stay
   legible — quiet, but never smaller than the error text it sits beside. Full width of the
   form row above it, no cap of its own. */
.signup .consent{font-size:13px;line-height:1.45;color:var(--ink-soft);margin:12px 0 0;text-wrap:balance}
.signup .success{
  display:none;align-items:center;justify-content:center;gap:10px;
  padding:15px 20px;border-radius:999px;
  background:linear-gradient(180deg,var(--glass-hi),var(--glass-lo));
  -webkit-backdrop-filter:blur(18px) saturate(170%);
  backdrop-filter:blur(18px) saturate(170%);
  border:1px solid rgba(255,255,255,.55);border-top-color:var(--glass-edge);
  box-shadow:var(--shadow-soft), inset 0 1px 0 rgba(255,255,255,.75);
  font-weight:900;font-size:15px;
}
.signup .success .check{
  width:22px;height:22px;border-radius:50%;background:var(--brand);color:#fff;
  display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;
}
.signup .success .check svg{width:11px;height:11px}
.signup.is-done .signup-row,.signup.is-done .error,.signup.is-done .consent{display:none}
.signup.is-done .success{display:flex;animation:pop 340ms var(--ease-out)}
@keyframes pop{0%{transform:scale(.9);opacity:0;filter:blur(3px)}60%{transform:scale(1.02)}100%{transform:scale(1);opacity:1;filter:blur(0)}}

/* ============ /beta ============
   The unlisted closed-beta page: one translucent card floating over the chrome and the
   drifting objects, where the homepage has the wordmark and form. The gray is the FAQ
   overlay's tone lifted a step and left open enough for the objects to register through it. */
.beta-card{
  max-width:520px;margin:0 auto;
  padding:28px 32px 26px;border-radius:26px;
  background:rgba(214,215,220,.8);
  -webkit-backdrop-filter:blur(18px) saturate(140%);
  backdrop-filter:blur(18px) saturate(140%);
  box-shadow:var(--shadow-soft);
}
/* The app icon stands in for the wordmark; 22.5% is the iOS icon corner ratio. */
.beta-icon{
  display:block;width:74px;height:74px;margin:0 auto 24px;
  border-radius:22.5%;
  box-shadow:0 6px 18px rgba(30,30,36,.18);
}
.beta-copy{font-size:clamp(17px,2.2vw,19px);line-height:1.35;color:var(--ink-soft);text-wrap:balance}
.beta-copy + .beta-copy{margin-top:1.5em}
.beta-cta{
  display:inline-block;margin-top:30px;
  background:var(--brand);color:#fff;font-size:20px;font-weight:900;letter-spacing:.03em;text-transform:uppercase;
  padding:13px 48px;border-radius:999px;text-decoration:none;
  transition:transform 120ms var(--ease-out), background-color 150ms ease;
}
.beta-cta:hover{background:var(--brand-dark)}
.beta-cta:active{transform:scale(.96)}

/* ============ modal chrome ============
   The FAQ dialog on the homepage — and, reused as a whole page, the privacy policy
   (views/legal/privacy.html.erb renders this chrome always-open with the X as a link home),
   so both surfaces share one overlay, one sheet width, and one X.
   [hidden]'s UA display:none loses to the .modal rule on specificity, so it is restated. */
.modal{
  position:fixed;inset:0;z-index:80;
  display:flex;align-items:flex-start;justify-content:center;
  padding:
    calc(24px + env(safe-area-inset-top))
    calc(24px + env(safe-area-inset-right))
    calc(24px + env(safe-area-inset-bottom))
    calc(24px + env(safe-area-inset-left));
  overflow-y:auto;overscroll-behavior:contain;
  background:rgba(198,199,204,.8);
  -webkit-backdrop-filter:blur(22px) saturate(150%);
  backdrop-filter:blur(22px) saturate(150%);
}
.modal[hidden]{display:none}
/* margin:0 auto, not auto: in this flex row an all-auto margin would center the sheet
   vertically whenever it's shorter than the viewport — both surfaces top-align instead. */
.modal-sheet{
  position:relative;width:100%;max-width:680px;margin:0 auto;
  padding:44px 40px 40px;
}
.modal:not([hidden]) .modal-sheet{animation:sheet-in 340ms var(--ease-out)}
@keyframes sheet-in{from{opacity:0;transform:translateY(18px);filter:blur(6px)}to{opacity:1;transform:none;filter:blur(0)}}
/* Pinned to the viewport, not the sheet: both surfaces scroll and the X has to stay
   reachable the whole way down. It lives as a sibling of .modal-sheet — inside it, the open
   animation's transform would re-anchor fixed positioning to the sheet while it plays.
   z-index lifts it over the sheet, which paints later in DOM order. */
.modal-close{
  position:fixed;
  top:calc(20px + env(safe-area-inset-top));
  right:calc(20px + env(safe-area-inset-right));
  z-index:1;
  width:40px;height:40px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:rgba(255,255,255,.6);color:var(--ink);
  text-decoration:none;
  transition:background-color 150ms ease, transform 120ms var(--ease-out);
}
.modal-close:hover{background:#fff}
.modal-close:active{transform:scale(.94)}
.modal-close svg{width:14px;height:14px}
.modal-title{
  font-size:clamp(30px,5vw,48px);font-weight:900;letter-spacing:-.01em;line-height:1.02;
  text-transform:uppercase;color:var(--brand);
  margin-bottom:28px;text-wrap:balance;
}
.faq-list{list-style:none}
.faq-item{border-top:1px solid rgba(26,26,30,.16)}
.faq-item:last-child{border-bottom:1px solid rgba(26,26,30,.16)}
.faq-q{
  width:100%;display:flex;align-items:center;gap:16px;justify-content:space-between;
  padding:20px 2px;text-align:left;
  font-size:clamp(15px,2vw,18px);font-weight:900;letter-spacing:.02em;text-transform:uppercase;
  color:var(--brand);line-height:1.25;
  transition:color 150ms ease;
}
.faq-q:hover{color:var(--brand-dark)}
/* A plus that loses its upright stroke when the answer is open. */
.faq-mark{position:relative;width:14px;height:14px;flex-shrink:0}
.faq-mark::before,.faq-mark::after{
  content:"";position:absolute;left:50%;top:50%;background:currentColor;border-radius:2px;
  transition:transform 220ms var(--ease-out), opacity 220ms var(--ease-out);
}
.faq-mark::before{width:14px;height:2px;transform:translate(-50%,-50%)}
.faq-mark::after{width:2px;height:14px;transform:translate(-50%,-50%)}
.faq-q[aria-expanded="true"] .faq-mark::after{transform:translate(-50%,-50%) rotate(90deg);opacity:0}
.faq-a{padding:0 2px 24px;max-width:56ch}
.faq-a p{font-size:15.5px;line-height:1.55;color:var(--ink)}
.faq-a:not([hidden]){animation:answer-in 260ms var(--ease-out)}
@keyframes answer-in{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
body.modal-open{overflow:hidden}

/* ============ responsive ============ */
@media (max-width:640px){
  /* Where both bars' problems live: the runway parks the scroll past zero for the status bar,
     the bleed keeps the park below maxScroll for the toolbar's glass. */
  :root{--runway:62px;--bleed:120px}

  /* One page, no scrolling — but the scroll STATE has to stay parked on the runway, since a
     page resting at scrollY 0 is what resurrects the guillotine, so the range itself can't be
     removed. Instead touch-action turns the pan gesture off at the root: html sits in the
     consulted chain of every document pan, while the FAQ sheet scrolls itself and so is never
     consulted — it keeps scrolling. Pinch zoom is deliberately left on. The mandatory snap
     stays as the net for scrolls that aren't gestures — focus reveals, find-in-page — with the
     hero as the only snap area, no resting position above the runway line is valid. */
  html{scroll-snap-type:y mandatory;touch-action:pinch-zoom}
  .hero{scroll-snap-align:start}

  /* Same three slots, shrunk through the shared base so the slot proportions survive. The
     fixed elements need no bleed/runway corrections: they measure from the viewport, not the
     document, so the surplus strips never enter their math. */
  body{
    --obj-w:min(73vw,367px);
    /* Phones sink the bottom object deeper than desktop's -.52 — at the narrow width its top
       edge otherwise crowds the signup row. */
    --obj-b-yf:-.64;
  }

  /* The links keep the base rule's pinning to the hero's floor — the viewport bottom, since
     the parked page fills it with the hero exactly. --pad-b reserves their row's height so the
     centered copy can't collide with it on short screens. */
  .hero{--pad-t:56px;--pad-x:20px;--pad-b:88px}
  .beta-card{padding:26px 24px 24px}

  .modal{
    padding:
      calc(16px + env(safe-area-inset-top))
      calc(16px + env(safe-area-inset-right))
      calc(16px + env(safe-area-inset-bottom))
      calc(16px + env(safe-area-inset-left));
  }
  .modal-sheet{padding:40px 20px 32px}
  .modal-close{
    top:calc(12px + env(safe-area-inset-top));
    right:calc(12px + env(safe-area-inset-right));
  }
  .faq-q{padding:18px 2px}
}

/* ============ accessibility prefs ============ */
@media (prefers-reduced-motion:reduce){
  .signup-row.shake{animation:none}
  .signup.is-done .success{animation:none}
  .modal:not([hidden]) .modal-sheet{animation:none}
  .faq-a:not([hidden]){animation:none}
  .faq-mark::before,.faq-mark::after{transition:none}
}
@media (prefers-reduced-transparency:reduce){
  .signup-row,.signup .success{
    background:#f5f5f7;-webkit-backdrop-filter:none;backdrop-filter:none;
  }
  .modal{background:#c6c7cc;-webkit-backdrop-filter:none;backdrop-filter:none}
  .beta-card{background:#d6d7dc;-webkit-backdrop-filter:none;backdrop-filter:none}
}
