/* ============================================================
   COMING SOON — standalone holding page.

   Self-contained on purpose: it does NOT load assets/styles.css, so nothing
   here can be broken by (or break) the main site while that is still being
   built. It only borrows two things from the real site, byte-identical:

     · the deck   — the cream knit of the homepage start slider (body.home)
     · the logo   — the two-line black logo, centred, same width formula as
                    the hero logo (assets/styles.css → .world--hero / .claim--logo)

   All paths are RELATIVE and point at this folder's OWN assets/ copy, so the
   whole page ships as one self-contained directory — upload /coming-soon/ and
   nothing outside it is needed. Works over HTTP and from disk (file://).

   Everything tunable sits in the :root block below.
   ============================================================ */

:root {
  /* deck (mirrors body.home in assets/styles.css) */
  --cs-deck: #efece4;

  /* logo — a 320px cap, pulled back from the 400px maximum so it sits calmly in
     the middle of the page. All three terms scale together, so the mark shrinks
     proportionally on smaller viewports instead of just hitting a lower ceiling.
     Width-driven, but the height term binds on short / landscape viewports so it
     never overflows. svh with a vh fallback. */
  --cs-logo-w: min(61vw, 320px, 40vh);
  --cs-logo-w: min(61vw, 320px, 40svh);
  --cs-logo-ar: 1.1755;                                  /* 710 / 604 */

  /* Base travel speed, read by coming-soon.js. Each flyer scatters around it by
     ±--cs-speed-spread, and its mode scales it again — a drift crawls, a zip
     bolts. */
  --cs-drift-speed: 105;                                 /* px per second (unitless for JS) */
  --cs-swap-fade: .55s;                                  /* black ⇄ black/white on CONTACT hover */

  /* The third dimension. Depth is a real translateZ against this perspective,
     so the logo looms and recedes instead of merely scaling. A SHORT
     perspective plus a LONG depth corridor is what makes the size swing read:
     at --cs-drift-near it renders about 1.7× its base size, at --cs-drift-far
     about 0.48× — roughly a 3.5:1 range. */
  --cs-perspective: 1000px;
  --cs-drift-near: 420;                                  /* px of translateZ toward the viewer */
  --cs-drift-far: 1100;                                  /* px of translateZ away */
  --cs-drift-zspeed: 80;                                 /* px per second through depth */

  /* In flight the logo starts SMALLER than at rest, so there is headroom to
     loom without instantly hitting an edge (the bounds shrink as it grows, and
     a logo wider than the screen would just pin itself to the middle). */
  --cs-fly-w: min(46vw, 240px, 30vh);
  --cs-fly-w: min(46vw, 240px, 30svh);

  /* Arrivals. One pops into being somewhere on screen, the next sails in from
     beyond an edge, and they keep coming until the air is full — nothing ever
     leaves again, a full screen is the point. */
  --cs-hatch: .45;                                       /* seconds to pop into being */
  --cs-fly-seed: 3;                                      /* how many arrive on the click */
  --cs-spawn-min: .16;                                   /* shortest gap between arrivals */
  --cs-spawn-max: .5;                                    /* longest gap between arrivals */
  --cs-enter-fly: .45;                                   /* share that fly in from off-screen */
  --cs-egg-spawn: .22;                                   /* chance a bounce still spawns one */

  /* Size spread. Each flyer draws its own base size once, on top of which the
     depth still scales it — so no two are alike and the flock never reads as a
     set of identical stamps. Speed is scattered by the same idea. */
  --cs-fly-min: .58;                                     /* smallest base size */
  --cs-fly-max: 1.28;                                    /* largest base size */
  /* Hard ceiling on base size × depth. Beyond this the perspective distortion
     stops reading as depth and starts reading as a mistake — a near-camera
     logo tilted 17° smears badly and eats the whole screen. */
  --cs-scale-cap: 2.05;
  /* Big flyers bank LESS. The tilt is a nice cue at normal size; up close the
     same angle throws the artwork into an unreadable wedge. */
  --cs-bank-damp: 1.15;
  --cs-speed-spread: .7;                                 /* ± fraction of the base speed */

  /* Character. Randomising size and speed alone still produces fifteen copies
     of ONE behaviour — so each flyer instead gets a genuinely different way of
     moving, drawn from these weights (they need not sum to 1, they are
     normalised):

       bounce    straight-ish, ricochets off the walls — the classic
       drift     slow, unhurried, holds its depth: quiet background presence
       zip       fast and dead straight, and it dashes off every wall
       orbit     circles a point of its own instead of bouncing at all
       pulse     ambles along while breathing in and out
       swing     swings to and fro along one axis like a pendulum
       jitter    nervous, changing its mind several times a second
       dive      works mostly in DEPTH: rushes at the viewer and falls back

     Depth is a first-class part of this, not a garnish. The orbit is a tilted
     ring in 3D, so it passes behind its neighbours and then in front of them;
     the diver crosses the whole corridor; and every flyer's own near/far limits
     differ, so the sizes never pulse in unison. */
  --cs-mode-bounce: 26;
  --cs-mode-drift: 14;
  --cs-mode-zip: 11;
  --cs-mode-orbit: 14;
  --cs-mode-pulse: 10;
  --cs-mode-swing: 8;
  --cs-mode-jitter: 8;
  --cs-mode-dive: 12;

  --cs-calm-share: .3;                                   /* share that fly without tilt */
  --cs-still-share: .22;                                 /* share that hold their depth */

  /* What share arrives on the schedule. The REST are held back deliberately and
     can only ever be brought into the world by a BOUNCE — which is what makes
     the multiplication visible again instead of the pool being exhausted before
     a single impact has happened. */
  --cs-fly-auto: .72;

  /* Easter eggs, as odds per bounce. Rare on purpose: they should be a reward
     for watching, not a tic. --cs-squash is the always-on impact deformation,
     the one that makes the collisions read as physical. */
  --cs-squash: .34;                                      /* impact squash amount */
  --cs-egg-roll: .09;                                    /* chance of a barrel roll */
  --cs-egg-flip: .06;                                    /* chance of a forward flip */
  --cs-egg-dash: .05;                                    /* chance of a sudden dash */
  --cs-egg-tumble: .04;                                  /* chance of tumbling on both axes */

  /* Atmospheric perspective: distance drains a little presence, so depth reads
     even when a flyer is momentarily still. The single strongest depth cue
     after the scaling itself, and it costs nothing. */
  --cs-haze: .5;                                         /* opacity lost at the far plane */

  /* A dash stretches along its own direction of travel — the classic way to
     sell speed without an actual motion blur. */
  --cs-dash-stretch: .35;

  /* Flyers notice each other: pass close enough and both get jostled. The value
     is a fraction of their combined radii, so big ones have a wider bubble. */
  --cs-bump-range: .62;

  /* The opening. The flight begins on the black logo, held for a beat.
       hold    the black logo stands centred, exactly as at rest
       swarm   it breaks apart, fills the screen and keeps flying
     There is no ending — the black logo is the opening beat only, and a click
     anywhere is what takes the page back to rest. */
  --cs-cycle-hold: 1.1;                                  /* seconds the black logo stands */

  /* The ground changes too, but only while the flight runs — at rest the page
     stays on cream. Long fades and long holds on purpose: it should be
     something you notice having happened, not something you watch happen. */
  --cs-deck-fade: 4.5s;                                  /* cross-fade duration */
  --cs-deck-hold-min: 6;                                 /* shortest time on one ground */
  --cs-deck-hold-max: 13;                                /* longest time on one ground */

  /* How full the screen gets, on ANY viewport. The count is derived from the
     field's area divided by one flyer's area, times this coverage — so a wide
     desktop fills with more of them and a phone with fewer, but both end up
     equally full. Above 1 they overlap, which is the intent.
     There are only fifteen colours; where more bodies are needed than that, the
     artwork is simply reused (JS clones the nodes). --cs-fly-hard is the safety
     ceiling, since every live flyer is its own composited layer holding a large
     PNG — a memory and fill-rate budget as much as a design choice. */
  --cs-fly-cover: 1.45;
  --cs-fly-hard: 34;

  /* How hard it banks into its own direction of travel, and how far the path
     curves instead of running straight. 0 = the plain linear bounce. */
  --cs-drift-bank: 17;                                   /* max degrees of tilt */
  --cs-drift-wander: .34;                                /* how strongly the course curves */

  --cs-top: clamp(24px, 7vh, 72px);                      /* stage top → logo; also mail → stage bottom */
  --cs-gap: clamp(18px, 4vh, 44px);                      /* logo bottom → copy */
  --cs-ink: #0a0a0a;

  /* the knit CONTACT mark at the foot — its own width, deliberately NOT tied to
     the logo, so the two can be sized against each other by eye. */
  --cs-mail-w: min(44vw, 230px, 29vh);
  --cs-mail-w: min(44vw, 230px, 29svh);
  --cs-mail-scale: 1.07;                                 /* hover grow, the mark */
  --cs-logo-hover-scale: 1.22;                           /* hover grow, the logo — deliberately
                                                            bigger: it has the room, and the
                                                            size change is the point */
  --cs-hover: .45s;                                      /* crossfade + scale duration */
  --cs-ease: cubic-bezier(.22, .61, .36, 1);

  /* The address: three long lines, at the logo's width — but with a height term
     of its own so
     that it shrinks on short / landscape viewports like the logo and the mark
     do. Ratio is 1046/664 ≈ 1.575, so 44svh of width caps its height at ~28svh. */
  --cs-addr-w: min(76vw, 400px, 44vh);
  --cs-addr-w: min(76vw, 400px, 44svh);
  --cs-addr-rise: 10px;                                  /* how far it drifts up into place */
  --cs-imprint-gap: 5px;                                 /* CONTACT bottom → IMPRINT top */
  --cs-swap: .7s;
  --cs-swap-in: .98;                                     /* starts a hair small */
}

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

