/* ==========================================================================
   BOOKALLO — GLOBAL MOTION + DEPTH SYSTEM
   One motion language for the whole product. Loaded on EVERY reader page by
   scripts/build_dist.js, for the reason janet_premium.css is: a site where
   half the pages move and half do not is two products, and the visitor
   changes product by clicking a nav item.

   WHAT THIS IS BUILT ON, AND WHY IT IS NOT THREE.JS
   ------------------------------------------------
   This product is 60-odd static HTML pages of vanilla CSS and JS. There is no
   React, no bundler and no animation library anywhere in it - measured, not
   assumed. So there is no Framer Motion (no React to hook), no GSAP
   (IntersectionObserver and CSS transitions already do what this page needs),
   and no Three.js.

   The approved direction is a photographic composite: a dark destination
   frame, floating panels, an angled console, a receding rail. A WebGL plane
   carrying that same photograph costs ~150 KB, a context, a fallback path and
   a disposal contract to look identical to a transformed <img>. And the one
   thing WebGL would genuinely add - parallax INSIDE a photograph - needs a
   per-image depth map, which this product does not hold. Saying so is cheaper
   than shipping a scene that claims depth it does not have.

   RULES THIS SHEET KEEPS
   ----------------------
   - Only transform, opacity, filter, clip-path and box-shadow animate.
     Nothing that triggers layout is on a scroll or pointer path.
   - Nothing is hidden unless JS is present to reveal it. Every hiding rule is
     behind html.js, so a page with no JS is fully visible and indexable.
   - prefers-reduced-motion removes MOVEMENT, never content and never state.
   - No scroll hijacking. Scroll drives values; it never takes them over.
   - Logical properties throughout - this site is RTL first and LTR second, and
     a left here is a bug on one of the two.
   - Quality tiers are set on <html> by bk_motion.js. A rule that must not run
     on a phone is scoped, not hoped for.

   01 tokens              05 floating panels      09 nav + header
   02 stage + depth       06 reveals              10 skeleton + loading
   03 pointer camera      07 micro-interactions   11 quality tiers
   04 tilt cards          08 section transitions  12 reduced motion
   ========================================================================== */


/* == 01 - TOKENS ========================================================== */

:root {
  /* ---- Duration. Five tiers, and each one has a job. A product with two
     durations reads as either twitchy or slow; a product with nine has no
     rhythm at all. ---- */
  --bk-t-micro:     180ms;   /* buttons, icons, focus rings          */
  --bk-t-fast:      280ms;   /* tabs, chips, hover lifts             */
  --bk-t-normal:    450ms;   /* cards, panels, dropdowns             */
  --bk-t-cinematic: 900ms;   /* section reveals, rail travel         */
  --bk-t-intro:    1400ms;   /* the one first-load camera push       */

  /* ---- Easing. premium is the house curve: a fast start that settles
     without overshoot, which is what reads as weight rather than bounce.
     Nothing in this product springs back - a bounce is a toy. ---- */
  --bk-e-premium: cubic-bezier(.22, 1, .36, 1);
  --bk-e-smooth:  cubic-bezier(.16, 1, .3, 1);
  --bk-e-soft:    cubic-bezier(.25, .1, .25, 1);
  --bk-e-in:      cubic-bezier(.55, 0, 1, .45);

  /* ---- Depth, in PIXELS OF TRAVEL at full pointer deflection - not in Z.
     See the note above section 02 for why no large element in this system
     carries a 3D matrix. Near layers travel further than far ones, which is
     what reads as depth; the content plane travels zero, because type that
     slides under the pointer is type somebody is trying to read while it
     moves. ---- */
  --bk-d-bg:        7px;
  --bk-d-scenery:  13px;
  --bk-d-content:   0px;
  --bk-d-floating: 26px;
  --bk-d-fore:     38px;

  /* ---- Perspective, for the SMALL elements that may still use one: a card
     under the pointer, the console at rest. Those are individually
     transformed, individually composited, and have never blanked anything. ---- */
  --bk-persp:        1400px;
  --bk-persp-close:   900px;

  /* ---- Card tilt. Degrees, and deliberately small: the brief.s ceiling is
     2-6deg, and past that on a full-page surface is where nausea starts and
     where text stops being crisp. ---- */
  --bk-tilt-card-x: 5deg;
  --bk-tilt-card-y: 5deg;

  /* ---- Parallax coefficients: fraction of scroll distance a plane travels
     against the page. far barely moves; near moves most. ---- */
  --bk-px-far:  .08;
  --bk-px-mid:  .18;
  --bk-px-near: .32;
  --bk-px-ui:   .12;

  /* ---- Reveal geometry ---- */
  --bk-rise:    26px;
  --bk-rise-sm: 14px;
  --bk-blur:    12px;

  /* ---- Light. The champagne edge that makes a glass panel read as a lit
     object rather than a translucent rectangle. ---- */
  --bk-edge:  rgba(201, 162, 74, .34);
  --bk-edge-soft: rgba(201, 162, 74, .16);
  --bk-sheen: linear-gradient(105deg,
                rgba(255, 255, 255, 0) 38%,
                rgba(255, 255, 255, .13) 48%,
                rgba(255, 255, 255, 0) 58%);
}


