/* ══════════════════════════════════════════════════════════════
   Keln — the Title Screen ("Cold Open").

   ══ IMPORTANT — READ BEFORE YOU REPLACE YOUR EXISTING FILE ══
   The file currently sitting at public/landing-cinema.css does
   NOT contain title-screen styles. It contains `.lf-hud` (the
   overlay that floats above the three fullscreen modules) and the
   landing-only calendar demo. Those styles are load-bearing —
   landing.js builds `.lf-hud` markup every time a portal node is
   opened — but they were filed under the wrong name, which is why
   the title screen has had no CSS at all.

   Do this, in order, before dropping this file in:
     1. Create public/landing-fullscreen.css
     2. Paste the ENTIRE current contents of landing-cinema.css
        into it. Change nothing.
     3. In landing.html, add, next to the other fullscreen links:
          <link rel="stylesheet" href="/landing-fullscreen.css">
     4. Now replace landing-cinema.css with this file.

   Nothing else needs to change. Same filename, same link tag,
   same load order.

   ══ THE Z-INDEX ═════════════════════════════════════════════
   3200 — above every landing surface, above the sidebar (40) and
   its scrim (39), above the fullscreen chrome. Toasts live at
   9999 and will render over this, which is correct: a real error
   during arrival should be visible.

   ══ WHAT OWNS WHAT ══════════════════════════════════════════
   JS owns the sky (one canvas, three star layers) and the exit
   trigger. CSS owns the mark, the prompt, the vignette and every
   transition. Nothing is animated from JS that a compositor could
   animate for free.
   ══════════════════════════════════════════════════════════════ */

/* ── Scroll lock ────────────────────────────────────────────
   Applied to <html> AND <body>: iOS Safari honours one and
   ignores the other depending on version, so we set both. */
html.kc-lock,
body.kc-lock {
  overflow: hidden !important;
  overscroll-behavior: none;
  touch-action: none;
}

/* ── Stage ──────────────────────────────────────────────────
   Pure black, not near-black. This is the one surface in the
   product where the background is meant to disappear entirely so
   the star field has nothing to compete with. */
#kelnCinema.kc {
  position: fixed;
  inset: 0;
  z-index: 3200;

  display: grid;
  place-items: center;

  background: #000;
  color: #e7eef8;
  /* --sans, deliberately, NOT --font. The product voice is the
     serif; the title screen is the one surface that must not be it.
     A serif "PRESS ANYTHING TO START" under a logotype reads as a
     book plate, and the moment before the product opens should read
     as an instrument powering up. */
  font-family: var(--sans, Inter, system-ui, -apple-system, sans-serif);

  /* No cursor games here — the custom cursor mounts site-wide and
     should be visible over the title screen too. */
  contain: strict;

  opacity: 1;
  will-change: opacity;
  /* Opacity ONLY. An earlier revision also transitioned
     `filter: brightness()` here, which forces a full-viewport
     repaint — canvas and all — on every frame of the dissolve, on
     the same main thread that is still drawing the star warp. The
     light flood now lives on .kc__flash, which composites. */
  transition: opacity .90s cubic-bezier(.4, 0, .2, 1);
}

/* The stage takes focus when it opens (see landing-cinema.js) so
   screen readers enter the dialog. It is a container, not a
   control, so it must never paint a ring. */
#kelnCinema.kc:focus,
#kelnCinema.kc:focus-visible { outline: none; }

/* The dissolve. Brightness lifts fractionally as it goes so the
   screen reads as being flooded with light rather than simply
   switched off. */
#kelnCinema.kc--out {
  opacity: 0;
  pointer-events: none;
}

/* ── Light flood ────────────────────────────────────────────
   Fires once, on exit. Blooms out of the mark's position and is
   gone before the stage is. Animating opacity on an already
   composited layer costs nothing; the brightness filter it
   replaced cost a full-screen repaint per frame. */