html, body {
  height: 100%;
  margin: 0;
  padding: 0;
}

/* Makes the artwork awkward to lift: nothing is selectable, nothing is
   draggable, and iOS raises no long-press "save image" sheet. Combined with the
   fact that every image is a CSS background rather than an <img>, the casual
   routes are closed. The files are of course still in the network tab — that is
   unavoidable, the browser has to receive them to draw them. */
.cs, .cs * {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
}

body.cs {
  /* Only the flat colour here — the knit itself is the first deck layer below,
     so it can be cross-faded against the others. This stays as the fallback if
     the images never arrive. */
  background: var(--cs-deck);
  color: var(--cs-ink);
  font-family: 'Anton', system-ui, sans-serif;
  overflow: hidden;                                      /* the site never scrolls the body */
}


/* ---- the decks -------------------------------------------------------------
   Five knit grounds stacked behind everything. Only the first shows at rest;
   while the flight runs, JS cross-fades between them at random.

   The trick that makes it seamless: the INCOMING layer is raised above the
   others and faded in over them, and the outgoing ones are only cleared once it
   is fully opaque. Fading one out while fading the next in would dip through
   the bare colour for a moment — very visible on a light deck. */
.cs__decks {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.cs__deck {
  position: absolute;
  inset: 0;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity var(--cs-deck-fade) linear;
  will-change: opacity;
}
.cs__deck:first-child { opacity: 1; }           /* cream, the resting ground */

/* Each ground in two sizes. A phone never needs the 2560px plate, so it is only
   fetched where it can actually be seen — the small tier is a quarter of the
   bytes and indistinguishable on a handset. */
.cs__deck--cream { background-image: url('assets/backgrounds/bg-cream.webp'); }
.cs__deck--mint  { background-image: url('assets/backgrounds/bg-mint.webp'); }
.cs__deck--pink  { background-image: url('assets/backgrounds/bg-pink.webp'); }
.cs__deck--peach { background-image: url('assets/backgrounds/bg-peach.webp'); }
.cs__deck--blue  { background-image: url('assets/backgrounds/bg-blue.webp'); }

@media (max-width: 820px) {
  .cs__deck--cream { background-image: url('assets/backgrounds/bg-cream-sm.webp'); }
  .cs__deck--mint  { background-image: url('assets/backgrounds/bg-mint-sm.webp'); }
  .cs__deck--pink  { background-image: url('assets/backgrounds/bg-pink-sm.webp'); }
  .cs__deck--peach { background-image: url('assets/backgrounds/bg-peach-sm.webp'); }
  .cs__deck--blue  { background-image: url('assets/backgrounds/bg-blue-sm.webp'); }
}


/* full-viewport stage: the logo centred in the field, the marks at the foot */
.cs__stage {
  position: relative;
  z-index: 1;                                            /* above the deck layers */
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  padding: calc(env(safe-area-inset-top) + var(--cs-top))
           env(safe-area-inset-right)
           calc(env(safe-area-inset-bottom) + var(--cs-top))
           env(safe-area-inset-left);
}

/* The field the logo roams: the flex child that grows, so it claims exactly the
   space between the stage's top padding and the foot. Bounds are read off this
   element in JS — nothing about the drift area is hard-coded. */
.cs__field {
  position: relative;
  flex: 1 1 auto;
  align-self: stretch;
}

/* The logo box holds its own aspect ratio, so the twelve colour cuts (whose
   source files differ in size) can all sit absolutely inside it and object-fit
   into the same box — nothing shifts between frames.
   Default position is dead centre: that is what a visitor sees if JS never runs
   or motion is reduced. Once the drift starts, JS drives `transform` instead. */
.cs__logo {
  margin: 0;
  width: var(--cs-logo-w);
  aspect-ratio: var(--cs-logo-ar);
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* transform picks up the CONTACT hover timing exactly, so the logo and the
     mark grow as one gesture */
  transition: opacity var(--cs-swap) var(--cs-ease),
              transform var(--cs-hover) var(--cs-ease);
}

/* Both resting cuts share one box and one fade. Painted as BACKGROUNDS rather
   than <img>: a background offers no "save image as", cannot be dragged out and
   raises no long-press menu on a phone. The meaning is carried by role="img" +
   aria-label on the parent, so nothing is lost to a screen reader. */
.cs__logo span {
  position: absolute;
  inset: 0;
  display: block;
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity var(--cs-swap-fade) var(--cs-ease);
}
.cs__logo-rest { background-image: url('assets/logo-2line/logo-black.webp'); }
.cs__logo-bw   { background-image: url('assets/logo-2line/logo-bw.webp'); }

/* At rest: black, and nothing else. Scoped through .cs__logo on purpose — a bare
   .cs__logo-rest would lose to the `.cs__logo img` rule above on specificity and
   the logo would never show at all. */
.cs__logo .cs__logo-rest { opacity: 1; }

/* ---- the flock ------------------------------------------------------------
   One flyer per colour. Dormant by default and out of the layout entirely, so
   the idle boxes cost nothing; JS drives each one's transform once it hatches.

   The container is explicitly positioned rather than relying on the children
   resolving against .cs__field — the flock must stay pinned to the field even
   if something is ever inserted between the two. */
.cs__flyers {
  /* Pinned to the whole STAGE, so the flock uses the entire screen — including
     the band the foot occupies and the safe-area padding. The resting logo
     stays in .cs__field, which is what keeps it from moving on the click. */
  position: absolute;
  inset: 0;
  z-index: 2;                                  /* over the logo, under the address */
  pointer-events: none;

  /* The perspective lives HERE, on the flyers' direct parent, and the flyers
     stay FLAT (the default). That combination is deliberate:

       · each flyer's translateZ is still projected through this perspective, so
         the looming and receding is real, not a faked scale;
       · but the flyers do not share one 3D space, so their planes cannot
         intersect. preserve-3d here looked correct until two banked logos
         overlapped — the browser then sliced both along their intersection
         line, which is what cut the artwork in half.

     Depth ordering is handled in JS instead, by writing z-index from each
     flyer's distance. Same result, none of the slicing. */
  perspective: var(--cs-perspective);
  perspective-origin: 50% 45%;
}
.cs__flyer {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--cs-fly-w);
  aspect-ratio: var(--cs-logo-ar);
  visibility: hidden;
  pointer-events: none;
  /* Flat on purpose — see the note on .cs__flyers above. */
}
.cs__flyer {
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
}
.cs__flyer.is-live {
  visibility: visible;
  will-change: transform;
}