/* == 02 - STAGE + DEPTH =================================================== */

/*
 * DEPTH IS 2D TRANSLATION, AND THERE IS NO STANDING will-change ANYWHERE.
 *
 * A camera pan across layers at different depths IS, to first order, a screen
 * translation proportional to depth. The shear it also produces is a fraction
 * of a degree at these amplitudes and nobody has ever seen it. So the runtime
 * writes two unitless numbers on the rig, every plane multiplies them by its
 * own depth, and the result is translate3d with a zero Z - which composites
 * correctly on any element, at any size, inside any ancestor, with no
 * preserve-3d group for a descendant three levels down to flatten.
 *
 * A shared 3D space was built first and torn out during a debugging session
 * whose premise turned out to be false (see note 1 in bk_hero.css - the
 * preview pane was screenshotting a correct page as black). It is not being
 * put back, because this shape is simpler and gives up nothing anyone can see.
 *
 * There is no standing `will-change` either. It is a hint the specification
 * says to apply sparingly and remove when the work is done; the compositor
 * promotes a layer on its own the moment a transform actually animates, which
 * is the only moment the hint was ever worth anything. A hundred permanently
 * promoted layers is a hundred layers the compositor keeps alive for nothing.
 */
.bk-stage { position: relative; }

/* The rig. It carries the INTRO - a flat lift and settle - and it is where the
   pointer camera's two numbers are written for the planes to inherit. */
.bk-cam {
  position: relative;
  transform: translate3d(0, var(--bk-cam-ty, 0px), 0) scale(var(--bk-cam-s, 1));
}

/*
 * A plane. --bk-mx / --bk-my are the pointer, -0.5..0.5, inherited from the
 * rig. --bk-depth is how far THIS layer travels at full deflection. --bk-py is
 * the scroll offset, written per element by the parallax pass.
 */
.bk-plane {
  transform: translate3d(
    calc(var(--bk-mx, 0) * var(--bk-depth, 0px)),
    calc(var(--bk-my, 0) * var(--bk-depth, 0px) + var(--bk-py, 0px)),
    0);
}
/*
 * Near layers travel further than far ones, which is what reads as depth. The
 * CONTENT plane deliberately travels ZERO: the headline and the search console
 * live on it, and type that slides under the pointer is type somebody is
 * trying to read while it moves.
 */
.bk-plane--bg      { --bk-depth:  7px; }
.bk-plane--scenery { --bk-depth: 13px; }
.bk-plane--content { --bk-depth:  0px; }
.bk-plane--floating{ --bk-depth: 26px; }
.bk-plane--fore    { --bk-depth: 38px; }