.kc__flash {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  will-change: opacity;
  background: radial-gradient(circle at 50% 50%,
    rgba(255, 255, 255, .92) 0%,
    rgba(198, 226, 255, .42) 18%,
    rgba(34, 211, 166, .12)  38%,
    transparent 66%);
}
.kc--out .kc__flash {
  animation: kc-flash .80s cubic-bezier(.4, 0, .2, 1) forwards;
}
@keyframes kc-flash {
  0%   { opacity: 0; transform: scale(.72); }
  24%  { opacity: .60; }
  100% { opacity: 0; transform: scale(1.5); }
}

/* ── Sky ────────────────────────────────────────────────────── */
.kc__sky {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* ── Vignette ───────────────────────────────────────────────
   Two jobs. It darkens the corners so the eye is pulled to the
   mark, and it puts the faintest cool wash behind centre so the
   logo's glow has something to sit in instead of pure void.

   It is the only gradient on the screen. Two competing radial
   glows — the mistake in the old title frame — is what made the
   old screen read as a template. */
.kc__vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 46% 34% at 50% 50%,
      rgba(91, 140, 255, .062) 0%,
      rgba(74, 122, 232, .026) 46%,
      transparent 74%),
    radial-gradient(ellipse 120% 90% at 50% 50%,
      transparent 42%,
      rgba(0, 0, 0, .55) 88%,
      rgba(0, 0, 0, .82) 100%);
  opacity: 0;
  transition: opacity 1.4s ease .15s;
}
.kc--in .kc__vignette { opacity: 1; }

/* ── Centre column ──────────────────────────────────────────── */
.kc__center {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 34px;
  padding: var(--s-10);
  text-align: center;
  /* Optical centring. The mark plus the prompt is a bottom-heavy
     block, so mathematically centring it makes the mark itself sit
     low. Lifting the group ~2.5vh puts the MARK on the optical
     centre, which is the thing the eye is actually measuring. */
  transform: translateY(-2.5vh);
}

/* ── The mark ───────────────────────────────────────────────
   Two glows, layered, doing different jobs:
     · a tight cool halo that reads as the logo being lit
     · a wide teal bloom that reads as it being POWERED

   Both live on `filter`, which the compositor handles, and both
   breathe on the same 5.2s cycle so they never beat against each
   other. A hard pulse reads as an alert; a slow swell reads as
   something running. */
.kc__logo {
  display: block;
  width: clamp(180px, 26vw, 300px);
  height: auto;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;

  opacity: 0;
  transform: translateY(10px) scale(.965);
  filter:
    drop-shadow(0 0 1px rgba(226, 240, 255, .85))
    drop-shadow(0 0 14px rgba(140, 190, 255, .22))
    drop-shadow(0 0 44px rgba(34, 211, 166, .07));
  transition:
    opacity   1.20s cubic-bezier(.16, .86, .22, 1) .70s,
    transform 1.20s cubic-bezier(.16, .86, .22, 1) .70s,
    filter     .80s ease;
}

.kc--in .kc__logo,
.kc--forced .kc__logo {
  opacity: 1;
  transform: none;
  animation: kc-breathe 5.2s ease-in-out 1.9s infinite;
}

/* The glow is deliberately cold — a white core, a blue halo, and
   only a whisper of the brand teal at the very outside. Leading
   with teal turns the mark green and reads as a status light
   rather than as something powered. */
@keyframes kc-breathe {
  0%, 100% {
    filter:
      drop-shadow(0 0 1px  rgba(226, 240, 255, .85))
      drop-shadow(0 0 14px rgba(140, 190, 255, .22))
      drop-shadow(0 0 44px rgba(34, 211, 166, .07));
  }
  50% {
    filter:
      drop-shadow(0 0 2px  rgba(240, 248, 255, 1))
      drop-shadow(0 0 22px rgba(160, 205, 255, .34))
      drop-shadow(0 0 70px rgba(34, 211, 166, .13));
  }
}

/* The bloom on the way out. The mark is the last thing to go and
   it goes by getting brighter, not dimmer. */
