/* ============================================================
   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;

  --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);

  /* 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}}

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

#vIntro.on{animation:nsOut .85s ease 13.95s 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)}
#vIntro.on .b1{animation:nsBeat 2.5s ease 4.8s both}
#vIntro.on .b2{animation:nsBeat 2.2s ease 7.35s 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) 10.15s both}
.orb-breathe{position:absolute;inset:0}
#vIntro.on .orb-breathe{animation:nsBreathe 4.2s ease-in-out 11.3s 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 11.5s 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 1.8s ease 10.7s both}
#vIntro.on .a2{animation:nsBeat 2s ease 12.5s 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}
#vLand.on .cta{animation:nsFade .6s ease .95s both,nsCtaTravel .85s cubic-bezier(.5,0,.22,1) 2.95s both}
#vLand.on .cta-big{animation:nsOut .3s ease 3.12s both}
#vLand.on .cta-small{animation:nsFade .35s ease 3.4s both}
#vLand.on .cta-grad{animation:nsFade .4s ease 3.5s both}
#vLand.on .pill-chrome{animation:nsFade .45s ease 3.55s both}
#vLand.on .cta-slot{animation:nsCollapse .55s var(--enter) 3.9s both}
#vLand.on .field{animation:nsFade .5s ease 3.95s 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 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 is bare. */
.dock{
  position:fixed;left:0;right:0;z-index:20;
  bottom:var(--kb);
  display:flex;justify-content:center;
  padding:0 0 calc(18px + env(safe-area-inset-bottom));
  pointer-events:none;
  transition:bottom .2s var(--enter);
}
.dock-in{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. Deliberately
   no backdrop-filter: at this size it smears a band of the content sitting
   above the pill's own rect. */
.pill-chrome{
  position:absolute;inset:0;border-radius:999px;
  background:rgba(12,17,22,.92);
  border:1px solid var(--line-3);
  box-shadow:0 20px 50px rgba(0,0,0,.6);
}
/* 16px is a floor, not a preference: anything smaller makes iOS zoom the page
   on focus and the visitor loses the dock. */
.field{
  position:relative;
  flex:1;min-width:0;
  border:0;outline:none;background:transparent;
  color:var(--cream);font-size:16px;
  min-height:46px;padding:10px 8px 10px 14px;
}
.field::placeholder{color:var(--faint)}

/* 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 8px 22px rgba(242,103,58,.3);
}
/* 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;
  }
  #vLand *,#vFin *{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. */
}