/* Hovering CONTACT swaps the resting black for the black-and-white cut. The
   logo sits BEFORE the mark in the DOM and in a different parent, so :has() on
   the stage is what reaches it. Unsupported browsers just keep the black logo —
   nothing breaks. Gated on real hover so a tap can't strand it.

   The logo also grows by the same factor, over the same duration and curve as
   the mark itself — one gesture in two places. The centring translate has to be
   restated here, otherwise the scale would replace it and the logo would jump
   out of the middle. */
@media (hover: hover) {
  .cs__stage:has(.cs__mail:hover) .cs__logo-rest { opacity: 0; }
  .cs__stage:has(.cs__mail:hover) .cs__logo-bw { opacity: 1; }
  .cs__stage:has(.cs__mail:hover) .cs__logo {
    transform: translate(-50%, -50%) scale(var(--cs-logo-hover-scale));
  }
}
.cs__stage:has(.cs__mail:focus-visible) .cs__logo-rest { opacity: 0; }
.cs__stage:has(.cs__mail:focus-visible) .cs__logo-bw { opacity: 1; }
.cs__stage:has(.cs__mail:focus-visible) .cs__logo {
  transform: translate(-50%, -50%) scale(var(--cs-logo-hover-scale));
}

/* Copy / countdown / form land here, under the logo. Empty → zero height. */
.cs__body {
  margin-top: var(--cs-gap);
  width: min(90vw, 640px);
  text-align: center;
}
.cs__body:empty { margin-top: 0; }

