/* ============================================================
   getnessi.ai — intro → landing → ceremony → handoff

   Tokens are the Nessi intro design system (see the design
   handoff's styleguide.md), not the Bubl marketing palette.

   THE TWO-ACCENT RULE. Cyan is Nessi. Coral is the user. The
   headline gradient runs cyan → coral because that is Nessi
   reaching toward the person, and the Get Nessi button picks up
   where that gradient ended so it reads as the completion of the
   headline rather than a second competing sweep. Never introduce
   a third accent.

   Two documented exceptions, both for the same reason — they
   depict real software, so they carry real software's colours:
   the storm's app tiles, and the iMessage thread on the landing.
   ============================================================ */

:root{
  /* Base */
  --ink:#07090C;
  --panel:#0C1116;
  --card:rgba(22,28,36,.94);
  --surface:#171F27;
  --line:#1C242E;
  --line-2:#161D25;
  --line-3:#1E2731;
  --line-4:#232C36;
  --line-mono:#2A3441;

  /* Text */
  --cream:#F2EDE4;
  --body:#DCD7CE;
  --body-2:#B7BFC9;
  --body-3:#E7E2D9;
  --lede:#A8B1BC;
  --muted:#8A93A0;
  --faint:#6E7885;
  --ghost:#4E5865;

  /* Accent */
  --cyan:#34E7DC;
  --cyan-deep:#1FC3CE;
  --cyan-light:#C9FFFB;
  --coral:#FF8F6B;
  --coral-mid:#F2673A;
  --coral-deep:#E0501F;
  --green:#4ADE80;
  --on-cyan:#05201D;
  --on-coral:#2B0C02;

  /* The dock's own surfaces. Deliberately NOT --panel/--line-3: the dock has to
     read as a solid object resting on the page, so it sits a shade darker than
     the panels behind it and its border a shade lighter than the page's rules.
     Reusing the general tokens here is what made it look like glass. */
  --dock:#0B1015;
  --dock-line:#2B3744;
  --field:#0F151B;
  --field-line:#33404E;
  --field-focus:#131A21;

  --grad-head:linear-gradient(100deg,#34E7DC,#FF8F6B);
  --grad-orb:linear-gradient(150deg,#C9FFFB,#34E7DC 52%,#FF8F6B);
  --grad-cta:linear-gradient(120deg,#FF8F6B,#F2673A 62%,#E0501F);
  --grad-go:linear-gradient(140deg,#34E7DC,#1FC3CE);
  --conic:conic-gradient(from 0deg,rgba(52,231,220,0),#34E7DC,rgba(255,143,107,.9),rgba(52,231,220,0));

  --font-sans:'Schibsted Grotesk',system-ui,-apple-system,sans-serif;
  --font-mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;

  /* Entrances overshoot. Exits don't. Things being consumed accelerate away. */
  --enter:cubic-bezier(.16,1,.3,1);
  --settle:cubic-bezier(.3,1.12,.45,1);
  --spring:cubic-bezier(.2,1.15,.3,1);
  --absorb:cubic-bezier(.6,0,.35,1);
  --pop:cubic-bezier(.2,1.2,.3,1);
  /* More overshoot than --spring. The dock is the one thing that should look
     like it has mass when it arrives. */
  --land:cubic-bezier(.2,1.25,.3,1);

  /* Mobile portrait is the design target; desktop is the same column on a
     wider field. */
  --col:430px;
  /* Raised by JS when a mobile keyboard opens. */
  --kb:0px;
}

*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--ink);-webkit-text-size-adjust:100%;overscroll-behavior-y:none}
button,a,input{-webkit-tap-highlight-color:transparent;touch-action:manipulation}
input{font-family:inherit}
a{color:var(--cyan);text-decoration:none}
a:hover{color:var(--coral)}

body{
  font-family:var(--font-sans);
  color:var(--cream);
  -webkit-font-smoothing:antialiased;
}

.stage{display:flex;justify-content:center;min-height:100dvh}

/* The 430px column. Light falls from above, so the page is a radial wash
   rather than a flat fill. Do NOT put transform or filter on .frame — the
   dock is position:fixed and would get trapped inside it. */
.frame{
  position:relative;
  width:100%;max-width:var(--col);min-height:100dvh;
  overflow:hidden;
  background:radial-gradient(120% 70% at 50% -10%,#0D1319 0%,#07090C 62%);
}

.view{
  position:absolute;inset:0;
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .45s var(--enter),visibility 0s linear .45s;
}
.view.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .45s var(--enter)}

.grad{
  background:var(--grad-head);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}

:where(a,button,input):focus-visible{
  outline:2px solid var(--cyan);
  outline-offset:3px;
  border-radius:6px;
}

/* ── keyframes ─────────────────────────────────────────── */

@keyframes nsBlink{0%,45%{opacity:.9}55%,100%{opacity:.12}}
@keyframes nsCardIn{
  from{opacity:0;transform:translate(var(--fx),var(--fy)) scale(.86) rotate(0deg)}
  to{opacity:1;transform:translate(0,0) scale(1) rotate(var(--r))}
}
@keyframes nsFall{to{transform:translate(var(--px),var(--py)) rotate(var(--pr)) scale(.93)}}
@keyframes nsConverge{
  0%{opacity:1;transform:translate(0,0) scale(1);filter:blur(0)}
  55%{opacity:.9}
  100%{opacity:0;transform:translate(var(--cx),var(--cy)) scale(.04);filter:blur(2px)}
}
@keyframes nsStormDim{to{opacity:.55;filter:blur(2px) saturate(.7)}}
@keyframes nsDrift{0%,100%{transform:translate(0,0)}50%{transform:translate(-6px,-10px)}}

/* One keyframe handles a story beat's arrival AND its departure. Delay is
   when it starts, duration is how long it lives. Text enters from below and
   leaves upward, so the words move up past the reader's eye. */
@keyframes nsBeat{
  0%{opacity:0;transform:translateY(16px)}
  14%{opacity:1;transform:translateY(0)}
  84%{opacity:1;transform:translateY(0)}
  100%{opacity:0;transform:translateY(-9px)}
}
@keyframes nsUp{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}
@keyframes nsFade{from{opacity:0}to{opacity:1}}
@keyframes nsOut{to{opacity:0}}
@keyframes nsOrbIn{
  0%{opacity:0;transform:scale(.1)}
  62%{opacity:1;transform:scale(1.16)}
  100%{opacity:1;transform:scale(1)}
}
@keyframes nsBreathe{0%,100%{transform:scale(1)}50%{transform:scale(1.055)}}
@keyframes nsSpin{to{transform:rotate(360deg)}}
@keyframes nsRing{0%{opacity:.5;transform:scale(.8)}100%{opacity:0;transform:scale(1.9)}}
@keyframes nsDot{
  0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(74,222,128,.45)}
  50%{opacity:.75;box-shadow:0 0 0 9px rgba(74,222,128,0)}
}
@keyframes nsLineIn{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}
@keyframes nsGlow{
  0%,100%{box-shadow:0 0 0 1px rgba(255,143,107,.5),0 18px 50px rgba(242,103,58,.24)}
  50%{box-shadow:0 0 0 1px rgba(255,143,107,.75),0 22px 62px rgba(242,103,58,.42)}
}
/* The CTA's displaced position under the headline. --cdx/--cdy are measured at
   runtime by app.js against the button's final rect. */
@keyframes nsCtaTravel{
  0%{transform:translate(var(--cdx,0px),var(--cdy,0px))}
  100%{transform:translate(0,0)}
}
@keyframes nsCollapse{0%{height:44px}100%{height:0}}

/* The dock comes up from below and widens around the button that just landed
   in it, overshooting slightly. scaleX is on the chrome layer only — the pill's
   contents must not stretch, and .pill-chrome is a sibling of nothing, so this
   is safe. It is also BELOW .dock in the tree, never between .dock and .cta,
   so it cannot corrupt the travel measurement. */
@keyframes nsDockLand{
  0%{opacity:0;transform:translateY(26px) scaleX(.86)}
  100%{opacity:1;transform:translateY(0) scaleX(1)}
}
/* Wipes open leftward — see transform-origin on .field for why that direction
   is the whole point. */
@keyframes nsFieldIn{
  0%{opacity:0;transform:scaleX(.2)}
  100%{opacity:1;transform:scaleX(1)}
}
/* A coral ring pushed off the field's border. Runs a COUNTED three times and
   stops; see .field for why this must never be `infinite`. */
@keyframes nsFieldPulse{
  0%{box-shadow:0 0 0 0 rgba(255,143,107,.5)}
  38%{box-shadow:0 0 0 9px rgba(255,143,107,0)}
  100%{box-shadow:0 0 0 0 rgba(255,143,107,0)}
}

/* ══════════════ INTRO ══════════════ */

#vIntro.on{animation:nsOut .85s ease 20.05s both}

.idot{
  position:absolute;left:50%;top:50%;
  width:7px;height:7px;margin:-3px 0 0 -3px;
  border-radius:50%;background:var(--muted);
}
#vIntro.on .idot{animation:nsBlink 1s steps(1,end) 0s 3,nsOut .3s ease .8s both}

/* ── the storm ──────────────────────────────────────────
   Cards are injected by app.js. Each needs THREE nested wrappers because it
   performs three separate transforms that must compose: fly in, fall to the
   pile, get absorbed into the orb. Collapsing them into one element makes the
   later animations fight the earlier ones. */
.storm{position:absolute;inset:0}
#vIntro.on .storm{
  animation:nsStormDim 1.1s ease 4.3s both,nsDrift 16s ease-in-out 5.2s infinite;
}
.sc{position:absolute;left:var(--l);top:var(--t)}
#vIntro.on .sc{animation:nsCardIn .5s var(--enter) var(--d1) both}
#vIntro.on .sc-f{animation:nsFall .9s var(--settle) var(--d2) both}
#vIntro.on .sc-c{animation:nsConverge .9s var(--absorb) var(--d3) both}

.card{
  width:250px;padding:12px 13px;
  border-radius:16px;
  background:var(--card);
  border:1px solid var(--line);
  box-shadow:0 18px 40px rgba(0,0,0,.5);
  display:flex;gap:11px;align-items:flex-start;
}
.card-t{min-width:0}
.card-l{
  font-family:var(--font-mono);font-size:10px;
  letter-spacing:.1em;text-transform:uppercase;color:var(--faint);
}
/* The pile needs exactly one point of alarm. */
.card-l.od{color:var(--coral)}
.card-m{margin-top:4px;font-size:14px;line-height:1.35;color:var(--body)}

/* ── app tiles ──────────────────────────────────────────
   The one place real-world colour enters the system, and only here: these are
   the problem, not the brand. Original designs in each app's signature colour,
   not the real logos — swap in licensed assets if you get rights, but keep the
   34px / radius-9px / 0 3px 8px envelope. */
.tile{
  flex-shrink:0;width:34px;height:34px;border-radius:9px;
  display:flex;align-items:center;justify-content:center;
}
.tile svg{width:19px;height:19px}
.t-mail{background:linear-gradient(160deg,#4DA3FF,#1471E8);box-shadow:0 3px 8px rgba(20,113,232,.4)}
.t-msg{background:linear-gradient(160deg,#5BE370,#21B93F);box-shadow:0 3px 8px rgba(33,185,63,.4)}
.t-team{
  background:#4A154B;box-shadow:0 3px 8px rgba(74,21,75,.45);
  font-family:var(--font-mono);font-size:18px;font-weight:500;color:#fff;line-height:1;
}
.t-cal{
  background:#FFF;overflow:hidden;box-shadow:0 3px 8px rgba(0,0,0,.35);
  display:block;
}
.t-cal .cb{display:block;height:11px;background:#FF3B30}
.t-cal .cd{
  display:flex;height:23px;align-items:center;justify-content:center;
  font-size:13px;font-weight:600;color:#16150F;font-style:normal;
}
.t-rem{
  background:#FFF;box-shadow:0 3px 8px rgba(0,0,0,.35);
  flex-direction:column;justify-content:center;gap:4px;padding:0 7px;
}
.t-rem i{display:flex;align-items:center;gap:4px;width:100%}
.t-rem i::before{content:"";width:5px;height:5px;border-radius:50%;flex:none}
.t-rem i::after{content:"";flex:1;height:2px;border-radius:2px;background:#D6D0C6}
.t-rem .r1::before{background:#FF3B30}
.t-rem .r2::before{background:#FF9500}
.t-rem .r3::before{background:#007AFF}
.t-rem.od i::before{background:#FF3B30}
.t-notes{
  background:#FFF;overflow:hidden;box-shadow:0 3px 8px rgba(0,0,0,.35);
  display:block;
}
.t-notes .nb{display:block;height:10px;background:#FFD60A}
.t-notes .nr{
  display:block;height:2px;margin:3px 7px 0;border-radius:2px;background:#D6D0C6;
}
.t-notes .nb + .nr{margin-top:6px}
.t-notes .nr.short{width:60%}

/* ── story copy ─────────────────────────────────────────
   Problem and empathy, layered so only one is ever on screen. */
.beats{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);padding:0 34px;text-align:center}
.beat{margin:0;text-wrap:pretty}
.b1,.b2{
  font-size:clamp(22px,6.4vw,27px);line-height:1.28;
  font-weight:500;letter-spacing:-.015em;
}
.b2{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);padding:0 34px}
.b2 span{color:var(--muted)}
/* THE BEAT DURATIONS ARE LONGER THAN THE HANDOFF SPECIFIES. Deliberate — do
   not "correct" them back.

   nsBeat only holds text at full opacity for 70% of its duration (14% to 84%),
   so the handoff's 2.5s problem beat gave a reader 1.75s for 17 words: about
   10 words/second. The empathy beat was 12/s and the closing answer 16/s.
   Comfortable reading of short centred display type is 4-6 words/second, so
   none of the four lines could actually be read.

   Durations now put every beat in the 5-7.5 w/s band. Delays after each beat
   shifted by the same amount so nothing overlaps — the storm's converge, the
   orb's birth, the overlay fade and the stage timer in app.js all moved with
   them. If you change a duration here you MUST reflow everything downstream;
   the whole chain is listed in site/README.md.

   Cost: the intro is 21.0s rather than 14.9s. Skip still appears at 3s. */
#vIntro.on .b1{animation:nsBeat 4s ease 4.8s both}
#vIntro.on .b2{animation:nsBeat 3.6s ease 8.85s both}

/* ── orb + answer ───────────────────────────────────────
   ONE centred flex column. Their spacing must not depend on viewport height:
   anchoring the orb and the copy independently by percentage printed the copy
   across the orb on anything shorter than ~700px. */
.answer{
  position:absolute;inset:0;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:38px;padding:0 34px;pointer-events:none;
}

/* Nessi's only visual form. Six stacked layers. Never flatten it to a plain
   circle, never put it on a light field. */
.orb{position:relative;flex-shrink:0;width:136px;height:136px}
#vIntro.on .orb{animation:nsOrbIn 1.1s var(--pop) 13.05s both}
.orb-breathe{position:absolute;inset:0}
#vIntro.on .orb-breathe{animation:nsBreathe 4.2s ease-in-out 14.2s infinite}
.orb i{position:absolute;border-radius:50%;font-style:normal}
.o-glow{inset:-34px;background:radial-gradient(circle,rgba(52,231,220,.26) 0%,rgba(52,231,220,0) 68%)}
.o-ring{inset:0;background:var(--conic);opacity:.5}
.o-core{inset:5px;background:radial-gradient(circle at 38% 32%,#0F1A20 0%,#07090C 74%);border:1px solid rgba(52,231,220,.28)}
.o-inner{
  inset:26px;filter:blur(7px);
  background:radial-gradient(circle at 40% 36%,rgba(156,253,246,.55) 0%,rgba(52,231,220,.2) 52%,rgba(52,231,220,0) 80%);
}
.o-n{
  inset:0;display:flex;align-items:center;justify-content:center;
  font-size:54px;font-weight:700;letter-spacing:-.05em;
  background:var(--grad-orb);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}
.o-pulse{inset:0;border:1px solid rgba(52,231,220,.5)}

/* Ambient loops are gated on the screen being up. Nothing on screen is ever
   fully static, but a hidden screen is not on screen — an infinite conic spin
   behind visibility:hidden is pure compositor work for nobody. */
#vIntro.on .o-ring{animation:nsSpin 7s linear infinite}
#vIntro.on .o-pulse{animation:nsRing 3.4s ease-out 14.4s infinite}

.answers{position:relative;width:100%;max-width:340px;text-align:center}
.a1{font-size:clamp(21px,6.2vw,26px);line-height:1.3;font-weight:500;letter-spacing:-.015em}
.a2{
  position:absolute;left:0;right:0;top:0;
  font-size:clamp(17px,5.1vw,21px);line-height:1.4;font-weight:400;color:var(--body-2);
}
.a2 span{color:var(--cream)}
#vIntro.on .a1{animation:nsBeat 2.8s ease 13.6s both}
#vIntro.on .a2{animation:nsBeat 4.2s ease 16.4s both}

/* ── skip ──────────────────────────────────────────────
   Shared by the intro and the ceremony. */
.skip{
  position:absolute;z-index:12;
  right:16px;top:calc(18px + env(safe-area-inset-top));
  padding:10px 16px;border-radius:999px;
  border:1px solid var(--line-4);
  background:rgba(7,9,12,.7);
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  color:var(--muted);
  font-family:var(--font-mono);font-size:11px;
  letter-spacing:.08em;text-transform:uppercase;
  cursor:pointer;
}
.skip:hover{color:var(--cream);border-color:var(--cyan)}
#vIntro.on .skip{animation:nsFade .6s ease 3s both}

/* ══════════════ LANDING ══════════════ */

.land{
  position:absolute;inset:0;
  overflow-y:auto;-webkit-overflow-scrolling:touch;
  padding:
    calc(26px + env(safe-area-inset-top)) 24px
    calc(150px + env(safe-area-inset-bottom));
}
.land::-webkit-scrollbar{width:0}
.col{width:100%}

/* The entrance is staged so the headline gets its own moment, the CTA appears
   as display copy beneath it, and the name field is the very last thing to
   arrive — an empty box right after a phrase that just said what you're doing,
   so no instruction copy is needed. Ends at ~4.45s. */
#vLand.on .lockup{animation:nsUp .7s var(--enter) .15s both}
#vLand.on .h1{animation:nsUp .9s var(--enter) .35s both}
#vLand.on .lede{animation:nsUp .8s var(--enter) 1.5s both}
#vLand.on .thread{animation:nsUp .85s var(--enter) 1.85s both}
#vLand.on .closer{animation:nsUp .8s var(--enter) 2.25s both}
#vLand.on .trust{animation:nsUp .8s var(--enter) 2.45s both}
/* Slotted in BEFORE the travel starts at 2.95s, not after the dock settles.
   Nothing may move on this page after the field, or the field stops being the
   last thing the eye saw — which is the entire instruction mechanism. It is
   below the fold anyway, so nobody watches it arrive. */
#vLand.on .legal{animation:nsFade .6s ease 2.6s both}

/* ── the arrival ───────────────────────────────────────
   The travel, the pill landing and the field opening overlap into one ~0.7s
   event. They used to be a 0.9s run of separate gentle fades, and the dock
   simply resolved out of nothing rather than arriving.

   The big label now holds almost the whole way down and cuts late, so there is
   never a stretch where both labels are half-visible. Nudge these apart again
   and the softness comes straight back. */
#vLand.on .cta{animation:nsFade .6s ease .95s both,nsCtaTravel .72s cubic-bezier(.5,0,.22,1) 2.95s both}
#vLand.on .dock::before{animation:nsFade .5s ease 3.3s both}
#vLand.on .cta-big{animation:nsOut .2s ease 3.36s both}
#vLand.on .pill-chrome{animation:nsDockLand .52s var(--land) 3.46s both}
#vLand.on .cta-grad{animation:nsFade .18s ease 3.5s both}
#vLand.on .cta-small{animation:nsFade .16s ease 3.52s both}
#vLand.on .field{animation:nsFieldIn .42s var(--pop) 3.62s both,
                 nsFieldPulse 2.1s ease-out 4.2s 3}
/* Stops the ring the instant the visitor engages, and it MUST live here rather
   than in the .field:focus rule below. `.field:focus` is (0,2,0) and this
   timeline rule is (1,2,0), so a focus-side `animation` loses the cascade and
   the pulse just keeps going — the colours still change, which makes it look
   like it worked. Measured, not assumed.

   The nsFieldIn half is character-identical to the line above on purpose:
   animations are matched by name and position, so an identical declaration
   continues the finished animation instead of restarting it. Retune the delay
   here without retuning it above and focusing the field blanks it to scaleX(.2)
   for 3.6 seconds. */
#vLand.on .field:focus{animation:nsFieldIn .42s var(--pop) 3.62s both}
#vLand.on .cta-slot{animation:nsCollapse .55s var(--enter) 3.9s both}

.lockup{display:flex;align-items:center;gap:10px}
.mark{position:relative;width:30px;height:30px;flex:none}
.mark i{position:absolute;border-radius:50%;font-style:normal}
.m-ring{inset:0;background:var(--conic);opacity:.55}
.m-core{inset:2px;background:var(--ink)}
.m-glow{
  inset:6px;
  background:radial-gradient(circle at 40% 36%,rgba(156,253,246,.4),rgba(52,231,220,0) 72%);
}
#vLand.on .m-ring{animation:nsSpin 7s linear infinite}
#vLand.on .m-glow{animation:nsBreathe 4.2s ease-in-out infinite}
.m-n{
  inset:0;display:flex;align-items:center;justify-content:center;
  font-size:15px;font-weight:700;letter-spacing:-.04em;color:var(--cyan);
}
.lk-word{font-size:17px;font-weight:600;letter-spacing:-.01em}
.lk-tag{
  margin-left:auto;
  font-family:var(--font-mono);font-size:11px;
  letter-spacing:.1em;text-transform:uppercase;color:var(--faint);
}

.h1{
  margin:46px 0 0;
  font-size:clamp(42px,13.5vw,56px);
  line-height:.98;font-weight:700;letter-spacing:-.035em;
}

.lede{
  margin:20px 0 0;
  font-size:18px;line-height:1.5;color:var(--lede);
  max-width:21em;text-wrap:pretty;
}

/* ── the proof screenshot ──────────────────────────────────
   Reads as an actual iOS Messages screen in dark mode, not a chat widget:
   conversation header, timestamp divider, read receipt, rich-link footer,
   composer. Everything inside uses the system face so it renders as real SF
   on Apple devices. iMessage blue is not a brand colour — it's here for the
   same reason the storm's app tiles carry their apps' colours.
   ──────────────────────────────────────────────────────── */
.thread{
  --tail-mask:#0A0E13;
  margin:34px 0 0;
  font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',Roboto,sans-serif;
  background:#0A0E13;
  border:1px solid #1A222B;
  border-radius:22px;
  overflow:hidden;
  box-shadow:0 18px 44px -22px rgba(0,0,0,.8);
}

/* conversation header */
.ios-bar{
  display:grid;grid-template-columns:24px 1fr 30px;align-items:center;
  padding:9px 13px 8px;
  background:rgba(20,25,31,.92);
  backdrop-filter:saturate(180%) blur(14px);
  -webkit-backdrop-filter:saturate(180%) blur(14px);
  border-bottom:.5px solid rgba(255,255,255,.1);
}
.ios-back{width:11px;height:18px;color:#3E8DF7}
.ios-video{width:25px;height:16px;color:#3E8DF7;justify-self:end}
.ios-who{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:0}
.av{
  width:42px;height:42px;border-radius:50%;
  background:var(--grad-go);color:var(--on-cyan);
  font-size:18px;font-weight:600;
  display:flex;align-items:center;justify-content:center;flex:none;
  letter-spacing:.01em;
}
.tname{
  display:flex;align-items:center;gap:3px;
  font-size:12px;font-weight:500;color:var(--cream);letter-spacing:-.01em;
}
.tname .chev{width:6px;height:10px;color:#6E6E73;margin-top:.5px}

/* message stack */
.msgs{display:flex;flex-direction:column;gap:2px;padding:10px 12px 12px}
.stamp{align-self:center;font-size:11px;color:#8E8E93;margin:2px 0 10px;letter-spacing:-.01em}
.stamp b{font-weight:600;color:#8E8E93}
.receipt{align-self:flex-end;font-size:10.5px;color:#8E8E93;margin:3px 3px 0;letter-spacing:-.01em}
.receipt b{font-weight:600}
.cap{align-self:flex-start;font-size:10.5px;color:#8E8E93;margin:4px 0 0 6px;letter-spacing:-.01em}

.m{
  position:relative;max-width:76%;
  font-size:15px;line-height:1.29;letter-spacing:-.015em;
  padding:8px 13px 9px;border-radius:19px;z-index:1;
}
.m.in{background:#26272B;color:var(--cream);align-self:flex-start}
.m.out{
  background:linear-gradient(180deg,#28A0FF 0%,#0B7CFC 100%);
  color:#fff;align-self:flex-end;
}
.m.gap{margin-top:8px}

/* composer */
.ios-input{
  display:flex;align-items:center;gap:9px;
  padding:9px 12px 11px;
  border-top:.5px solid rgba(255,255,255,.08);
  background:rgba(20,25,31,.92);
}
.plus{
  width:29px;height:29px;border-radius:50%;flex:none;
  background:#2C2C2E;color:#8E8E93;
  display:flex;align-items:center;justify-content:center;
}
.plus svg{width:15px;height:15px}
.ifield{
  flex:1;min-width:0;display:flex;align-items:center;justify-content:space-between;
  height:29px;padding:0 9px 0 12px;
  border:.5px solid #3A3A3C;border-radius:15px;
  font-size:13.5px;color:#8E8E93;letter-spacing:-.01em;
}
.ifield .mic{width:12px;height:17px;color:#8E8E93;flex:none}

/* iMessage tails.
   The ::after mask is painted with the message area's own background. If the
   stack ever moves onto a different surface, --tail-mask must move with it or
   you get bright notches under every tail.
   The outgoing tail matches the BOTTOM stop of the bubble gradient — it sits
   at the bubble's baseline, so anything lighter reads as a seam. */
.m.tail::before{content:"";position:absolute;bottom:-.5px;width:18px;height:18px;z-index:-1}
.m.tail::after{content:"";position:absolute;bottom:-.5px;width:11px;height:18px;background:var(--tail-mask);z-index:-1}
.m.in.tail::before{left:-6px;background:#26272B;border-bottom-right-radius:16px}
.m.in.tail::after{left:-11px;border-bottom-right-radius:11px}
.m.out.tail::before{right:-6px;background:#0B7CFC;border-bottom-left-radius:16px}
.m.out.tail::after{right:-11px;border-bottom-left-radius:11px}

/* Maps rich link preview */
.m.link{padding:0;overflow:hidden;max-width:62%;border-radius:18px}
.m.link .map{display:block;width:100%;height:104px}
.m.link .meta{padding:7px 11px 9px;background:#26272B}
.m.link .mt{font-size:12.5px;font-weight:600;color:var(--cream);line-height:1.25;letter-spacing:-.01em}
.m.link .md{font-size:11px;color:#8E8E93;margin-top:1px;letter-spacing:-.01em}
.m.link.tail::before{border-bottom-right-radius:16px}

.closer{
  margin:30px 0 0;
  font-size:22px;line-height:1.3;font-weight:500;letter-spacing:-.02em;
}
.closer span{color:var(--muted)}

/* Factual reassurance. The last thing above the dock is the price: nothing. */
.trust{
  margin:30px 0 0;padding-top:22px;
  border-top:1px solid var(--line-2);
  display:grid;grid-template-columns:repeat(3,1fr);gap:14px;
}
.trust-item{display:flex;flex-direction:column;gap:5px}
.trust-val{font-size:24px;font-weight:600;letter-spacing:-.02em}
.trust-lbl{font-size:11.5px;line-height:1.3;color:var(--faint)}

/* The privacy link. Set in --ghost rather than --faint so it reads as the
   footnote it is and does not compete with the trust row directly above
   it, but still clears contrast on hover and on focus. */
.legal{margin:26px 0 0;padding-top:18px;border-top:1px solid var(--line-2)}
.legal a{
  font-size:11.5px;letter-spacing:.01em;
  color:var(--ghost);text-decoration:none;
  transition:color .16s ease;
}
.legal a:hover{color:var(--muted)}
.legal a:focus-visible{outline:2px solid var(--cyan);outline-offset:3px;border-radius:3px}

/* ── the dock ──────────────────────────────────────────
   NO TRANSFORM may appear between .dock and .cta. The travel offsets are
   measured against the button's final on-screen rect, and any ancestor
   transform corrupts that measurement. That is why the keyboard lift moves
   `bottom` rather than translateY here — a transform on this element would
   put the displaced CTA in the wrong place — and why .dock-in carries only
   position and z-index, never a transform.

   .pill-chrome's nsDockLand scaleX is fine: it is a SIBLING of the button, not
   an ancestor, so it never enters the measured chain. */
.dock{
  position:fixed;left:0;right:0;z-index:20;
  bottom:var(--kb);
  display:flex;justify-content:center;
  /* The 64px top padding is not spacing — it is the runway the scrim needs to
     ramp from nothing to solid. Take it away and the gradient becomes a hard
     line across the conversation card. */
  padding:64px 0 calc(18px + env(safe-area-inset-bottom));
  pointer-events:none;
  transition:bottom .2s var(--enter);
}
/* The scrim: the cheapest move in the whole arrival. The dock does not get
   brighter, everything behind it gets quieter, and contrast does the pointing.
   Safe over the content because .dock is pointer-events:none — it dims the
   conversation card without ever intercepting a tap meant for it.

   ON A PSEUDO-ELEMENT, NOT ON .dock. The handoff puts this background and its
   3.3s fade on the dock's own wrapper, and that wrapper contains the button.
   `both` fills backwards, so an opacity animation there pins the WHOLE dock —
   button included — at opacity 0 until 3.3s, and the CTA's display-copy phase
   under the headline (from .95s) plus most of its travel simply would not be
   visible. Only the scrim may fade; the button has its own schedule. */
.dock::before{
  content:'';
  position:absolute;inset:0;
  background:linear-gradient(to top,
    var(--ink) 44%,
    rgba(7,9,12,.92) 74%,
    rgba(7,9,12,0));
}
/* position/z-index paint the pill above the scrim without a negative z-index,
   which would drop it behind .dock's own stacking context. Neither is a
   transform, so the travel measurement is unaffected — see the note above. */
.dock-in{position:relative;z-index:1;width:100%;max-width:var(--col);padding:0 16px}
.pill{
  position:relative;
  display:flex;gap:8px;align-items:center;
  margin:0;padding:10px;border-radius:999px;
  background:transparent;
  pointer-events:auto;
}
/* The chrome is its own layer so the pill can be invisible while the button is
   displaced yet still lay out, keeping the button right-anchored.

   OPAQUE, on purpose. It was rgba(12,17,22,.92) and you could read the
   conversation card through it, which made the dock look like glass floating
   over the page instead of a bar resting on it. The arrival has to land on
   something solid.

   Still deliberately no backdrop-filter: it smeared a band of the content above
   the pill's rect, and against an opaque fill it would buy nothing anyway. */
.pill-chrome{
  position:absolute;inset:0;border-radius:999px;
  background:var(--dock);
  border:1px solid var(--dock-line);
  box-shadow:0 26px 64px rgba(0,0,0,.85);
}
/* 16px is a floor, not a preference: anything smaller makes iOS zoom the page
   on focus and the visitor loses the dock.

   This used to be borderless and transparent — a placeholder floating in the
   pill with nothing saying "this is a field, type here". It now draws itself.

   transform-origin is the load-bearing line. The field wipes open LEFTWARD,
   away from the button, because the eye is already at the button when the
   travel ends and the wipe pulls it left into the empty box. Grow it from the
   left instead and the motion pushes attention off the edge of the screen. */
.field{
  position:relative;
  flex:1;min-width:0;
  outline:none;
  border:1px solid var(--field-line);border-radius:999px;
  background:var(--field);
  color:var(--cream);font-size:16px;
  min-height:46px;padding:12px 18px;
  transform-origin:right center;
}
.field::placeholder{color:var(--faint)}
/* The 18px inner padding is 14px more than the placeholder can spare at a
   320px viewport — measured, and it clips to "Your first nam". Every live phone
   width from 344px up has room, so this only reaches back for the oldest small
   screens and split-screen windows. The vertical padding and the 16px font are
   deliberately untouched: the first is the field's height, the second is the
   iOS zoom floor. */
@media (max-width:340px){
  .field{padding-left:10px;padding-right:10px}
}
/* Re-declaring `animation` with only nsFieldIn is what drops nsFieldPulse, so
   the ring stops the instant the visitor engages.

   The nsFieldIn half must stay CHARACTER-IDENTICAL to the base rule. Animations
   are matched by name and position in the list: keep it identical and the
   browser treats it as the same already-finished animation and holds the final
   state. Change the delay or the easing here by even a little and it counts as
   a new animation, restarts, and — with a 3.62s delay and `both` filling
   backwards — blanks the field to scaleX(.2) for three and a half seconds the
   moment someone taps it. */
.field:focus{
  border-color:var(--coral);
  background:var(--field-focus);
}

/* One element in two states. The button is transparent; the orange pill is a
   child layer that fades in as it arrives, so the same node can read as display
   copy at the top of the page and as a button in the dock. */
.cta{
  position:relative;
  flex-shrink:0;min-height:46px;padding:13px 20px;
  border:0;border-radius:999px;
  background:transparent;color:var(--on-coral);
  font-family:inherit;font-size:15px;font-weight:600;
  cursor:pointer;white-space:nowrap;
}
.cta-grad{
  position:absolute;inset:0;border-radius:999px;
  background:var(--grad-cta);
  box-shadow:0 10px 26px rgba(242,103,58,.42);
}
/* Absolutely positioned so it contributes no layout width to the button. The
   20px left offset matches the button's left padding and the measurement in
   app.js depends on that number — change one, change both. */
.cta-big{
  position:absolute;left:20px;top:50%;transform:translateY(-50%);
  white-space:nowrap;
  font-size:clamp(30px,9vw,38px);font-weight:700;letter-spacing:-.03em;
  color:var(--coral);
}
.cta-small{position:relative}
.cta:hover .cta-grad{filter:brightness(1.08)}
.cta:active{transform:scale(.975)}

/* Reserves the big label's space under the headline, then collapses. The lede
   keeps its own 20px margin: 44 + 20 clears the label, and the resting gap
   returns to 20px. Zero the lede's margin instead and it rides up into the
   headline's descenders once this collapses. */
.cta-slot{height:44px}

/* ══════════════ CEREMONY ══════════════
   A terminal, not a slideshow. One flat column that fills from the top, then
   pins each new line to the bottom and pushes everything above it up. Lines
   persist and scroll away rather than flashing in place — so "I'm awake." and
   "Hi {name}." get displaced by the learning lines, and that displacement is
   the point.

   app.js owns the geometry: it sizes .stream-view at runtime and drives
   .stream's transform from measured row positions. Do not reintroduce fixed-px
   keyframes here — they had to be rebuilt every time the window height changed,
   and they silently did nothing on tall phones. */
.cer{
  position:absolute;inset:0;
  display:flex;flex-direction:column;justify-content:center;
  padding:
    calc(48px + env(safe-area-inset-top)) 26px
    calc(40px + env(safe-area-inset-bottom));
  font-family:var(--font-mono);
}

/* The mask ramp must be in px. A percentage ramp scales with the panel height,
   and since the column starts at translateY(0) and only ever moves up, the
   first log line then spends its whole life inside the fade at ~20% opacity.
   It was never legible. */
.stream-view{
  flex:0 1 auto;
  height:420px;            /* placeholder; app.js overwrites on mount */
  max-height:100%;
  overflow:hidden;
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 26px);
  mask-image:linear-gradient(to bottom,transparent 0,#000 26px);
}
/* Pairs with the mask so row 0 starts below the ramp. The scroll math reads
   offsetTop, so this padding is absorbed automatically. */
.stream{
  padding-top:34px;
  transition:transform .55s var(--enter);
}

/* Row spacing is padding, NOT gap: the scroll math measures
   offsetTop + offsetHeight, and offsetHeight includes padding but not margin
   or flex gap. The 18px also keeps the stream tall enough to overflow the
   tallest window we render — see app.js. */
.row{padding-bottom:18px;animation:nsLineIn .4s ease both}

/* label ······················· value */
.lg{display:flex;align-items:baseline;gap:8px;font-size:13px;color:var(--lede)}
.lg i{flex:1;border-bottom:1px dotted var(--line-mono)}
.lg .ok{color:var(--cyan);font-weight:400}
.lg .val{color:var(--cream);font-weight:400}
.lg .empty{color:var(--faint);font-weight:400}
.lg.faint{color:var(--faint)}
.lg.faint .val{color:var(--lede)}
.row.owner{padding-top:24px}

/* The switch from mono to Schibsted is the emotional pivot of the scene.
   Mono means the machine is speaking; this is Nessi speaking. Don't flatten it. */
.awake{
  padding:20px 0 18px;
  font-family:var(--font-sans);font-size:26px;font-weight:500;letter-spacing:-.02em;
  background:var(--grad-head);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}
.hi{
  font-family:var(--font-sans);font-size:36px;font-weight:700;letter-spacing:-.03em;
  color:var(--cream);
}

.lrn{font-size:12.5px;color:var(--faint)}
.lrn.first{padding-top:22px}
.lrn.wink{color:var(--coral)}

/* ══════════════ HANDOFF ══════════════
   One tap. Everything else on this screen exists to not compete with it. */
.fin{
  position:absolute;inset:0;overflow-y:auto;
  display:flex;flex-direction:column;
  padding:
    calc(90px + env(safe-area-inset-top)) 26px
    calc(44px + env(safe-area-inset-bottom));
}
.fin::-webkit-scrollbar{width:0}

.live{display:flex;align-items:center;gap:9px}
.dot{width:8px;height:8px;border-radius:50%;background:var(--green)}
#vFin.on .dot{animation:nsDot 2s ease-in-out infinite}
.live-lbl{
  font-family:var(--font-mono);font-size:11px;
  letter-spacing:.12em;text-transform:uppercase;color:var(--faint);
}

/* Deliberately de-emphasised — no gradient, muted colour — so the button owns
   the screen. Do not "fix" this by making it brighter. */
.finh{
  margin:20px 0 0;
  font-size:clamp(28px,8.6vw,36px);line-height:1.08;
  font-weight:600;letter-spacing:-.03em;color:var(--muted);
}
.finh span{color:var(--cream)}

.spacer{flex:1;min-height:34px}

/* Coral, like Get Nessi — both buttons are the user acting. This ramp is
   DARKER than the dock's because it carries light text and the label sits over
   the gradient's lightest stop: off-white on the dock's #FF8F6B measures ~2.9:1,
   under the 3:1 floor for large text. This ramp measures 3.85:1 there. The dock
   button keeps dark ink on the lighter ramp, which is correct at 15px; the two
   never appear on the same screen. */
.go{
  display:flex;align-items:center;justify-content:center;gap:12px;
  padding:25px 24px;border-radius:24px;
  background:linear-gradient(120deg,#D9501F,#C4441A 62%,#A83510);
  color:#FFF6F1;
  font-size:22px;font-weight:700;letter-spacing:-.015em;
  box-shadow:0 0 0 1px rgba(255,143,107,.42),0 18px 50px rgba(217,80,31,.3);
}
.go span{font-weight:600}
.go:hover{filter:brightness(1.08);color:#FFF6F1}

.fine{
  margin:18px 0 0;
  display:flex;flex-direction:column;align-items:center;gap:5px;
  text-align:center;
  font-size:12.5px;line-height:1.4;color:var(--faint);
}
.fine-row{display:flex;align-items:center;gap:8px}
/* No shadow at this size — a 0 8px 22px glow under a 19px tile reads as a smudge. */
.ptile{
  display:none;flex-shrink:0;width:19px;height:19px;border-radius:5px;
  align-items:center;justify-content:center;
}
.ptile svg{width:12px;height:12px}
.ptile-ios{background:linear-gradient(160deg,#5BE370,#21B93F)}
.ptile-android{background:#FFF}
.fin .ptile-ios{display:flex}
.fin.android .ptile-ios{display:none}
.fin.android .ptile-android{display:flex}
.num{
  font-family:var(--font-mono);font-size:11.5px;letter-spacing:.04em;
  color:var(--ghost);
  user-select:all;-webkit-user-select:all;
}

#vFin.on .live{animation:nsUp .7s var(--enter) .1s both}
#vFin.on .finh{animation:nsUp .8s var(--enter) .22s both}
#vFin.on .go{animation:nsUp .8s var(--enter) .5s both,nsGlow 2.6s ease-in-out 1.4s infinite}
#vFin.on .fine{animation:nsFade .8s ease .66s both}
#vFin.on .qrwrap.on{animation:nsFade .8s ease .8s both}

/* ── desktop QR ────────────────────────────────────────
   sms: is inert on most desktops. Rather than leave a dead button, hand them
   the same pre-filled message on their phone. */
.qrwrap{
  display:none;flex-direction:column;align-items:center;gap:10px;
  margin:26px 0 0;
}
.qrwrap.on{display:flex}
.qr{
  width:132px;height:132px;overflow:hidden;
  background:#fff;border-radius:12px;
  box-shadow:0 10px 30px rgba(0,0,0,.5);
}
.qr svg{display:block;width:100%;height:100%}
.qrcap{font-size:11.5px;color:var(--ghost);margin:0;line-height:1.45;text-align:center}

/* ── reduced motion ─────────────────────────────────────
   Durations and loops go everywhere. Delays are handled per screen, because
   the screens are not the same kind of thing.

   Landing and handoff are plain entrances — staging exists only to give the
   motion somewhere to happen. With the motion gone, keeping the delays leaves
   a reduced-motion visitor watching blank space while the copy, the proof card
   and (worst) the whole dock pop in over three seconds. So: delays to zero,
   the screen is simply there.

   The ceremony keeps its delays. It is a narrative — provision, dim, wake,
   greet — and collapsing it onto one frame doesn't remove motion, it removes
   the story. Nine seconds of lines arriving on cue with no movement is still
   the scene.

   The intro is skipped outright in app.js. Zeroing its delays instead — which
   the design handoff's snippet does globally — fires all twelve storm cards,
   four story beats and the orb on a single frame, which is louder than the
   motion it was meant to spare you. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms!important;
    animation-iteration-count:1!important;
    transition-duration:.01ms!important;
    scroll-behavior:auto!important;
  }
  /* ::before/::after are spelled out because `#vLand *` does NOT match a
     pseudo-element. Without them the dock's scrim keeps its 3.3s delay while
     everything around it snaps in at zero, and the dock sits bare for three
     seconds — the one place in this view that has a pseudo-element animation. */
  #vLand *,#vLand *::before,#vLand *::after,
  #vFin *,#vFin *::before,#vFin *::after{animation-delay:0s!important}
  #vIntro.on{animation:none!important}

  /* The ceremony's delays are NOT zeroed, so app.js keeps its normal schedule
     here rather than jumping the stream to its resting position. Every row now
     uses nsLineIn, which ends visible and stays — the old montage, which ended
     at opacity 0 and needed a special case to survive this, is gone. The column
     still advances on cue; the .55s transition just collapses, so it steps
     instead of gliding. */
}