/* Scroll parallax. --bk-py is written by the runtime, once per frame, from a
   single shared rAF - never from a scroll handler. An element that is ALSO a
   plane already carries --bk-py inside the plane transform, so this rule must
   not apply there: two transform declarations on one element is the later one
   winning and the pointer parallax silently disappearing. */
.bk-px:not(.bk-plane) { transform: translate3d(0, var(--bk-py, 0px), 0); }


/* == 03 - POINTER CAMERA ================================================== */

/*
 * Only a device that actually has a pointer gets a pointer camera. hover:hover
 * is the honest query: a phone reports a coarse pointer and simulating a
 * cursor there is the gimmick the brief rules out.
 */
@media (hover: none), (pointer: coarse) {
  .bk-plane { --bk-mx: 0; --bk-my: 0; }
  .bk-tilt { transform: none !important; }
}


/* == 04 - TILT CARDS ====================================================== */

.bk-tilt {
  transform-style: preserve-3d;
  transition: transform var(--bk-t-fast) var(--bk-e-premium),
              box-shadow var(--bk-t-fast) var(--bk-e-premium);
  transform:
    perspective(var(--bk-persp-close))
    rotateX(var(--bk-rx, 0deg))
    rotateY(var(--bk-ry, 0deg))
    translate3d(0, var(--bk-lift, 0px), var(--bk-tz, 0px));
}
/* While the pointer is inside, the transition is off: a 280ms ease on a value
   that changes every frame is a lag, not a smoothing. The runtime lerps. */
.bk-tilt.is-tracking { transition: box-shadow var(--bk-t-fast) var(--bk-e-premium); }

.bk-tilt__sheen {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  background: var(--bk-sheen);
  background-size: 220% 100%;
  background-position: var(--bk-sheen-x, 100%) 0;
  transition: opacity var(--bk-t-fast) var(--bk-e-premium);
  /* NO mix-blend-mode. A blend mode on any descendant of a preserve-3d
     container forces that container into a flattened group, and Chromium
     composited the group as solid black: one `mix-blend-mode: screen` on a
     sheen inside the hero rig blanked the ENTIRE hero - photograph, headline,
     console, panels. Every element still measured correctly (opacity 1, real
     rects, elementFromPoint returning the h1), so only a screenshot could find
     it. A white gradient at low alpha over a dark ground looks the same. */
}
.bk-tilt:hover .bk-tilt__sheen,
.bk-tilt:focus-within .bk-tilt__sheen { opacity: 1; }


/* == 05 - FLOATING PANELS ================================================= */

/*
 * The idle float is a CSS keyframe, not a JS loop. It composites on its own
 * layer, costs the main thread nothing, and pauses for free when the tab is
 * hidden. The runtime only ever adds the pointer tilt on top.
 */
@keyframes bk-float {
  0%, 100% { translate: 0 0; rotate: 0deg; }
  50%      { translate: 0 calc(-1 * var(--bk-float-y, 11px)); rotate: var(--bk-float-r, .35deg); }
}
.bk-float {
  animation: bk-float var(--bk-float-dur, 8s) var(--bk-e-soft) infinite;
  animation-delay: var(--bk-float-delay, 0s);
}
/* Offscreen or hidden: stop. An ambient loop running behind a results list is
   GPU spent on something nobody is looking at. */
.bk-float[data-bk-paused="1"] { animation-play-state: paused; }


/* == 06 - REVEALS ========================================================= */

/*
 * Every reveal is behind html.js. With JS off, none of these rules exist and
 * the page is simply visible - which is also what a crawler sees.
 */
