/* cloudloop.io coming-soon page. Single dark theme on purpose: the page is
   a black-space scene, so there is no light variant. */
:root {
  --bg: #000;
  --ink: #fff;
  --ink-2: rgba(255, 255, 255, 0.78);
  --ink-3: rgba(255, 255, 255, 0.55);
  --warm: #ffeee4;
  --accent: #a855f7;
  --accent-hover: #c084fc;
  --accent-soft: rgba(168, 85, 247, 0.18);
  --line: rgba(255, 255, 255, 0.14);
  --ok: #4ade80;
  --err: #fca5a5;
  --sans: "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Phone height drives the whole scene. The black hole is centred on the
     phone with its shadow (radius 0.62 H, js SHADOW) around it, so the
     stage is 1.32 H tall; the rest of the height holds the title, the
     tagline and the signup row. The width cap keeps the shadow on screen. */
  --H: clamp(260px, min(calc((82.5svh - 196px) / 1.32), calc((100vw - 32px) * 0.8)), 660px);
  --W: calc(var(--H) * 0.488);
}

*, *::before, *::after { box-sizing: border-box; }

html { background: var(--bg); color-scheme: dark; }
body {
  margin: 0;
  min-height: 100svh;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
@supports (overflow: clip) { body { overflow-x: clip; } }

.vh {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- Portal (WebGL black hole) ---- */
.portal-bg {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
  pointer-events: none;
}
.no-webgl .portal-bg { display: none; }

/* Static stand-in when WebGL is unavailable: shadow disk round the phone,
   photon ring, halo for the lensed arcs and the bright band crossing in
   front of the shadow (behind the phone). */
.portal-fallback { display: none; }
.no-webgl .portal-fallback {
  display: block;
  position: absolute;
  left: 0;
  width: 100%;
  top: 50%;
  height: calc(var(--H) * 2.6);
  margin-top: calc(var(--H) * -1.3);
  pointer-events: none;
  --R: calc(var(--H) * 0.62);
  background:
    radial-gradient(ellipse calc(var(--H) * 2.3) calc(var(--H) * 0.035) at 50% 50.6%, rgba(255, 244, 236, 0.95), rgba(255, 190, 160, 0.45) 45%, rgba(255, 140, 110, 0) 100%),
    radial-gradient(circle at 50% 50%, #000 0, #000 calc(var(--R) - 1px), rgba(255, 240, 230, 0.9) var(--R), rgba(255, 210, 190, 0.35) calc(var(--R) + 2px), rgba(0, 0, 0, 0) calc(var(--R) + 6px)),
    radial-gradient(circle at 50% 50%, rgba(255, 200, 175, 0) calc(var(--R) * 1.02), rgba(255, 214, 196, 0.75) calc(var(--R) * 1.2), rgba(255, 160, 130, 0.28) calc(var(--R) * 1.65), rgba(255, 120, 90, 0) calc(var(--R) * 2.25));
}

/* Turn controls: js/portal.js adds .can-orbit when the WebGL black hole
   runs. Open space (the portal canvas underneath the page), the title and
   the tagline grab; the page box itself lets the pointer through. The form
   and the phone keep their own cursors and the browser's touch handling,
   so pinch-zoom still works over them. A one-finger drag on the scene turns
   the hole, so touch panning is off there unless the page has to scroll
   (.scrolly), when vertical pans stay with the page, or a field has focus
   (.typing). Interactive overlays added inside .page need
   pointer-events: auto of their own. */
.can-orbit .page { pointer-events: none; }
.can-orbit .title, .can-orbit .tagline, .can-orbit .signup > *, .can-orbit .portal-bg { pointer-events: auto; }
.can-orbit .portal-bg, .can-orbit .title, .can-orbit .tagline { cursor: grab; touch-action: none; }
.can-orbit.scrolly .portal-bg, .can-orbit.scrolly .title, .can-orbit.scrolly .tagline { touch-action: pan-y; }
.can-orbit.typing .portal-bg, .can-orbit.typing .title, .can-orbit.typing .tagline { touch-action: auto; }
.orbiting, .orbiting * { cursor: grabbing !important; }
.orbiting body { -webkit-user-select: none; user-select: none; }

/* ---- Infall: instruments dropped into the hole ----
   The portal canvas draws them itself (over the black hole, below .page and
   the particle title). This full-screen canvas is only the ?sprites=1
   review grid. */
.infall {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
  pointer-events: none;
}
.infall-grid { z-index: 20; }

/* ---- Layout ---- */
.page {
  position: relative;
  z-index: 1;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 16px max(24px, env(safe-area-inset-bottom));
  /* The portal art (fallback gradient, phone clip box) is wider and taller
     than the content; never let it add scroll. */
  overflow: hidden;
}

.title {
  position: relative;
  z-index: 4;
  margin: clamp(22px, 4.2vh, 52px) 0 clamp(14px, 2.4vh, 30px);
  font-family: var(--sans);
  font-weight: 800;
  font-size: clamp(34px, min(10.4vw, 12.4vh), 136px);
  line-height: 1;
  letter-spacing: 0.035em;
  white-space: nowrap;
  text-align: center;
  color: var(--ink);
  text-shadow: 0 0 30px rgba(255, 210, 190, 0.25);
}
/* The particle canvas draws the title; the real text stays for a11y/SEO. */
.fx-title .title { color: transparent; text-shadow: none; }
.title-fx {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 3;
  pointer-events: none;
}

/* The stage holds the black hole: its centre (#hole) is the phone's centre.
   Spare height is shared above the stage and above the signup row. */
.stage {
  position: relative;
  width: 100vw;
  height: calc(var(--H) * 1.32);
  margin-top: auto;
  flex: none;
}
.hole-anchor {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
}
/* JS moves this with one matrix3d: the rise (surfacing from deep in the
   shadow), the float and the parallax tilt towards the pointer. */
.phone-float {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 2;
  width: var(--W);
  height: var(--H);
  margin: calc(var(--H) / -2) 0 0 calc(var(--W) / -2);
  will-change: transform;
  transform-origin: 50% 50%;
  pointer-events: none;
}
.phone {
  position: relative;
  /* Catches the pointer so drags that start on the phone never turn the
     hole. */
  pointer-events: auto;
  width: 100%;
  height: 100%;
  border-radius: calc(var(--W) * 0.17);
  padding: calc(var(--W) * 0.014);
  background: linear-gradient(150deg, #6b6874 0%, #2c2a33 22%, #18171d 50%, #2f2d36 76%, #77737f 100%);
  /* Inside the black shadow: a crisp edge and a faint violet bloom, no
     warm halo (that would light the shadow up). */
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.2),
    0 0 0 1px rgba(0, 0, 0, 0.7),
    0 0 calc(var(--H) * 0.07) rgba(168, 85, 247, 0.13),
    0 calc(var(--H) * 0.03) calc(var(--H) * 0.08) rgba(0, 0, 0, 0.6);
}
.phone-screen {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: calc(var(--W) * 0.158);
  padding: calc(var(--W) * 0.02);
  background: #000;
  overflow: hidden;
}
/* The sheen on the glass: a soft diagonal band that slides across the
   screen as the phone tilts (JS translates it). */
.glare {
  position: absolute;
  left: -40%;
  top: -40%;
  width: 180%;
  height: 180%;
  pointer-events: none;
  will-change: transform;
  background: linear-gradient(118deg, rgba(255, 255, 255, 0) 34%, rgba(255, 255, 255, 0.05) 42%, rgba(255, 255, 255, 0.12) 47%, rgba(255, 255, 255, 0.04) 52%, rgba(255, 255, 255, 0) 60%);
}
/* Darkness the phone surfaces out of (JS fades it during the rise). */
.phone-shade {
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  background: #000;
  opacity: 0;
  pointer-events: none;
}
.screen {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: calc(var(--W) * 0.14);
  background: #0e0e14;
}
.island {
  position: absolute;
  left: 50%;
  top: calc(var(--W) * 0.05);
  width: calc(var(--W) * 0.3);
  height: calc(var(--W) * 0.088);
  transform: translateX(-50%);
  border-radius: 999px;
  background: #000;
}
.island::after {
  content: "";
  position: absolute;
  right: 18%;
  top: 50%;
  width: calc(var(--W) * 0.03);
  height: calc(var(--W) * 0.03);
  transform: translateY(-50%);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #2b3350, #0a0c14 70%);
}
.btn {
  position: absolute;
  width: calc(var(--W) * 0.014);
  border-radius: 2px;
  background: linear-gradient(90deg, #2d2b33, #74717d 50%, #2d2b33);
}
.btn-action { left: calc(var(--W) * -0.011); top: 17%; height: 4%; }
.btn-volup { left: calc(var(--W) * -0.011); top: 24.5%; height: 7.5%; }
.btn-voldn { left: calc(var(--W) * -0.011); top: 33.5%; height: 7.5%; }
.btn-power { right: calc(var(--W) * -0.011); top: 27%; height: 11%; }

/* Two short lines just below the shadow, over the lower arc: the portal
   draws a soft dark plate behind them (and the text has a dark halo), so
   they stay legible at any pose and zoom. */
.tagline {
  position: relative;
  z-index: 4;
  margin: calc(var(--H) * 0.012) 0 0;
  max-width: calc(var(--H) * 0.78);
  font-family: var(--sans);
  font-weight: 600;
  font-size: clamp(12px, calc(var(--H) * 0.04), 22px);
  letter-spacing: 0.2em;
  line-height: 1.4;
  text-align: center;
  color: var(--warm);
  /* A dark halo first (the disk can swing behind the text), then a faint
     warm glow. */
  text-shadow: 0 0 2px #000, 0 0 4px #000, 0 0 10px rgba(0, 0, 0, 0.9), 0 0 18px rgba(0, 0, 0, 0.95), 0 0 30px rgba(255, 190, 160, 0.22);
  /* letter-spacing adds a trailing gap after the last glyph; re-centre. */
  padding-left: 0.2em;
}
.tagline span { display: block; white-space: nowrap; }

/* ---- Beta signup: one slim row pinned to the very bottom ----
   Desktop: "Sign up for beta access" + email + the four platform devices +
   button on one line. Narrower: the heading takes its own line, then email
   + button, then the devices. The legend stays for screen readers only. */
.signup {
  position: relative;
  z-index: 4;
  width: 100%;
  max-width: 1120px;
  margin-top: auto;
  padding-top: clamp(14px, 2.4vh, 28px);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 10px 16px;
}
.signup h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: var(--ink);
}
#signup-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-width: 0;
}
.field-email { flex: 1 1 240px; min-width: 200px; max-width: 300px; }
/* Readable over the bright lower arc of the disk. */
.signup h2 {
  text-shadow: 0 0 2px #000, 0 0 12px rgba(0, 0, 0, 0.95), 0 0 26px rgba(0, 0, 0, 0.85);
}
@media (min-width: 1001px) {
  .signup, #signup-form, .chips { flex-wrap: nowrap; }
  .signup h2, .platforms, .submit { flex: none; }
}
.submit { white-space: nowrap; }
@media (max-width: 1000px) {
  .signup h2 { flex-basis: 100%; text-align: center; }
  #signup-form { flex: 1 1 100%; }
}
@media (max-width: 760px) {
  .signup { gap: 8px; }
  #signup-form { width: 100%; max-width: 480px; gap: 8px; }
  .field-email { order: 1; flex: 1 1 0; max-width: none; min-width: 0; }
  .submit { order: 2; }
  .platforms { order: 3; flex-basis: 100%; }
  /* Four devices on one line. */
  .chips { justify-content: center; flex-wrap: nowrap; gap: 5px; }
  .chip { flex: 1 1 0; min-width: 0; max-width: 92px; }
  .dev-tile { width: 100%; }
}

input[type="email"] {
  width: 100%;
  height: 40px;
  padding: 0 14px;
  border-radius: 10px;
  border: 1px solid var(--line);
  /* Near-opaque instead of a backdrop blur: the scene behind changes every
     frame, so a blur there is recomputed every frame (costly on integrated
     GPUs). */
  background: rgba(12, 10, 18, 0.93);
  color: var(--ink);
  font: 500 16px/1 var(--sans);
}
input[type="email"]::placeholder { color: var(--ink-3); }
input[type="email"]:focus-visible {
  outline: 2px solid var(--accent-hover);
  outline-offset: 2px;
  border-color: transparent;
}
input[type="email"][aria-invalid="true"] { border-color: var(--err); }

.platforms {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
/* Each platform is a drawing of its devices with the name under it; the
   real checkbox stays in the label for keyboards and screen readers. */
.chip { position: relative; cursor: pointer; }
.chip input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
}
.dev-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  width: 84px;
  height: 58px;
  padding: 3px 4px 4px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(12, 10, 18, 0.93);
  color: var(--ink-2);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.1;
  white-space: nowrap;
  user-select: none;
  transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
}
.dev {
  display: block;
  width: 66px;
  height: 40px;
  opacity: 0.72;
  transition: opacity 0.15s;
}
/* A small check light in the corner: the selected state is not colour
   alone. */
.dev-tile::after {
  content: "";
  position: absolute;
  top: 4px;
  right: 4px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, 0.3);
  transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.chip:hover .dev-tile { border-color: rgba(255, 255, 255, 0.3); color: var(--ink); }
.chip:hover .dev { opacity: 0.85; }
.chip input:checked + .dev-tile {
  border-color: var(--accent);
  background: #22103a;
  color: var(--ink);
  box-shadow: 0 0 0 1px var(--accent), 0 0 18px rgba(168, 85, 247, 0.55), inset 0 0 14px rgba(168, 85, 247, 0.28);
}
.chip input:checked + .dev-tile .dev { opacity: 1; }
.chip input:checked + .dev-tile::after {
  border-color: var(--accent-hover);
  background: var(--accent-hover);
  box-shadow: 0 0 6px rgba(192, 132, 252, 0.9);
}
.chip input:focus-visible + .dev-tile {
  outline: 2px solid var(--accent-hover);
  outline-offset: 2px;
}
.platforms[aria-invalid="true"] .dev-tile { border-color: rgba(252, 165, 165, 0.6); }

.submit {
  height: 40px;
  padding: 0 18px;
  border: 0;
  border-radius: 10px;
  /* #9333ea keeps white text above 4.5:1 (the lighter #a855f7 is ~4:1). */
  background: #9333ea;
  color: #fff;
  font: 600 15px/1 var(--sans);
  cursor: pointer;
  transition: background 0.15s, opacity 0.15s;
}
.submit:hover { background: #7e22ce; }
.submit:focus-visible { outline: 2px solid var(--accent-hover); outline-offset: 2px; }
.submit:disabled { opacity: 0.6; cursor: progress; }

.status {
  order: 9;
  flex-basis: 100%;
  margin: 0;
  font-size: 14px;
  text-align: center;
  color: var(--ink-2);
}
.status:empty { display: none; }
.status:focus { outline: none; }
.status.ok { color: var(--ok); }
.status.err { color: var(--err); }
.signup.done #signup-form > :not(.status) { display: none; }
.signup.done .status { flex-basis: auto; font-size: 15px; }

.hp {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The signup wraps to two rows below 1000 px and three below 760 px (the
   devices get their own row); keep its room. */
@media (max-width: 1000px) {
  :root { --H: clamp(260px, min(calc((83svh - 240px) / 1.32), calc((100vw - 32px) * 0.8)), 660px); }
}
@media (max-width: 760px) {
  :root { --H: clamp(220px, min(calc((100svh - 312px - min(10.4vw, 12.4vh)) / 1.32), calc((100vw - 32px) * 0.8)), 560px); }
}

/* og-image render mode (?og=1): no form, scene fills the card. */
.og .signup { display: none; }
.og { --H: calc((100vh - 196px) / 1.32); }
