/* ============================================================
   LATITUDE — shared
   One field of points runs every page. One preloader, one nav, one
   way text arrives. Pages differ only in what the field is currently
   describing and what sits on top of it.
   ============================================================ */

:root{
  --ink:#0A0A10; --ink2:#0D0D16; --ink3:#11101C; --ink4:#151324;
  --bone:#F4F3EE; --violet:#BC94FB; --violet-d:#7C5FD3; --violet-l:#F6B0FF;
  --graphite:#7E838C;
  --ui:"Helvetica Neue",Inter,system-ui,-apple-system,sans-serif;
  --mono:ui-monospace,"SF Mono",Menlo,monospace;
  /* viewport-fit=cover deliberately extends the page under the notch and
     the home indicator, so every edge measurement has to add the inset
     back. Without this the nav wordmark sits under the Dynamic Island in
     landscape and the footer sits under the home indicator. */
  --sat:env(safe-area-inset-top,0px);
  --sar:env(safe-area-inset-right,0px);
  --sab:env(safe-area-inset-bottom,0px);
  --sal:env(safe-area-inset-left,0px);
  --pad:calc(clamp(20px,4.4vw,64px) + max(var(--sal),var(--sar)));
  --hair:rgba(244,243,238,.11);
}

*{margin:0;padding:0;box-sizing:border-box}
html{background:var(--ink);-webkit-text-size-adjust:100%}
body{background:var(--ink);color:var(--bone);font-family:var(--ui);
  -webkit-font-smoothing:antialiased;overflow-x:hidden;
  overscroll-behavior-y:none}
/* 100vh is the tall viewport: on iOS it stays tall while the URL bar is
   showing, so a locked body was 60-100px taller than the screen. */
body.lock{overflow:hidden;height:100dvh}
a{color:inherit;text-decoration:none}
img{display:block;max-width:100%}
::selection{background:var(--violet);color:var(--ink)}

/* The aurora wash. Its backing store is deliberately half resolution and
   the GPU stretches it to fill: it is three soft gradients, so native
   resolution buys nothing and the compositor does the scale for free. */
/* top/left only, never inset:0 or height:100%. Both canvases are given an
   explicit pixel height by resize(), taller than the viewport, so that an
   in-app browser toolbar sliding in cannot rescale or re-centre the field.
   See the VIEWPORT STABILITY note in app.js. */
#aurora{position:fixed;top:0;left:0;z-index:0;display:block;
  width:100%;height:100%;pointer-events:none}
#field{position:fixed;top:0;left:0;z-index:0;display:block;width:100%;height:100%}
/* ---------- FILM GRAIN ----------
   This used to be a full-screen pattern fillRect inside the canvas, i.e. a
   third full-screen pass over 3 MILLION device pixels on every single
   frame, on the main thread. As a fixed layer with a tiled image and a
   stepped transform it lives on the compositor and costs the frame loop
   nothing at all. background-size is set from JS to tile/DPR so one texel
   is one DEVICE pixel, otherwise it goes back to DPR x DPR blocks.
   Plain alpha, never mix-blend-mode: a blended full-screen layer over a
   canvas that repaints every frame is what tore the old site. */
/* background-size lands on a fractional CSS px (128/3), so the tile is
   resampled and the noise smears back into DPRxDPR blobs. pixelated forces
   nearest-neighbour, which is what keeps one texel on one device pixel. */
#grain{position:fixed;inset:-64px;z-index:0;pointer-events:none;
  background-image:url(grain.png?v=202608090318);background-repeat:repeat;
  image-rendering:pixelated;
  will-change:transform;animation:grainshift .6s steps(1,end) infinite}
