/* ky-wall — video wallpaper backdrops for the hero and the footer.

   This is ky-lofi's engine grown a video source. Everything visual is
   deliberately the SAME vocabulary as ky/ky-lofi.css, because that treatment
   was already tuned against this palette:

     1. duotone shift toward the ground colour  (mix-blend-mode: color)
     2. a soft-light pass in the same ground
     3. a flat ground wash
     4. scrim gradients shaped to where the type actually lives
        (hero: name at the bottom, nav at the top;
         footer: menu / socials run nearly full height)

   ky-wall adds a FIFTH layer ky-lofi does not have: .ky-wall-boost, a flat
   ground plane whose opacity is SOLVED at runtime by ky-wall.js from the real
   decoded pixels of the current wallpaper, so that text contrast over the
   backdrop can never drop below 4.5:1 no matter what the video is showing.
   --kyw-boost is that solved number. It is the only value JS writes here.

   Every colour resolves from the site's own theme vars, so the ky-switch panel
   re-tints a video backdrop live exactly as it re-tints the lofi stills.
   Layers are absolute (zero CLS), aria-hidden and pointer-events:none. */

/* ---- zone host ----------------------------------------------------------- */
.ky-wall-host { position: relative; isolation: isolate; }

.ky-wall {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  contain: paint;
}

/* If ky-lofi is ALSO wired, ky-wall owns the zone and the still-image engine
   stands down. No edit to ky-lofi is needed for this; its own guard sees its
   layer still parented to the host and leaves it alone. */
html.ky-wall-live #ky-lofi-hero,
html.ky-wall-live #ky-lofi-foot { display: none !important; }

/* ---- the poster still ------------------------------------------------------
   Always painted FIRST and never removed: the video fades in on top of it, so
   a stalled or refused video can only ever reveal the poster, never a hole. */
/* --kyw-con is the SECOND solved variable (see ky-wall.js). It squeezes the
   wallpaper's own luminance range toward mid-tone, which the duotone then
   pushes to the theme ground. Every edge and shape in the picture survives;
   only its contrast range collapses. Solved before --kyw-boost is allowed to
   climb, because a flat ground wash heavy enough to fix a light theme on its
   own leaves nothing of the image at all. 1 = untouched (the usual case on
   dark themes). */
.ky-wall-still,
.ky-wall-vid { filter: contrast(var(--kyw-con, 1)); }

/* SCALE 1, AND IT MUST STAY 1.
   The poster and the video are the SAME framing of the same artwork: the
   video is object-fit:cover, which already crops to fill without scaling
   anything up. The poster shipped at scale(1.06) with a Ken Burns breath
   pushing it on to 1.13, which meant two visible faults on every single
   wallpaper:

     1. every plate opened 6% cropped in — the composition the wallpaper was
        chosen for was never what you actually saw, and on the two still
        plates (no video to hand over to) it kept creeping to 13%
     2. the handoff SNAPPED. The poster cross-fades to the video over 600ms,
        and for that whole fade two copies of the same frame sat on top of
        each other at different scales.

   That is the reported "it kind of zooms in for some reason". Matching the
   poster to the video's own framing fixes both at once, and costs nothing:
   the still is a decode-time placeholder, not a shot.

   The still plates are not left static by this — house-of-dreams and tuesday
   get real depth from the parallax stack at the bottom of this file, which is
   motion with a reason rather than a slow push on a flat picture. */
.ky-wall-still {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  opacity: 0;
  transform: scale(1);
  transition: opacity 0.5s linear;
  will-change: opacity;
}
.ky-wall-still.on { opacity: 1; }

/* ---- the video ------------------------------------------------------------- */
.ky-wall-vid {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.6s linear;
  will-change: opacity;
  pointer-events: none;
  background: transparent;
}
.ky-wall-vid.on { opacity: 1; }