html.js [data-bk-reveal] {
  opacity: 0;
  transition:
    opacity var(--bk-t-cinematic) var(--bk-e-smooth) var(--bk-delay, 0ms),
    transform var(--bk-t-cinematic) var(--bk-e-smooth) var(--bk-delay, 0ms),
    clip-path var(--bk-t-cinematic) var(--bk-e-smooth) var(--bk-delay, 0ms),
    filter var(--bk-t-cinematic) var(--bk-e-smooth) var(--bk-delay, 0ms);
}
html.js [data-bk-reveal="rise"]   { transform: translate3d(0, var(--bk-rise), 0); }
html.js [data-bk-reveal="depth"]  { transform: translate3d(0, var(--bk-rise-sm), -70px); }
html.js [data-bk-reveal="near"]   { transform: translate3d(0, 0, 90px); filter: blur(6px); }
html.js [data-bk-reveal="mask"]   { clip-path: inset(0 0 100% 0); transform: scale(1.04); }
html.js [data-bk-reveal="mask-i"] { clip-path: inset(0 100% 0 0); }
html.js [data-bk-reveal="soft"]   { filter: blur(var(--bk-blur)); transform: translate3d(0, var(--bk-rise-sm), 0); }
html.js [data-bk-reveal="slide"]  { transform: translate3d(var(--bk-slide, 40px), 0, 0); }

html.js [data-bk-reveal].is-in {
  opacity: 1;
  transform: none;
  filter: none;
  clip-path: inset(0 0 0 0);
}
/* Once a reveal has played, take the hint off. will-change left on a hundred
   elements is a hundred layers the compositor keeps alive for nothing. */
html.js [data-bk-reveal].is-done { transition: none; }

/* A light sweep that crosses a revealed surface exactly once. */
@keyframes bk-sweep {
  from { background-position: 140% 0; opacity: 0; }
  20%  { opacity: 1; }
  to   { background-position: -40% 0; opacity: 0; }
}
.bk-sweep { position: relative; overflow: hidden; }
.bk-sweep::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--bk-sheen);
  background-size: 220% 100%;
  opacity: 0;
}
html.js .bk-sweep.is-in::after { animation: bk-sweep 1200ms var(--bk-e-smooth) 260ms 1; }


/* == 07 - MICRO-INTERACTIONS ============================================== */

/*
 * One grammar, applied by CLASS rather than by element, so a link that acts as
 * a button gets it and a <button> that is really a tab does not.
 */