@keyframes grainshift{
  0%  {transform:translate3d(0,0,0)}
  8%  {transform:translate3d(-19px,11px,0)}
  16% {transform:translate3d(7px,-23px,0)}
  25% {transform:translate3d(-31px,-7px,0)}
  33% {transform:translate3d(13px,29px,0)}
  41% {transform:translate3d(-5px,-17px,0)}
  50% {transform:translate3d(27px,3px,0)}
  58% {transform:translate3d(-11px,21px,0)}
  66% {transform:translate3d(17px,-29px,0)}
  75% {transform:translate3d(-25px,-13px,0)}
  83% {transform:translate3d(3px,19px,0)}
  91% {transform:translate3d(-17px,-3px,0)}
}
@media (prefers-reduced-motion:reduce){#grain{animation:none}}
#shell{position:relative;z-index:1}
/* ---------- SCROLL OWNERSHIP ----------
   On a touch device the DOCUMENT does not scroll, #shell does. A browser
   only collapses its chrome for the main frame, so locking the document
   pins the toolbar: the viewport stops resizing on every change of scroll
   direction and the browser stops yanking the scroll position to re-anchor.
   Measured in Instagram's browser: 21 resizes and a 136px yank become 0
   and 0. See the SCROLL OWNERSHIP note in app.js. Desktop never gets this
   class, so wheel and smooth scroll are untouched. */
html.isc,html.isc body{height:100%;overflow:hidden;overscroll-behavior:none}
html.isc #shell{position:fixed;top:0;left:0;right:0;bottom:0;
  overflow-y:auto;overflow-x:hidden;overscroll-behavior-y:none}
/* the preloader locks the page: with an inner scroller that is the scroller */
html.isc body.lock #shell{overflow:hidden}
section{position:relative}

/* ---------- type ---------- */
.eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:.20em;
  text-transform:uppercase;color:var(--graphite)}
.dsp{font-weight:500;letter-spacing:-.03em;line-height:1.02;
  font-size:clamp(38px,7.4vw,104px)}
.mid{font-weight:500;letter-spacing:-.022em;line-height:1.1;
  font-size:clamp(24px,3.5vw,46px)}
.body{font-size:clamp(15px,1.15vw,17px);line-height:1.62;color:#B9BCC4;max-width:46ch}
.vi{color:var(--violet)}
.dot{display:inline-block;vertical-align:baseline;width:.15em;height:.15em;
  border-radius:50%;background:var(--violet);margin-left:.08em;
  box-shadow:0 0 22px rgba(188,148,251,.55)}

/* ============================================================
   TEXT MOTION
   Words are set into place from below behind a mask, not faded in.
   A mask is the whole difference between "it appeared" and "it was
   put there".
   ============================================================ */
.sp{display:inline-block;overflow:hidden;vertical-align:bottom;
  padding-bottom:.14em;margin-bottom:-.14em}
.sp > .wd{display:inline-block;transform:translateY(115%);
  transition:transform .9s cubic-bezier(.16,1,.3,1)}
.rv.in .sp > .wd{transform:translateY(0);transition-delay:var(--wd,0ms)}
.rv.up .sp > .wd{transform:translateY(-115%)}

/* anything not word split still travels, it just travels whole */
.rv:not([data-split]){opacity:0;transform:translateY(26px);
  transition:opacity .75s cubic-bezier(.16,1,.3,1),transform .75s cubic-bezier(.16,1,.3,1)}
.rv:not([data-split]).up{transform:translateY(-26px)}
.rv:not([data-split]).in{opacity:1;transform:none;
  transition-delay:var(--d,0ms);transition-duration:.95s}

@media (prefers-reduced-motion:reduce){
  .rv:not([data-split]){opacity:1;transform:none;transition:none}
  .sp > .wd{transform:none !important;transition:none}
}

/* ============================================================
   PRELOADER
   Two halves of one composition, each clipped to its own side. They
   look like a single panel until they leave in opposite directions,
   so the wordmark tears down the middle rather than fading out.
   ============================================================ */
#pre{position:fixed;inset:0;z-index:100;pointer-events:none}
#pre .half{position:absolute;top:0;bottom:0;width:50vw;overflow:hidden;
  background:var(--ink);
  transition:transform 1.15s cubic-bezier(.76,0,.24,1)}
#pre .l{left:0}
#pre .r{left:50vw}
#pre .half .inner{position:absolute;top:0;left:0;width:100vw;height:100%;
  display:flex;flex-direction:column;align-items:center;justify-content:center}
#pre .r .inner{left:-50vw}
#pre.open .l{transform:translateX(-100.5%)}
#pre.open .r{transform:translateX(100.5%)}
/* the wordmark is set the same way the page transition sets its label:
   mono, wide, bracketed, so arriving and moving are the same voice */
#pre .wm{display:flex;align-items:center;gap:.68em;padding-left:.34em;
  font-family:var(--mono);font-weight:400;line-height:1;
  font-size:clamp(19px,3.6vw,42px);letter-spacing:.34em;
  text-transform:uppercase;color:var(--bone)}