/* Foot block, pushed to the bottom of the stage (margin-top:auto), so the CONTACT
   mark's gap to the bottom edge is exactly the stage's bottom padding = --cs-top.
   Also the positioning context for the address hovering above it. */
.cs__foot {
  /* no auto margin here — the logo's own auto margins already claim the free
     space, which leaves the foot sitting on the stage's bottom padding */
  padding-top: var(--cs-gap);
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.cs__mail {
  width: var(--cs-mail-w);
  display: block;
  position: relative;                                    /* anchors the colour crop */
  transition: transform var(--cs-hover) var(--cs-ease);
  will-change: transform;
}
.cs__mail { aspect-ratio: 1300 / 410; }         /* the art's ratio, which the <img> used to carry */
.cs__mail-img {
  position: absolute;
  inset: 0;
  display: block;
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
  background-image: url('assets/headlines/headline-contact.webp');
}

/* The rainbow crop sits exactly on top of the black one (same box, both at
   width:100%) and is faded in on hover — so the mark "colours in" instead of
   swapping. Pointer-events off: the anchor stays the only hit target. */
.cs__mail-img--color {
  background-image: url('assets/headlines/headline-contact-color.webp');
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--cs-hover) var(--cs-ease);
}

/* The postal address: centred at the foot of the stage, over everything, and
   never a click target — while it is up, a click anywhere ends the flight.
   It sits on the same bottom rhythm as the CONTACT mark it replaces. */