.kc--out .kc__logo {
  animation: none;
  opacity: 0;
  transform: scale(1.13);
  /* Stepped, not transitioned. A drop-shadow tween is a repaint of
     the blurred surface on every frame; the flood layer is already
     selling the bloom, so one repaint is enough. */
  filter:
    drop-shadow(0 0 4px  rgba(255, 255, 255, 1))
    drop-shadow(0 0 40px rgba(34, 211, 166, .7));
  transition:
    opacity   .75s cubic-bezier(.4, 0, .2, 1) .10s,
    transform .90s cubic-bezier(.16, .86, .22, 1);
}

/* Logo failed to load. Rather than an empty screen, set the
   wordmark in type — deliberately, not apologetically. */
.kc--nologo .kc__logo { display: none; }
.kc--nologo .kc__center::before {
  content: "KELN";
  font-size: clamp(44px, 8vw, 92px);
  font-weight: 800;
  letter-spacing: .16em;
  text-indent: .16em;
  color: #f2f7ff;
  text-shadow: 0 0 40px rgba(34, 211, 166, .45);
}

/* ── The prompt ─────────────────────────────────────────────
   This is a real <button>, so keyboard and screen-reader users
   have something to land on, but it is styled as pure type. A
   bordered pill here would look like a cookie banner.

   Mono, uppercase, wide tracking: the same micro-type grammar
   used for every eyebrow in the product, so the very first thing
   a visitor reads is already in the product's voice. */
.kc__enter {
  appearance: none;
  background: none;
  border: 0;
  padding: var(--s-5) var(--s-8);
  margin: 0;
  cursor: pointer;
  border-radius: var(--r-sm);

  font-family: var(--mono, 'JetBrains Mono', ui-monospace, monospace);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: .34em;
  text-indent: .34em;      /* offsets the trailing letter-space */
  text-transform: uppercase;
  color: rgba(214, 230, 250, .74);

  opacity: 0;
  transform: translateY(6px);
  transition:
    opacity   .75s ease 1.55s,
    transform .75s cubic-bezier(.16, .86, .22, 1) 1.55s,
    color     .25s ease;
}

.kc--in .kc__enter,
.kc--forced .kc__enter {
  opacity: 1;
  transform: none;
}

.kc--in .kc__enter .kc__enter-text {
  display: inline-block;
  animation: kc-prompt 3.4s ease-in-out 2.3s infinite;
}

@keyframes kc-prompt {
  0%, 100% { opacity: .48; }
  50%      { opacity: 1; }
}

.kc__enter:hover { color: #ffffff; }
.kc__enter:focus-visible {
  outline: 1px solid rgba(34, 211, 166, .55);
  outline-offset: 4px;
  color: #ffffff;
}

/* On the way out the prompt is the FIRST thing to leave. It is an
   instruction, and the instruction has been followed. */
.kc--out .kc__enter {
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity .16s ease, transform .16s ease;
}
.kc--out .kc__enter .kc__enter-text { animation: none; }

/* ── Small screens ──────────────────────────────────────────── */
@media (max-width: 560px) {
  .kc__center { gap: 26px; }
  .kc__logo   { width: min(62vw, 230px); }
  .kc__enter  {
    font-size: var(--t-2xs);
    letter-spacing: .26em;
    text-indent: .26em;
  }
}

/* Short landscape phones: the gap is what breaks first, and there
   is no headroom left to spend on optical centring. */
@media (max-height: 460px) {
  .kc__center { gap: 18px; transform: none; }
  .kc__logo   { width: min(34vw, 200px); }
}

/* ── Reduced motion ─────────────────────────────────────────
   landing-cinema.js returns before mounting anything under
   reduced motion, so in practice none of this renders. These
   rules exist so that if the stage is ever forced up another way
   it presents as a still frame rather than animating at someone. */
@media (prefers-reduced-motion: reduce) {
  #kelnCinema.kc,
  .kc__vignette,
  .kc__logo,
  .kc__enter {
    transition-duration: .01ms !important;
    transition-delay: 0s !important;
    animation: none !important;
  }
  .kc__logo  { opacity: 1; transform: none; }
  .kc__enter { opacity: 1; transform: none; }
  .kc__vignette { opacity: 1; }
}