/* ---- theme-aware treatment (identical values to ky-lofi) ------------------- */
.ky-wall-tint {
  position: absolute;
  inset: 0;
  background: var(--color-accent-200, #E3AC33);
  mix-blend-mode: color;
  opacity: .34;
}
.ky-wall-soft {
  position: absolute;
  inset: 0;
  background: var(--color-accent-200, #E3AC33);
  mix-blend-mode: soft-light;
  opacity: .22;
}
.ky-wall-wash {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--color-accent-200, #E3AC33) 28%, transparent);
}
.ky-wall.ky-wall-dark .ky-wall-wash {
  background: color-mix(in srgb, var(--color-accent-200, #E3AC33) 40%, transparent);
}

/* hero: the name sits at the bottom, the nav at the top */
.ky-wall-scrim-hero {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top,
      color-mix(in srgb, var(--color-accent-200, #E3AC33) 94%, transparent) 0%,
      color-mix(in srgb, var(--color-accent-200, #E3AC33) 72%, transparent) 26%,
      transparent 56%),
    linear-gradient(to bottom,
      color-mix(in srgb, var(--color-accent-200, #E3AC33) 88%, transparent) 0%,
      color-mix(in srgb, var(--color-accent-200, #E3AC33) 40%, transparent) 12%,
      transparent 30%);
}

/* footer: menu / socials / local-time run nearly full height */
.ky-wall-scrim-foot {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom,
      color-mix(in srgb, var(--color-accent-200, #E3AC33) 90%, transparent) 0%,
      color-mix(in srgb, var(--color-accent-200, #E3AC33) 62%, transparent) 40%,
      color-mix(in srgb, var(--color-accent-200, #E3AC33) 30%, transparent) 78%,
      color-mix(in srgb, var(--color-accent-200, #E3AC33) 72%, transparent) 100%),
    linear-gradient(to right,
      color-mix(in srgb, var(--color-accent-200, #E3AC33) 55%, transparent) 0%,
      transparent 60%);
}

/* ---- the solved legibility floor --------------------------------------------
   Flat, full-coverage, the page's own paper. ky-wall.js samples the live
   wallpaper into a 64x36 canvas, models this exact compositing stack, and
   binary-searches --kyw-boost for the smallest value that keeps the worst
   pixel behind the type at >= 4.8:1 against --color-foreground. Flat (rather
   than gradient-shaped) is deliberate: it makes the model exact and always
   solvable, and it means a wallpaper that needs a large boost visibly washes
   out in the dev panel readout instead of silently failing contrast. */
.ky-wall-boost {
  position: absolute;
  inset: 0;
  background: var(--color-accent-200, #E3AC33);
  opacity: var(--kyw-boost, 0);
  transition: opacity 0.8s linear;
}

/* ---- reduced transparency ----------------------------------------------------
   Drop both blend passes (they are the "transparency" effect here) and put a
   hard floor under the ground plane. JS reads the same query and raises its
   solver floor to match, so the measured contrast stays honest. */
@media (prefers-reduced-transparency: reduce) {
  .ky-wall-tint,
  .ky-wall-soft { display: none; }
  .ky-wall-boost { opacity: max(var(--kyw-boost, 0), 0.42); }
}

/* ---- reduced motion ----------------------------------------------------------
   JS never fetches a video at all under this query; this is the belt-and-braces
   half. The poster still remains, static: a photographic backdrop is not
   motion, and removing it would change the layout's colour balance. */
@media (prefers-reduced-motion: reduce) {
  .ky-wall .ky-wall-still,
  .ky-wall .ky-wall-still.on {
    animation: none !important;
    transition: none !important;
    transform: scale(1) !important;
  }
  .ky-wall-vid { display: none !important; }
  .ky-wall-boost { transition: none; }
}

/* ---- the click flood --------------------------------------------------------
   The site's own arrival language, re-aimed. The veil that covers the page on
   load is --color-accent-200; the theme switch pours that same paper through
   #goo (see ky-motion.css #ky-mo-flood). This is that overlay with its origin
   moved to the pointer: identical z-index, identical blob size, identical
   0.7s cubic-bezier(.51,.92,.24,1.15). Nothing new is invented — the swap just
   happens underneath it, which is why no text ever re-animates. */
#ky-wall-flood {
  position: fixed;
  inset: 0;
  z-index: 12000;
  pointer-events: none;
  overflow: hidden;
  contain: strict;
}
#ky-wall-flood .ky-wf-stage {
  position: absolute;
  inset: 0;
  filter: url(#goo);
  will-change: filter;
}
#ky-wall-flood .ky-wf-blob {
  position: absolute;
  top: 0;
  left: 0;
  width: 220px;
  height: 220px;
  border-radius: 50%;
  background: var(--color-accent-200, #E3AC33);
  transform: translate(-50%, -50%) scale(0);
  will-change: transform;
  transition: transform 0.7s cubic-bezier(0.51, 0.92, 0.24, 1.15);
}
@media (prefers-reduced-motion: reduce) {
  #ky-wall-flood { display: none; }
}

/* ---- parallax stack (still plates only) -----------------------------------
   House of Dreams and Tuesday came out of Wallpaper Engine as SCENES — layered
   flat artwork with no video — so without this they are the only two worlds
   that do not move. ky-wall.js fills .ky-wall-plx with one plate per layer,
   back to front, and drives --kyw-px/--kyw-py from the pointer.

   --kyw-depth is 0 for the backmost plate and 1 for the frontmost, so the
   travel is the DIFFERENCE between them; that difference is the whole depth
   cue. Slightly over-scaled so no layer can expose an edge at full travel.

   THE BLEED IS BUDGETED, NOT GUESSED — and it used to be paid TWICE.
   The owner's verdict was "2 of the wallpapers are zoomed in", and these two
   are precisely the two that carry this stack. The cause was that `inset` and
   `scale` are two independent ways of spending the same bleed, and both were
   spent at once: inset:-4% makes the layer 108% of the frame, then scale(1.05)
   multiplied that again — 1.08 x 1.05 = 1.134, measured live. Every layer was
   13.4% larger than the frame, so House of Dreams lost its cottage off the left
   edge and Tuesday lost the top of its window, while the other four worlds —
   which have no parallax stack — sat at 1.0 and looked correctly composed.

   What the bleed actually has to cover is one number: PLX_MAX in ky-wall.js,
   26px of front-layer travel (x), 0.6 of that on y. On a 1440x900 frame that
   is 1.8% and 1.7%. inset:-3% gives 43px per side across and 27px down, which
   clears full travel with room to spare, and scale is now doing nothing at all
   so it is gone. Net overscale 1.06 instead of 1.134. If PLX_MAX ever grows
   past ~40px, raise the inset here rather than reintroducing a scale. */
.ky-wall-plx {
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 0;
  transition: opacity .5s linear;
  pointer-events: none;
}
.ky-wall-plxon .ky-wall-plx { opacity: 1; }

/* the flat poster stays underneath as the base: it is what is on screen while
   the webps decode, and what remains if one of them never arrives */
.ky-wall-plx-l {
  position: absolute;
  inset: -3%;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  transform:
    translate3d(calc(var(--kyw-px, 0px) * var(--kyw-depth, 0)),
                calc(var(--kyw-py, 0px) * var(--kyw-depth, 0)), 0);
  will-change: transform;
}

/* the still's Ken Burns breath would fight the parallax — two different slow
   moves on the same picture read as drift, not depth */
.ky-wall.ky-wall-plxon .ky-wall-still { animation: none; }

/* No travel under reduced motion, so no bleed is needed either — but the inset
   still applies, so this pulls the layer back to exactly the frame instead of
   leaving it 6% zoomed for the one audience least served by a crop. */
@media (prefers-reduced-motion: reduce) {
  .ky-wall-plx-l { inset: 0; transform: none; transition: none; }
}

/* ── THE SKY DRIFTS ON ITS OWN ───────────────────────────── 2026-09-01 ──
   The owner's standing law is that a static wallpaper is a BUG, and the
   complaint that started this was "I still see an image, not the proper
   video". Restoring the room satisfies the design but would hand back a
   still — so the one layer that parallax cannot move is given its own
   motion instead.

   WHY THE FIRST LAYER SPECIFICALLY: buildPlx() writes --kyw-depth as
   i/(n-1), so with [sky, field, room] the sky is depth 0 and its parallax
   translate multiplies out to exactly zero. It is the only plane in the
   stack that is otherwise nailed down, which makes it the only one whose
   own drift cannot be mistaken for, or fight with, the depth cue. The room
   and the field keep moving with the pointer; the clouds move regardless.

   WHY A REGISTERED PROPERTY AND NOT AN ANIMATED TRANSFORM: an `animation`
   that sets `transform` REPLACES the base transform outright, which would
   silently delete the parallax translate on this layer. Animating a
   registered <length> and composing it inside the same translate3d keeps
   both, and keeps the whole thing on the compositor.

   COST: one extra composited layer already promoted by will-change, moving
   28px over 96s. No repaint, no layout, no new bytes. */
@property --kyw-drift {
  /* length-PERCENTAGE, not length. The travel has to stay inside the layer's
     own `inset: -3%` bleed at EVERY viewport width: 3% of 2560 is 77px but 3%
     of 1280 is only 38px, so a fixed px amplitude that looks right on a desktop
     walks the sky's right edge into frame on a laptop. A percentage resolves
     against the element's own border-box and therefore scales with the bleed
     it has to respect. */
  syntax: '<length-percentage>';
  inherits: false;
  initial-value: 0px;
}

/* AMPLITUDE, and why it is this number.
   The first cut was 28px over 96s. Measured live on the running page, that is
   ~0.6px/sec — and it was doing exactly what it was told: `--kyw-drift` read
   -0.93px after a second and a half. It was invisible, not broken. Sustained
   motion below roughly 1px/sec does not read as movement at all; it reads as a
   still image, which is the exact bug this was added to fix.
   2.2% of the layer = 2.33% of the container, against a 3% bleed, so the far
   edge still covers with room to spare at any width. Over a 22s half-cycle
   that is ~2.7px/sec average and ~4.3px/sec at the middle of the ease — slow
   enough to stay "unhurried", fast enough that the clouds visibly move against
   the fixed window mullions, which are the reference that makes it legible. */
@keyframes kyw-sky-drift {
  from { --kyw-drift: 0% }
  to   { --kyw-drift: -2.2% }
}

/* specificity 0,2,0 beats the base .ky-wall-plx-l (0,1,0), and the base
   translate is restated here rather than dropped, so depth still applies
   if a future manifest ever gives the sky a non-zero depth. 28px sits well
   inside the layer's own `inset: -3%` bleed, so no edge can be exposed. */
.ky-wall-plx-l:first-child {
  transform:
    translate3d(calc(var(--kyw-px, 0px) * var(--kyw-depth, 0) + var(--kyw-drift, 0px)),
                calc(var(--kyw-py, 0px) * var(--kyw-depth, 0)), 0);
  animation: kyw-sky-drift 44s ease-in-out infinite alternate;
}

/* The existing reduced-motion block below zeroes `transform` on every layer
   but predates this animation, and this selector is more specific than the
   one it uses — so the stop is restated here rather than assumed. */
@media (prefers-reduced-motion: reduce) {
  .ky-wall-plx-l:first-child { transform: none; animation: none; }
}