.cs__addr {
  position: absolute;
  left: 50%;
  bottom: calc(env(safe-area-inset-bottom) + var(--cs-top));
  z-index: 3;
  width: var(--cs-addr-w);
  aspect-ratio: 1046 / 664;
  background-image: url('assets/headlines/headline-address.webp');
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, var(--cs-addr-rise)) scale(var(--cs-swap-in));
  transition: opacity var(--cs-swap) var(--cs-ease),
              transform var(--cs-swap) var(--cs-ease);
}

/* Gated on real hover so a tap on touch doesn't leave it stuck coloured;
   keyboard focus gets the same treatment. */
@media (hover: hover) {
  .cs__mail:hover { transform: scale(var(--cs-mail-scale)); }
  .cs__mail:hover .cs__mail-img--color { opacity: 1; }
}
.cs__mail:focus-visible {
  transform: scale(var(--cs-mail-scale));
  outline: none;
}
.cs__mail:focus-visible .cs__mail-img--color { opacity: 1; }

/* The IMPRINT button under CONTACT: same width, its own (taller) ratio carried
   by height:auto. Stripped back to bare art — it is a control, not a chrome. */
.cs__imprint {
  display: block;
  width: var(--cs-mail-w);
  margin: var(--cs-imprint-gap) 0 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition: transform var(--cs-hover) var(--cs-ease);
}
/* the button IS the mark now — no child element to speak of */
.cs__imprint {
  aspect-ratio: 1272 / 495;
  background-image: url('assets/headlines/headline-imprint.webp');
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
}
@media (hover: hover) {
  .cs__imprint:hover { transform: scale(var(--cs-mail-scale)); }
}
.cs__imprint:focus-visible {
  outline: 2px solid var(--cs-ink);
  outline-offset: 6px;
}

/* ---- the flight, started by the IMPRINT button -----------------------------
   The foot fades out and stops taking clicks, the field is promoted to the full
   stage so the logo has the whole screen to roam, and the address comes up. */
/* visibility (delayed until the fade has finished) is what takes CONTACT and
   IMPRINT out of the tab order too — pointer-events alone would leave a
   keyboard user focusing invisible controls mid-flight. */
body.is-imprint .cs__foot {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition: opacity var(--cs-swap) var(--cs-ease),
              visibility 0s linear var(--cs-swap);
}
/* The field deliberately KEEPS its layout box when the flight starts. Promoting
   it to the full stage made it grow downward into the space the foot vacates —
   and since the logo is centred inside it, the logo visibly jumped on the click.
   It now stands exactly where it stood at rest, and the flock departs from
   there. */
/* The logo steps aside only while the flock is actually in the air — NOT for the
   whole imprint state. That is what lets it stand there black between cycles:
   the flyers hatch on the exact spot it vacates and are drawn back onto it,
   so both the break-up and the reunion read as a handover. */
body.is-swarming .cs__logo { opacity: 0; }
body.is-imprint .cs__addr {
  opacity: 1;
  transform: translate(-50%, 0) scale(1);
}

/* No per-viewport counts any more: the coverage formula derives the number from
   the actual field area, so every screen fills to the same density by itself. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }

  /* No flight: the logo stays put in black and the flock never hatches.
     coming-soon.js checks the same query and never enters the cycle, so
     `is-swarming` is never set and the logo simply stays visible — the IMPRINT
     button still reveals the address, it just doesn't launch anything. */
  .cs__flyer { display: none !important; }
}
