/* ============================================================
   Blackbox Command Center, motion.css
   Companion stylesheet for js/motion.js (global BlackboxMotion). Every
   color here is derived from the tokens already defined in css/main.css
   (var(--ink), var(--card), var(--border), ...) via color-mix(), so every
   effect below reads correctly in both the light and dark palette without
   a single hardcoded hex value. Durations/eases reuse this app's own
   --dur*/--ease* tokens so the new effects read as part of the existing
   "Motion pass" rather than a bolt-on.

   Every @keyframes use below is wrapped in
   @media (prefers-reduced-motion: no-preference), matching the existing
   convention in main.css (.skel, .border-beam) — belt-and-suspenders on
   top of js/motion.js's own JS-level reduced-motion checks and this app's
   blanket `*{transition:none;animation:none}` override under `reduce`.
   ============================================================ */

/* ---------- split: per-word stagger reveal ----------
   .bb-split-ready is added once, at wire time; the words themselves carry
   their own animation-delay inline. Fill-mode 'both' means a word sits at
   its "from" state for the whole of its delay, so a five-word subtitle
   doesn't flash fully-visible before cascading in. Replays for free every
   time an ancestor .screen goes display:none -> block (browsers restart a
   running keyframe animation on that transition) — the exact same
   mechanic this app's own .page-title / .hero h1 span already rely on. */