#pre .wm b{color:var(--violet);font-weight:400}
#pre .stand{margin-top:30px;display:flex;align-items:center;gap:11px;
  font-family:var(--mono);font-size:10px;
  letter-spacing:.26em;text-transform:uppercase;color:var(--graphite)}
#pre .stand:before{content:'';width:5px;height:5px;border-radius:50%;
  background:var(--violet);box-shadow:0 0 11px rgba(188,148,251,.85);
  animation:preblink 1.7s ease-in-out infinite}
@keyframes preblink{0%,100%{opacity:.22}50%{opacity:1}}
#pre .barwrap{margin-top:44px;display:flex;align-items:center;gap:18px;
  width:min(46vw,520px)}
#pre .bar{position:relative;flex:1;height:1px;background:rgba(244,243,238,.16)}
#pre .bar span{position:absolute;left:0;top:0;bottom:0;width:0%;
  background:linear-gradient(90deg,rgba(124,95,211,.35),var(--violet))}
#pre .bar span:after{content:'';position:absolute;right:-1px;top:-2px;
  width:2px;height:5px;background:var(--bone)}
#pre .pc{font-family:var(--mono);font-size:11px;letter-spacing:.12em;
  color:var(--violet);width:4ch;text-align:right;font-variant-numeric:tabular-nums}
/* the seam only exists while the curtain is closed */
#pre .r .inner{border-left:1px solid rgba(244,243,238,.06)}

/* ============================================================
   NAV
   ============================================================ */
#nav{position:fixed;top:0;left:0;right:0;z-index:30;
  transition:transform 380ms cubic-bezier(.16,1,.3,1)}
#nav.hidden{transform:translateY(-102%);transition:transform 130ms linear}
#nav .prog{height:1px;background:rgba(244,243,238,.09);position:relative}
#nav .prog i{position:absolute;left:0;top:0;bottom:0;width:0%;background:var(--violet)}
#nav .in{display:flex;align-items:center;justify-content:space-between;
  padding:calc(22px + var(--sat)) var(--pad) 22px;
  border-bottom:1px solid transparent;transition:border-color .5s ease}
#nav.edge .in{border-bottom-color:var(--hair)}
#nav .wm{display:flex;align-items:center;gap:13px;font-size:15px;font-weight:500;
  letter-spacing:.30em;text-transform:uppercase}
#nav .wm svg{width:20px;height:20px;flex:none;overflow:visible;
  transition:transform 1.1s cubic-bezier(.16,1,.3,1)}
#nav .lk{display:flex;gap:clamp(16px,2.6vw,38px)}
#nav .lk a{position:relative;display:flex;align-items:baseline;gap:8px;
  font-family:var(--mono);font-size:12px;letter-spacing:.16em;
  text-transform:uppercase;color:#A7ABB4;
  padding-bottom:6px;transition:color .35s ease}
#nav .lk a em{font-style:normal;font-size:10px;color:var(--violet);opacity:.7;
  transition:opacity .35s ease}
#nav .lk a:before{content:'';position:absolute;left:0;right:0;bottom:0;height:1px;
  background:var(--violet);transform:scaleX(0);transform-origin:left;
  transition:transform .55s cubic-bezier(.16,1,.3,1)}
/* :hover latches on iOS after a tap and stays latched until you tap
   elsewhere, so a tapped link would sit permanently lit. Every hover state
   on the site is gated on an input that can actually hover. */
@media (hover:hover){
  #nav .wm:hover svg{transform:rotate(180deg)}
  #nav .lk a:hover{color:var(--bone)}
  #nav .lk a:hover em{opacity:1}
  #nav .lk a:hover:before{transform:scaleX(1)}
}
#nav .lk a.on{color:var(--bone)}
#nav .lk a.on em{opacity:1}
#nav .lk a.on:before{transform:scaleX(1)}
@media (max-width:620px){
  #nav .in{padding:calc(16px + var(--sat)) var(--pad) 16px}
  #nav .wm{letter-spacing:.2em;font-size:12px}
  #nav .wm svg{width:16px;height:16px}
  #nav .lk a em{display:none}
  #nav .lk a{font-size:11px}
  #nav .lk{gap:16px}
}
/* the three links were a 19px-tall hit area: the whole navigation of the
   site, well under the 44px a fingertip needs */
