/* ORBIT'S ORB, as a component -- B317.
 *
 * The orb already existed on the home page as a one-off. It is now
 * extracted here so she can appear ANYWHERE on the site: beside a
 * screenshot to say what you are looking at, beside a video to say what
 * it shows, in the middle of a long page to break it up with a voice
 * instead of another paragraph.
 *
 * Sized entirely by --orb-size, so the same markup is a 118px hero orb
 * and a 56px margin note.
 *
 * --amp (0..1) is the voice level. Everything reacts to it: the core
 * blooms, the halo swells, the whole orb scales. Feed it from real
 * audio and she is lip-syncing; feed it from a rhythm and she is
 * miming. orbit-say.js does one or the other depending on whether an
 * audio file exists yet.
 */

.orb {
  --orb-size: 118px;
  --amp: 0;
  position: relative; display: inline-block;
  width: var(--orb-size); height: var(--orb-size);
  border: 0; padding: 0; background: none; cursor: pointer;
  transform: scale(calc(1 + var(--amp) * 0.55));
  transition: transform 0.08s linear;
  flex: 0 0 auto;
}

.orb .blob {
  position: absolute; inset: 0; display: block; overflow: hidden;
  isolation: isolate;
  background:
    radial-gradient(circle at 32% 28%, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 24%),
    radial-gradient(circle at 74% 70%, #f0abfc 0%, rgba(240,171,252,0) 52%),
    radial-gradient(circle at 24% 72%, #38bdf8 0%, rgba(56,189,248,0) 56%),
    radial-gradient(circle at 70% 22%, #a78bfa 0%, rgba(167,139,250,0) 52%),
    radial-gradient(circle at 50% 50%, #4f46e5 0%, #1e1b4b 100%);
  border-radius: 44% 56% 54% 46% / 48% 44% 56% 52%;
  animation: ao-blob 9s ease-in-out infinite;
  filter: saturate(calc(0.95 + var(--amp) * 0.7))
          brightness(calc(0.98 + var(--amp) * 0.5));
}
@keyframes ao-blob {
  0%, 100% { border-radius: 46% 54% 57% 43% / 48% 42% 58% 52%; }
  16%      { border-radius: 61% 39% 42% 58% / 38% 61% 39% 62%; }
  33%      { border-radius: 40% 60% 64% 36% / 59% 41% 59% 41%; }
  50%      { border-radius: 56% 44% 37% 63% / 43% 64% 36% 57%; }
  66%      { border-radius: 37% 63% 56% 44% / 63% 37% 53% 47%; }
  83%      { border-radius: 58% 42% 47% 53% / 39% 58% 42% 61%; }
}

/* Ribbons of light inside the glass. Blurred and centre-masked: the
   raw conic wedges read as a pinwheel with a hard point in the middle,
   which is not what an orb looks like. */
.orb .ao-swirl, .orb .ao-mist {
  position: absolute; inset: -46%; display: block;
  mix-blend-mode: screen;
  -webkit-mask-image: radial-gradient(circle at 50% 50%, transparent 0 14%, #000 34%);
  mask-image: radial-gradient(circle at 50% 50%, transparent 0 14%, #000 34%);
}
.orb .ao-swirl {
  background: conic-gradient(from 0deg,
    transparent 0%, rgba(255,255,255,0.6) 8%, transparent 21%,
    rgba(56,189,248,0.7) 37%, transparent 54%,
    rgba(240,171,252,0.65) 69%, transparent 83%,
    rgba(167,139,250,0.65) 92%, transparent 100%);
  filter: blur(6px);
  animation: ao-spin var(--spin, 8s) linear infinite,
             ao-hue 13s ease-in-out infinite;
}
.orb .ao-mist {
  background: conic-gradient(from 180deg,
    transparent 0%, rgba(129,140,248,0.5) 14%, transparent 30%,
    rgba(232,121,249,0.4) 48%, transparent 62%,
    rgba(103,232,249,0.45) 80%, transparent 100%);
  filter: blur(10px);
  animation: ao-spin calc(var(--spin, 8s) * 1.6) linear infinite reverse;
}
@keyframes ao-spin { to { transform: rotate(360deg); } }
@keyframes ao-hue {
  0%, 100% { filter: blur(6px) hue-rotate(0deg); }
  50% { filter: blur(6px) hue-rotate(55deg); }
}

/* The heart: barely there in silence, blooming with every word. */
.orb .core {
  position: absolute; inset: 20%; display: block; border-radius: 50%;
  background: radial-gradient(circle,
    rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.35) 22%,
    rgba(196,181,253,0.7) 46%, transparent 72%);
  mix-blend-mode: screen; filter: blur(1px);
  opacity: calc(0.10 + 0.72 * var(--amp));
  transform: scale(calc(0.30 + 1.75 * var(--amp)));
  transition: opacity 0.08s linear, transform 0.08s linear;
}
.orb .halo {
  position: absolute; inset: calc(var(--orb-size) * -0.32); border-radius: 50%;
  background: radial-gradient(circle, rgba(240,171,252,0.50),
    rgba(167,139,250,0.36) 38%, rgba(139,92,246,0.18) 56%,
    rgba(139,92,246,0.07) 72%, transparent 92%);
  opacity: calc(0.38 + var(--amp) * 1.1);
  transform: scale(calc(1 + var(--amp) * 0.5));
  animation: orbBreathe 2.8s ease-in-out infinite,
             haloHue 7s ease-in-out infinite;
}
@keyframes haloHue {
  0%, 100% { filter: blur(18px) hue-rotate(-18deg); }
  50% { filter: blur(18px) hue-rotate(26deg); }
}
@keyframes orbBreathe {
  0%, 100% { transform: scale(0.96); filter: brightness(0.9); }
  50% { transform: scale(1.04); filter: brightness(1.12); }
}
.orb .ring {
  position: absolute; inset: -8px; border-radius: 50%;
  border: 1px solid rgba(94,234,212,0.35);
  opacity: calc(0.45 + var(--amp) * 0.8);
  transform: scale(calc(1 + var(--amp) * 0.14));
}

/* ==================================================================
   "ORBIT SAYS" -- the orb with something to say about what is next
   to it.
   ================================================================== */
.osay {
  display: flex; gap: 20px; align-items: flex-start;
  margin: 30px 0 0; padding: 18px 20px 18px 18px; border-radius: 18px;
  border: 1px solid rgba(167,139,250,0.22);
  background: linear-gradient(120deg, rgba(167,139,250,0.10),
    rgba(56,189,248,0.05) 42%, rgba(13,18,32,0.45));
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.16),
    0 14px 34px rgba(0,0,0,0.35);
}
/* Width/height repeated here on purpose. index.html carries its own
   older `.orb { width: 118px }` rule for the hero orb, and a bare
   `.orb` selector in this file loses to it -- the margin notes would
   all render at hero size. Two-class specificity settles it. */
.osay .orb { --orb-size: 62px; margin: 6px 8px 0 6px;
  width: var(--orb-size); height: var(--orb-size); }
.osay .orb .halo { inset: calc(var(--orb-size) * -0.32); }
.osay .orb .ring { inset: -5px; }
.osay .obub { flex: 1 1 auto; min-width: 0; }
.osay .obub b {
  display: block; font-family: 'Consolas', 'JetBrains Mono', monospace;
  font-size: 10.5px; letter-spacing: 0.24em; font-weight: 800;
  color: #c4b5fd; text-transform: uppercase; margin-bottom: 7px;
}
.osay .obub p {
  margin: 0; font-size: 15px; line-height: 1.66;
  color: #e6ecf8; font-style: italic;
}
.osay .obub p b, .osay .obub p strong { color: #fff; font-style: normal; }
.osay .obub .hint {
  display: block; margin-top: 9px; font-size: 11px; letter-spacing: 0.1em;
  color: #7c8aa5; font-style: normal; text-transform: uppercase;
}
.osay.narrow { max-width: 620px; }
.osay.center { margin-left: auto; margin-right: auto; }

/* While she is "speaking", the text arrives instead of just sitting
   there. Without this the orb pulses at a paragraph that was already
   finished, which reads as broken rather than alive. */
.osay .obub p .w { opacity: 0; transition: opacity 0.18s ease; }
.osay.spoken .obub p .w { opacity: 1; }

@media (max-width: 620px) {
  .osay { gap: 14px; padding: 15px 15px 16px 13px; }
  .osay .orb { --orb-size: 46px; }
  .osay .obub p { font-size: 14px; }
}
@media (prefers-reduced-motion: reduce) {
  .orb, .orb * { animation: none !important; transition: none !important; }
  .osay .obub p .w { opacity: 1 !important; }
}