.bb-split-word { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  .bb-split-ready .bb-split-word {
    animation: bb-motion-split-in var(--dur-slower) var(--ease) both;
  }
}
@keyframes bb-motion-split-in {
  from { opacity: 0; transform: translateY(6px); filter: blur(2px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}

/* ---------- reveal: stagger a container's direct children in, once ----------
   .bb-reveal-child is added to each existing child at the moment
   js/motion.js decides to reveal (see maybeReveal's "fires once, ever, per
   container" guard) — never re-added on a later poll refresh, so this
   never replays into a flicker. */
@media (prefers-reduced-motion: no-preference) {
  .bb-reveal-child {
    animation: bb-motion-reveal-in var(--dur-slow) var(--ease) both;
  }
}
@keyframes bb-motion-reveal-in {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

/* ---------- magnetic / dock ----------
   Both are pure transform, driven inline by js/motion.js on
   mousemove/mouseleave — nothing to define here beyond a stable
   transform-origin so the scale in "dock" grows from the item's own
   center rather than its box corner. */
[data-motion~="magnetic"] { display: inline-block; }
.side-nav[data-motion~="dock"] > * { transform-origin: center; }

/* ---------- tilt: pointer-tracked 3D card tilt + light sheen ----------
   The sheen is a radial highlight that follows the pointer via
   --bb-sheen-x/-y (set on mousemove) and fades in/out via --bb-sheen-o.
   It's deliberately built from var(--ink) rather than white/black: in the
   light theme --ink is a dark navy, so the sheen reads as a soft ambient
   shadow deepening toward the cursor (a highlight would be invisible on a
   white card); in the dark theme --ink is near-white, so the exact same
   rule reads as a gentle light bloom — both are "sheen," just the theme-
   appropriate version of it, entirely for free from one token. */
[data-motion~="tilt"] {
  position: relative;
  transition: box-shadow 0.16s ease;
}
[data-motion~="tilt"]::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(circle 200px at var(--bb-sheen-x, 50%) var(--bb-sheen-y, 50%), color-mix(in srgb, var(--ink) 10%, transparent) 0%, transparent 70%);
  opacity: var(--bb-sheen-o, 0);
  pointer-events: none;
  z-index: 1;
}
@media (prefers-reduced-motion: no-preference) {
  [data-motion~="tilt"]::after { transition: opacity var(--dur) var(--ease); }
}

/* ---------- glass: frosted surface ----------
   Two archetypes share the same attribute: a full-bleed scrim (the mobile
   nav backdrop, task drawer / lightbox / tools-gate backdrops — already
   translucent via their own main.css background rule, this only adds the
   blur) and a floating surface (the sticky mobile topbar, which gets a
   translucent var(--card) tint derived via color-mix so content scrolling
   underneath softly shows through). */
[data-motion~="glass"] {
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
}
.mobile-topbar[data-motion~="glass"] {
  background: color-mix(in srgb, var(--card) 78%, transparent);
}

/* ---------- repel: words gently pushed away from the pointer ---------- */
.bb-repel-word { display: inline-block; will-change: transform; }

/* ---------- marquee: seamless infinite scroll strip ---------- */
.bb-marquee { overflow: hidden; }
.bb-marquee-track { display: flex; width: max-content; }
.bb-marquee-group { display: flex; align-items: center; gap: 32px; flex-shrink: 0; padding-right: 32px; }
@media (prefers-reduced-motion: no-preference) {
  .bb-marquee-track {
    animation: bb-motion-marquee var(--bb-marquee-duration, 20s) linear infinite;
  }
  .bb-marquee:hover .bb-marquee-track,
  .bb-motion-paused .bb-marquee-track {
    animation-play-state: paused;
  }
}
@keyframes bb-motion-marquee {
  to { transform: translateX(-50%); }
}

/* ---------- loop: cycle phrases with a blurred vertical crossfade ---------- */
.bb-loop { display: inline-block; overflow: hidden; vertical-align: bottom; }
.bb-loop-slot { display: inline-block; opacity: 1; filter: blur(0); transform: none; }
@media (prefers-reduced-motion: no-preference) {
  .bb-loop-slot {
    transition: opacity var(--dur-slow) var(--ease), filter var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease);
  }
}
.bb-loop-slot.bb-loop-out { opacity: 0; filter: blur(4px); transform: translateY(-8px); }
.bb-loop-slot.bb-loop-in { opacity: 0; filter: blur(4px); transform: translateY(8px); }

/* ============================================================
   BEAM — the animated brand-gradient border ring, one parameterised primitive

   WHY THIS EXISTS. The app was saying the same thing two different ways: one
   hardcoded animated ring (.border-beam in main.css — a single 7s sweep, one
   thickness, one opacity, one usage site) sitting a few pixels away from
   surfaces that hand-painted a STATIC var(--grad) keyline with the
   padding-box/border-box trick (.nav-item.active at main.css:519,
   .rail-tint.green at main.css:906). On the Live CCU widget both were on
   screen at once — an animated ring on the card and a competing static ring
   on the tint 15px inside it. This is the one generalised version of that
   idea; main.css's .border-beam has been deleted in favour of it.

   THE TECHNIQUE, so it can be rebuilt from scratch if it ever needs to be: a
   conic-gradient painted on a pseudo-element covering the host, then masked
   down to an N-px ring by compositing two stacked masks with `exclude`
   (outer fill minus fill-inset-by-N = the ring). The sweep is driven by an
   @property-registered <angle> because a plain custom property is not
   interpolatable and would step instead of sweep. Same for the pulse's scale
   and drift.

   PURE CSS, DELIBERATELY. There is no JS anywhere in this block: no
   AUTO_SELECTORS entry, no wire function, no data-motion-wired-* flag. That
   is not laziness, it is the requirement — the app replaces large chunks of
   markup wholesale via innerHTML every ~8s poll, and a CSS-only effect
   survives that for free. It also means the two surfaces that live OUTSIDE
   the #app MutationObserver scope would be safe hosts too.

   ZERO LAYOUT IMPACT. The ring is a pseudo-element, never a real border, and
   nothing here sets overflow/width/height/padding on the host. Hosts that
   already paint a static var(--grad) keyline KEEP it: the beam defaults to
   inset 0 with a 1.5px stroke so it lands exactly on top of that existing
   1.5px transparent border rather than beside it, and the keyline underneath
   becomes the reduced-motion fallback for free.

   Knobs — all optional, set them on the host:
     --bb-beam-w         ring thickness                      1.5px
     --bb-beam-inset     inset from the host's border box    0px
     --bb-beam-dur       one full revolution                 7s
     --bb-beam-pulse     one breath                          3.4s
     --bb-beam-strength  master intensity, 0..1              1
     --bb-beam-tail      where the lit arc starts            60%
     --bb-beam-c1/c2/c3  the three lit stops                 brand red/blue/green
     --bb-beam-feather   how far the inner pulse reaches in  42%
     --bb-beam-bloom     outer-bloom blur radius             7px
     --bb-beam-ring-lo/-ring-hi, --bb-beam-glow-lo/-glow-hi  breathe range

   Variants — mutually exclusive, both own ::after:
     .bb-beam--inner   the colourful pulse INSIDE the ring
     .bb-beam--outer   the same light blooming OUTSIDE the box

   The literal brand stops below are the one sanctioned hex exception in this
   file (see the header): --grad is a linear-gradient token and
   conic-gradient() cannot consume it, and the brand triad is deliberately
   theme-invariant anyway (main.css:40-44), so it needs no light/dark split.
   ============================================================ */

/* Registered so they interpolate. --bb-angle keeps its original name because
   it is the same property main.css's .border-beam used; nothing else in the
   app referenced it, so this is a move, not a fork. */
@property --bb-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
@property --bb-glow-s { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --bb-glow-d { syntax: '<percentage>'; inherits: false; initial-value: 0%; }

/* The host. Safe to sit on anything permanently — it paints nothing until the
   element is lit, so all ten .nav-item buttons can carry the class and only
   the active one glows. `isolation` is what lets the inner pulse sit at
   z-index -1: inside the host's own stacking context a negative-z-index
   child paints ABOVE the host's background but BELOW its text, so a colour
   wash can back-light a nav label without ever washing over it. */
.bb-beam {
  position: relative;
  isolation: isolate;
  /* Symmetric in-out curve. The app's --ease is an out-curve, right for a
     thing arriving, wrong for a thing breathing — a breath has to decelerate
     into both ends or it reads as a blink. */
  --bb-beam-sine: cubic-bezier(0.37, 0, 0.63, 1);
  --bb-beam-ring-lo: 0.30;
  --bb-beam-ring-hi: 0.95;
  --bb-beam-glow-lo: 0.34;
  --bb-beam-glow-hi: 0.90;
}

/* LIT STATE. `.is-on` for surfaces that are always lit; `.active` because the
   app's primary target — .nav-item — already has a lit class that js/app.js
   toggles (js/app.js:122-124). Reading the app's own class is what keeps this
   JS-free: app.js goes on doing exactly what it did and knows nothing about
   the beam. */
.bb-beam.is-on::before,
.bb-beam.active::before {
  content: '';
  position: absolute;
  inset: var(--bb-beam-inset, 0px);
  border-radius: inherit;
  padding: var(--bb-beam-w, 1.5px);
  pointer-events: none;
  z-index: 2;
  --bb-beam-o-lo: var(--bb-beam-ring-lo);
  --bb-beam-o-hi: var(--bb-beam-ring-hi);
  opacity: calc(var(--bb-beam-o-hi) * var(--bb-beam-strength, 1));
  background: conic-gradient(
    from var(--bb-angle),
    transparent 0%,
    transparent var(--bb-beam-tail, 60%),
    var(--bb-beam-c1, #d95550) 72%,
    var(--bb-beam-c2, #4a6ad9) 82%,
    var(--bb-beam-c3, #2f9e68) 92%,
    transparent 100%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
}

/* INNER PULSE — the founder's ask, border-beam's "pulse-inner / colorful".
   Five soft colour blobs anchored just OUTSIDE each edge so their light
   enters through the border, then a two-axis feather mask so the glow only
   ever lives within --bb-beam-feather of an edge and the middle of the
   surface stays clean. Every size and offset is a PERCENTAGE, not the px the
   original used, so one declaration fits a 36px-tall nav item and a 120px
   rail widget without a per-host size table. */
.bb-beam--inner.is-on::after,
.bb-beam--inner.active::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: -1;
  --bb-beam-o-lo: var(--bb-beam-glow-lo);
  --bb-beam-o-hi: var(--bb-beam-glow-hi);
  opacity: calc(var(--bb-beam-o-hi) * var(--bb-beam-strength, 1));
  background:
    radial-gradient(ellipse calc(30% * var(--bb-glow-s)) calc(72% * var(--bb-glow-s)) at calc(26% + var(--bb-glow-d)) -10%,
      color-mix(in srgb, var(--bb-beam-c1, #d95550) 62%, transparent), transparent 72%),
    radial-gradient(ellipse calc(26% * var(--bb-glow-s)) calc(66% * var(--bb-glow-s)) at calc(84% - var(--bb-glow-d)) -8%,
      color-mix(in srgb, var(--bb-beam-c2, #4a6ad9) 58%, transparent), transparent 72%),
    radial-gradient(ellipse calc(20% * var(--bb-glow-s)) calc(90% * var(--bb-glow-s)) at -6% calc(64% + var(--bb-glow-d)),
      color-mix(in srgb, var(--bb-beam-c3, #2f9e68) 60%, transparent), transparent 72%),
    radial-gradient(ellipse calc(46% * var(--bb-glow-s)) calc(62% * var(--bb-glow-s)) at calc(58% - var(--bb-glow-d)) 108%,
      color-mix(in srgb, var(--bb-beam-c2, #4a6ad9) 55%, transparent), transparent 72%),
    radial-gradient(ellipse calc(22% * var(--bb-glow-s)) calc(78% * var(--bb-glow-s)) at 106% calc(32% - var(--bb-glow-d)),
      color-mix(in srgb, var(--bb-beam-c1, #d95550) 52%, transparent), transparent 72%);
  -webkit-mask-image:
    linear-gradient(#000, transparent var(--bb-beam-feather, 42%), transparent calc(100% - var(--bb-beam-feather, 42%)), #000),
    linear-gradient(to right, #000, transparent var(--bb-beam-feather, 42%), transparent calc(100% - var(--bb-beam-feather, 42%)), #000);
  -webkit-mask-composite: source-over;
  mask-image:
    linear-gradient(#000, transparent var(--bb-beam-feather, 42%), transparent calc(100% - var(--bb-beam-feather, 42%)), #000),
    linear-gradient(to right, #000, transparent var(--bb-beam-feather, 42%), transparent calc(100% - var(--bb-beam-feather, 42%)), #000);
  mask-composite: add;
}

/* OUTER PULSE — the same ring, blurred, sitting behind the host. Only the
   part that spills past the host's own opaque background is visible, which
   is exactly what makes it read as a bloom rather than a second ring. Host
   must therefore be opaque and must not clip: nothing here sets
   overflow:hidden, on purpose. */
.bb-beam--outer.is-on::after,
.bb-beam--outer.active::after {
  content: '';
  position: absolute;
  inset: var(--bb-beam-inset, 0px);
  border-radius: inherit;
  padding: calc(var(--bb-beam-w, 1.5px) * 2);
  pointer-events: none;
  z-index: -1;
  filter: blur(var(--bb-beam-bloom, 7px));
  --bb-beam-o-lo: var(--bb-beam-glow-lo);
  --bb-beam-o-hi: var(--bb-beam-glow-hi);
  opacity: calc(var(--bb-beam-o-hi) * var(--bb-beam-strength, 1));
  background: conic-gradient(
    from var(--bb-angle),
    transparent 0%,
    transparent var(--bb-beam-tail, 60%),
    var(--bb-beam-c1, #d95550) 72%,
    var(--bb-beam-c2, #4a6ad9) 82%,
    var(--bb-beam-c3, #2f9e68) 92%,
    transparent 100%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
}

/* The motion itself. Three periods that do not divide into each other (1 :
   1.53 : 2.27) so the pulse never visibly loops — the original package got
   the same effect from 17 rAF-driven oscillators; three coprime-ish CSS
   periods plus the sweep buy most of that life for none of the JS. */
@media (prefers-reduced-motion: no-preference) {
  .bb-beam.is-on::before,
  .bb-beam.active::before {
    animation:
      bb-beam-sweep var(--bb-beam-dur, 7s) linear infinite,
      bb-beam-breathe var(--bb-beam-pulse, 3.4s) var(--bb-beam-sine) infinite;
  }
  .bb-beam--inner.is-on::after,
  .bb-beam--inner.active::after {
    animation:
      bb-beam-breathe var(--bb-beam-pulse, 3.4s) var(--bb-beam-sine) infinite,
      bb-beam-swell calc(var(--bb-beam-pulse, 3.4s) * 1.53) var(--bb-beam-sine) infinite,
      bb-beam-drift calc(var(--bb-beam-pulse, 3.4s) * 2.27) var(--bb-beam-sine) infinite;
  }
  .bb-beam--outer.is-on::after,
  .bb-beam--outer.active::after {
    animation:
      bb-beam-sweep var(--bb-beam-dur, 7s) linear infinite,
      bb-beam-breathe var(--bb-beam-pulse, 3.4s) var(--bb-beam-sine) infinite;
  }
}

@keyframes bb-beam-sweep { to { --bb-angle: 360deg; } }
@keyframes bb-beam-breathe {
  0%, 100% { opacity: calc(var(--bb-beam-o-lo) * var(--bb-beam-strength, 1)); }
  50%      { opacity: calc(var(--bb-beam-o-hi) * var(--bb-beam-strength, 1)); }
}
@keyframes bb-beam-swell {
  0%, 100% { --bb-glow-s: 0.78; }
  50%      { --bb-glow-s: 1.26; }
}
@keyframes bb-beam-drift {
  0%, 100% { --bb-glow-d: -8%; }
  50%      { --bb-glow-d: 8%; }
}

/* REDUCED MOTION — the degradation the brief asked for, literally: the ring
   stops sweeping and stops breathing and paints the plain --grad token
   instead of the conic, i.e. it becomes the static signature keyline the app
   already had. The colour pulse is removed entirely rather than frozen: a
   motionless coloured wash behind a nav label is not a calmer version of the
   effect, it is a different (and worse) one. Four layers of protection, per
   the house convention: the no-preference wrapper above, this block, the
   selectors already listed in main.css's reduce block, and js/motion.js's own
   JS-level check — none of which this block depends on. */
@media (prefers-reduced-motion: reduce) {
  .bb-beam.is-on::before,
  .bb-beam.active::before {
    background: var(--bb-beam-fallback, var(--grad));
    opacity: calc(var(--bb-beam-ring-hi) * var(--bb-beam-strength, 1));
    animation: none !important;
  }
  .bb-beam--inner.is-on::after,
  .bb-beam--inner.active::after,
  .bb-beam--outer.is-on::after,
  .bb-beam--outer.active::after {
    display: none;
  }
}

/* ---------- beam: adoption sites ----------
   Three, and deliberately only three. The beam is the app's one brand moment
   (see the --grad comment at main.css:40-44) and it stops being a moment the
   instant every button has it, so ordinary .btn-ghost / .btn-mini / .btn-dark
   are left calm on purpose. What qualifies: a surface that already paints the
   signature gradient, or the single accent CTA. Tuning lives here rather than
   inline in markup so the values are reviewable in one place. */

/* 1. The active sidebar item — the primary target. It is on screen
   permanently, so the sweep is slowed well past the 7s default and the
   breath carries the signal instead; a ring circling in the corner of the
   eye forever becomes a distraction inside a minute. Strength is pulled back
   to 0.85 and the feather kept tight so the pulse stays a back-light behind
   the label, never a wash over it. The static gradient keyline at
   main.css:519 and the 1.5px transparent rest border at main.css:518 are
   both untouched — the ring lands exactly on the keyline, and the rest
   border is still what stops activation shifting layout. */
.nav-item.bb-beam {
  --bb-beam-dur: 11s;
  --bb-beam-pulse: 3.8s;
  --bb-beam-strength: 0.85;
  --bb-beam-feather: 32%;
  --bb-beam-ring-lo: 0.28;
  --bb-beam-glow-lo: 0.20;
  --bb-beam-glow-hi: 0.72;
}

/* 2. The Live CCU widget — the showcase, and the reason the double ring is
   gone. The beam moved OFF the outer .rail-card (index.html:333, which used
   to carry .border-beam) and ONTO the tint that was already painting the
   static gradient, so one surface now carries exactly one ring instead of
   two competing ones 15px apart. This is the full-strength "colourful inner
   pulse" the ask named, and it earns it: it is the only live-data surface on
   the dashboard. */
.rail-tint.green.bb-beam {
  --bb-beam-dur: 8s;
  --bb-beam-pulse: 3.2s;
  --bb-beam-strength: 1;
  --bb-beam-feather: 30%;
}

/* 3. The Toolbox agent CTA — the app's single accent button, and the one
   place the outer bloom belongs: it sits alone in generous whitespace on the
   page background (.agent-landing, main.css:2175), so light spilling past
   its edge has room to land. It gets no static keyline of its own, so this
   is the beam language spreading past nav by one deliberate step, not by
   default. The pill only lights when its install token is ready — js/toolbox.js
   adds `is-on` on the same condition it drops `disabled`, so a dead button
   never glows. */
.agent-pill.bb-beam {
  --bb-beam-inset: -1px;
  --bb-beam-w: 1.2px;
  --bb-beam-dur: 9s;
  --bb-beam-pulse: 4.2s;
  --bb-beam-strength: 0.8;
  --bb-beam-bloom: 6px;
}
/* Idles slow, quickens under the cursor. Swapping the duration re-times the
   sweep rather than restarting it, which shows as a small jump in the arc's
   position — acceptable on a single hover, and the alternative (a second
   animation) costs a whole extra layer. */
@media (prefers-reduced-motion: no-preference) and (hover: hover) and (pointer: fine) {
  .agent-pill.bb-beam:hover { --bb-beam-dur: 2.6s; }
}