.bk-press {
  transition:
    transform var(--bk-t-micro) var(--bk-e-premium),
    box-shadow var(--bk-t-micro) var(--bk-e-premium),
    background-color var(--bk-t-micro) var(--bk-e-premium),
    border-color var(--bk-t-micro) var(--bk-e-premium);
}
.bk-press:hover      { transform: translate3d(0, -2px, 0) scale(1.008); }
.bk-press:active     { transform: translate3d(0,  1px, 0) scale(.985); transition-duration: 90ms; }
.bk-press:focus-visible { outline: 2px solid var(--jp-gold, #C9A24A); outline-offset: 3px; }

/* An image inside a card: the picture moves, the type does not. Text that
   scales with its container is text that goes soft on hover. */
.bk-zoom { overflow: hidden; }
.bk-zoom > img,
.bk-zoom > picture > img {
  transition: transform var(--bk-t-normal) var(--bk-e-premium);
}
.bk-zoom:hover > img,
.bk-zoom:hover > picture > img,
.bk-zoom:focus-within > img { transform: scale(1.045); }

/* A nav underline that GLIDES between items instead of appearing under each.
   The bar is one element; the runtime moves it. */
.bk-underline {
  position: absolute;
  block-size: 2px;
  inset-block-end: -6px;
  inset-inline-start: 0;
  inline-size: var(--bk-ul-w, 0px);
  transform: translate3d(var(--bk-ul-x, 0px), 0, 0);
  background: var(--jp-gold, #C9A24A);
  border-radius: 2px;
  opacity: var(--bk-ul-o, 0);
  transition:
    transform var(--bk-t-fast) var(--bk-e-premium),
    inline-size var(--bk-t-fast) var(--bk-e-premium),
    opacity var(--bk-t-micro) var(--bk-e-premium);
  pointer-events: none;
}

/* Icons: a hint of movement, and only where it means something. */
.bk-ico { transition: transform var(--bk-t-fast) var(--bk-e-premium); }
.bk-press:hover .bk-ico { transform: translate3d(2px, 0, 0) rotate(-4deg); }

/* A field that lights rather than gains a loud ring. */
.bk-field { transition: border-color var(--bk-t-fast) var(--bk-e-premium),
                        box-shadow var(--bk-t-fast) var(--bk-e-premium),
                        background-color var(--bk-t-fast) var(--bk-e-premium); }
.bk-field:focus-within {
  border-color: var(--bk-edge);
  box-shadow: 0 0 0 1px var(--bk-edge), 0 8px 26px -14px rgba(201, 162, 74, .55);
}

/* Favourite: ONE pulse. An infinite pulse on a saved item is an alarm. */
@keyframes bk-pulse { 0% { scale: 1; } 42% { scale: 1.22; } 100% { scale: 1; } }
.bk-pulse-once { animation: bk-pulse 420ms var(--bk-e-premium) 1; }

/* A price that changed: a short champagne wash, then back. */
@keyframes bk-priceflash {
  0%   { color: var(--jp-gold, #C9A24A); }
  100% { color: inherit; }
}
.bk-price-changed { animation: bk-priceflash 900ms var(--bk-e-smooth) 1; }


/* == 08 - SECTION TRANSITIONS ============================================= */

/*
 * A section is not a box that fades in. It arrives with its own rhythm, and
 * the rhythms alternate down the page so the reader is never shown the same
 * gesture twice in a row.
 */
.bk-sec { position: relative; }

/* The hairline that draws itself as a section enters - the "route line". */
.bk-route {
  position: relative;
  block-size: 1px;
  overflow: hidden;
  background: linear-gradient(90deg,
    rgba(201, 162, 74, 0) 0%, var(--bk-edge) 22%,
    var(--bk-edge) 78%, rgba(201, 162, 74, 0) 100%);
  transform-origin: center;
  scale: 0 1;
  transition: scale var(--bk-t-cinematic) var(--bk-e-smooth);
}
html.js .bk-route.is-in { scale: 1 1; }
/* The travelling glow that runs the line once. */
.bk-route::after {
  content: "";
  position: absolute;
  inset-block: -3px;
  inset-inline-start: 0;
  inline-size: 74px;
  border-radius: 999px;
  background: radial-gradient(closest-side, rgba(201, 162, 74, .85), rgba(201, 162, 74, 0));
  opacity: 0;
}
@keyframes bk-route-run {
  from { transform: translateX(-90px); opacity: 0; }
  16%  { opacity: 1; }
  84%  { opacity: 1; }
  to   { transform: translateX(1400px); opacity: 0; }
}
html.js .bk-route.is-in::after { animation: bk-route-run 1500ms var(--bk-e-soft) 220ms 1; }


/* == 09 - NAV + HEADER ==================================================== */

/*
 * The header starts transparent over the hero photograph and takes on ground
 * as the page leaves it. bk-hd--solid is set by the runtime from the SAME rAF
 * that drives parallax - a header with its own scroll listener is a second
 * authority reading the same number.
 */
.hd {
  transition:
    background-color var(--bk-t-normal) var(--bk-e-premium),
    border-color var(--bk-t-normal) var(--bk-e-premium),
    box-shadow var(--bk-t-normal) var(--bk-e-premium);
}
html.js .hd.bk-hd--over {
  background: linear-gradient(180deg, rgba(6, 7, 9, .74), rgba(6, 7, 9, .18) 62%, rgba(6, 7, 9, 0));
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-block-end-color: transparent;
  box-shadow: none;
}
html.js .hd.bk-hd--solid {
  background: rgba(10, 11, 13, .88);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  backdrop-filter: blur(16px) saturate(140%);
  box-shadow: 0 1px 0 rgba(245, 243, 239, .08), 0 18px 40px -30px rgba(0, 0, 0, .9);
}
.hd-nav { position: relative; }
.hd-nav a {
  position: relative;
  transition: color var(--bk-t-fast) var(--bk-e-premium),
              transform var(--bk-t-fast) var(--bk-e-premium);
}
.hd-nav a:hover { transform: translate3d(0, -1px, 0); }


/* == 10 - SKELETON + LOADING ============================================== */

/*
 * A shimmer over a shape the size of the thing that is coming. Never a
 * percentage: this product cannot know how far along a provider is, and a
 * progress bar that invents one is the same class of lie as an invented price.
 */
@keyframes bk-shimmer { from { background-position: 180% 0; } to { background-position: -80% 0; } }
.bk-skel {
  background:
    linear-gradient(90deg,
      rgba(245, 243, 239, .05) 0%,
      rgba(245, 243, 239, .11) 42%,
      rgba(245, 243, 239, .05) 84%);
  background-size: 260% 100%;
  animation: bk-shimmer 1600ms linear infinite;
  border-radius: var(--jp-r-sm, 6px);
  color: transparent;
}


/* == 11 - QUALITY TIERS =================================================== */

/*
 * Set on <html> by the runtime from device memory, DPR, viewport and the
 * frame budget it actually measures. A tier is not a guess about the device
 * name; it is a decision about how much this machine has been seen to afford.
 */
html.bk-q-med  { --bk-tilt-x: 2.5deg; --bk-tilt-y: 3.5deg; --bk-blur: 8px; }
html.bk-q-med  .bk-sweep::after { display: none; }

html.bk-q-low  { --bk-tilt-x: 0deg;   --bk-tilt-y: 0deg;   --bk-blur: 0px; }
html.bk-q-low  .bk-float { animation: none; }
html.bk-q-low  .bk-cam   { transform: none !important; }
html.bk-q-low  .bk-sweep::after { display: none; }
html.bk-q-low  .bk-route::after { animation: none; }
html.bk-q-low  [data-bk-reveal] { transition-duration: 320ms; }

/* Mobile keeps the language and loses the amplitude - the brief's model, and
   the honest one: a phone paints the same pixels with a fraction of the power
   budget and no pointer to drive a camera with. */
@media (max-width: 720px) {
  :root {
    --bk-rise: 18px;
    --bk-rise-sm: 10px;
    --bk-blur: 8px;
    --bk-px-far: .05; --bk-px-mid: .10; --bk-px-near: .16; --bk-px-ui: .06;
    --bk-t-cinematic: 640ms;
    --bk-persp: 1100px;
  }
  .bk-plane--bg, .bk-plane--scenery, .bk-plane--floating, .bk-plane--fore { scale: 1; }
}


/* == 12 - REDUCED MOTION ================================================== */

/*
 * MOVEMENT goes. Content, state and every affordance stay. A reduced-motion
 * visitor still sees the hover, the focus ring, the selected tab and the
 * revealed section - they simply arrive rather than travel.
 */
@media (prefers-reduced-motion: reduce) {
  html.js [data-bk-reveal] {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
    transition: none !important;
  }
  .bk-float,
  .bk-sweep::after,
  .bk-route::after,
  .bk-skel,
  .bk-pulse-once,
  .bk-price-changed { animation: none !important; }
  .bk-cam, .bk-tilt, .bk-px { transform: none !important; }
  .bk-plane { --bk-mx: 0 !important; --bk-my: 0 !important; --bk-py: 0px !important; }
  .bk-route { scale: 1 1 !important; transition: none !important; }
  .bk-press:hover, .bk-press:active { transform: none; }
  .bk-zoom:hover > img, .bk-zoom:hover > picture > img { transform: none; }
  .bk-underline { transition: opacity var(--bk-t-micro) linear; }
  .bk-tilt__sheen { display: none; }
  *, *::before, *::after { scroll-behavior: auto !important; }
}