@media (pointer:coarse){
  #nav .lk a{min-height:44px;align-items:center;padding-bottom:0}
  #nav .lk a:before{bottom:11px}
  #nav .lk{gap:20px}
  #nav .wm{min-height:44px;align-items:center}
}

/* ============================================================
   CURSOR
   ============================================================ */
#cur{position:fixed;left:0;top:0;z-index:40;pointer-events:none;
  transform:translate3d(-300px,-300px,0);will-change:transform}
#cur span{display:grid;place-items:center;width:104px;height:104px;
  margin:-52px 0 0 -52px;border-radius:50%;
  background:rgba(13,11,24,.52);border:1px solid rgba(188,148,251,.62);
  backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px);
  font-family:var(--mono);font-size:10px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--bone);text-align:center;line-height:1.4;
  transform:scale(0);opacity:0;
  transition:transform .42s cubic-bezier(.16,1,.3,1),opacity .3s ease}
#cur.on span{transform:scale(1);opacity:1}
@media (pointer:coarse){#cur{display:none}}

/* ============================================================
   HERO
   ============================================================ */
#hero{height:100svh;min-height:600px;overflow:clip}

/* The hero says one thing and says it twice, low on either side of the
   world, so the instruction reads wherever the eye already is. Centred
   by layout, not by transform: the enter animation owns transform and
   would otherwise wipe the centring off. */
.scrollcue{pointer-events:none;user-select:none;-webkit-user-select:none;position:absolute;z-index:3;
  bottom:calc(clamp(84px,12vh,140px) + var(--sab));
  display:flex;flex-direction:column;gap:16px;
  opacity:0;transform:translateY(22px);
  transition:opacity .9s ease,transform 1s cubic-bezier(.16,1,.3,1)}
/* the tick hangs over the middle of the words, not off one end of them */
.scrollcue.l{left:var(--pad);align-items:center}
.scrollcue.r{right:var(--pad);align-items:center}
.scrollcue .track{position:relative;width:1px;height:clamp(42px,7vh,66px);
  background:rgba(244,243,238,.20);overflow:hidden}
.scrollcue .track i{position:absolute;left:-1px;width:3px;height:16px;border-radius:2px;
  background:var(--violet);box-shadow:0 0 14px rgba(188,148,251,.95)}
.scrollcue .lb{font-family:var(--mono);font-size:13px;letter-spacing:.30em;
  text-transform:uppercase;color:var(--bone);white-space:nowrap}
body.up .scrollcue{opacity:1;transform:none}
body.up .scrollcue.l{transition-delay:.26s}
body.up .scrollcue.r{transition-delay:.38s}
body.up .scrollcue .track i{animation:fall 2.1s cubic-bezier(.5,0,.5,1) infinite}
@keyframes fall{
  0%{transform:translateY(-18px);opacity:0}
  22%{opacity:1}
  78%{opacity:1}
  100%{transform:translateY(74px);opacity:0}
}
/* it has done its job the moment you scroll */
body.moved .scrollcue{opacity:0 !important;transform:translateY(14px) !important;
  transition:opacity .5s ease,transform .5s ease}
@media (max-width:760px){
  .scrollcue.r{display:none}
  .scrollcue.l{left:0;right:0;align-items:center}
  .scrollcue .lb{font-size:11px;letter-spacing:.22em}
}
/* A phone on its side is ~390px tall, so a 600px hero pushed both cues
   below the fold and the first screen carried no instruction at all. */
@media (max-height:520px){
  #hero{min-height:100svh}
  .scrollcue{bottom:calc(clamp(28px,7vh,60px) + var(--sab));gap:9px}
  .scrollcue .track{height:clamp(26px,5vh,40px)}
}

/* ============================================================
   CTA — a dial, not a box. The ring echoes the world in the hero and
   the halo of points that gathers around it.
   ============================================================ */
#cta{min-height:100svh;display:grid;place-items:center;padding:14vh var(--pad)}
.cta{position:relative;display:grid;place-items:center;cursor:none;
  width:clamp(250px,30vw,400px);aspect-ratio:1;border-radius:50%;
  transition:transform .8s cubic-bezier(.16,1,.3,1)}
.cta .ring{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.cta .ring circle{fill:none;stroke:rgba(188,148,251,.5);stroke-width:.5;
  transition:stroke .5s ease,stroke-width .5s ease}
.cta .ring .dash{stroke:rgba(188,148,251,.85);stroke-width:1;
  stroke-dasharray:2 12;transform-origin:50% 50%;
  transition:stroke .5s ease}
.cta.in .ring .dash{animation:dial 26s linear infinite}
@keyframes dial{to{transform:rotate(360deg)}}
.cta .fill{position:absolute;inset:0;border-radius:50%;
  background:radial-gradient(circle at 50% 62%,rgba(188,148,251,.22),rgba(188,148,251,0) 70%);
  opacity:.5;transition:opacity .7s ease}
.cta .txt{position:relative;text-align:center;z-index:2}
.cta .big{display:block;font-weight:500;letter-spacing:-.035em;line-height:.98;
  font-size:clamp(34px,4.6vw,64px)}
.cta .sm{display:flex;align-items:center;justify-content:center;gap:12px;
  margin-top:16px;font-family:var(--mono);font-size:11px;letter-spacing:.28em;
  text-transform:uppercase;color:#C6C9D0;transition:color .5s ease}
.cta .sm .ln{display:block;width:24px;height:1px;background:var(--violet);
  transition:width .6s cubic-bezier(.16,1,.3,1)}
@media (hover:hover){
  .cta:hover{transform:scale(1.035)}
  .cta:hover .ring circle{stroke:rgba(188,148,251,.9)}
  .cta:hover .fill{opacity:1}
  .cta:hover .sm{color:var(--bone)}
  .cta:hover .sm .ln{width:42px}
}
@media (pointer:coarse){.cta{cursor:pointer}}

/* ============================================================
   PAGE TRANSITION
   One motion in two halves. A panel wipes UP over the page carrying the
   name of where you are going, and on the far side it does not fall
   away, it tears down the middle and leaves sideways the way the
   preloader does. Arriving and moving read as the same gesture.
   ============================================================ */
/* --tr-bg is the colour of the page you are going TO, set before the
   panel is ever painted. A panel that is a different tone from the page
   it uncovers does not read as a wipe, it reads as a flash. */
#tr{position:fixed;inset:0;z-index:95;pointer-events:none}
#tr .half{position:absolute;top:0;bottom:0;width:50vw;overflow:hidden;
  background:var(--tr-bg,var(--ink));transform:translateY(100%);
  transition:transform .44s cubic-bezier(.76,0,.24,1)}
#tr .l{left:0}
#tr .r{left:50vw}
#tr .half .inner{position:absolute;top:0;left:0;width:100vw;height:100%;
  display:grid;place-items:center}
#tr .r .inner{left:-50vw;border-left:1px solid rgba(244,243,238,.05)}
#tr.cover .half{transform:translateY(0)}
#tr.out .half.l{transform:translateX(-100.5%)}
#tr.out .half.r{transform:translateX(100.5%)}
#tr .lb{display:flex;align-items:center;gap:.7em;
  font-family:var(--mono);font-size:clamp(13px,2.2vw,24px);
  letter-spacing:.34em;text-transform:uppercase;color:var(--bone);
  opacity:0;transition:opacity .2s ease}
#tr .lb b{color:var(--violet);font-weight:400}
#tr.cover .lb,#tr.out .lb{opacity:1}
html.nav-in #tr .half{transform:translateY(0);transition:none}
html.nav-in #tr .lb{opacity:1}
html.nav-in #pre{display:none}
html.nav-in.tr-ready #tr .half{transition:transform .5s cubic-bezier(.76,0,.24,1)}

/* ============================================================
   FOOT
   ============================================================ */
.foot{padding:0 var(--pad) calc(5vh + var(--sab))}
.foot .bar{padding-top:22px;border-top:1px solid var(--hair);
  display:flex;justify-content:space-between;gap:18px;flex-wrap:wrap;
  font-family:var(--mono);font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--graphite)}
@media (hover:hover){.foot .bar a:hover{color:var(--violet)}}
@media (pointer:coarse){.foot .bar a{display:inline-flex;align-items:center;min-height:44px}}

/* while the world is being dragged, nothing on the page may be text-selected */
body.nosel{user-select:none;-webkit-user-select:none}
body.nosel *{user-select:none;-webkit-user-select:none}
