/* OpenStudio — speaking indicators around the avatar medallion (see osfx.js).
   Loaded by BOTH studio.html (host stage, .o3) and index.html (guest program mirror,
   .stagebox), which is the point: every other tile feature in this app is hand-copied
   between those two stylesheets and drifts. These class names are new and unique, so
   they don't collide with either root's rules and don't need the root prefix.

   Everything reads two variables the driver writes on the tile:
     --dsc  the medallion's exact pixel size (JS-computed; CSS can't derive it — see
            sizeStageDiscs) — the indicator is sized off the circle it wraps.
     --lv   0..1, the smoothed live voice level. This is what makes the look react
            rather than just loop.

   EVERY length here is a fraction of --dsc, never a raw pixel. A medallion runs from
   ~28px in the settings swatch to ~460px on a solo 1080p tile — 16x — so a hard px
   band that looks like a bar in the picker is a hairline stub on the stage. The
   fractions below are the old constants divided by the swatch's 28.5px disc, so the
   picker is pixel-identical to what it always was and every larger size is now that
   same look, scaled. Each one carries a max() floor so a tile in a 20-up grid keeps a
   ring you can actually see rather than a sub-pixel ghost.
   (Spectrum never had this bug: it's canvas, and derives everything from the canvas
   width already.) */

/* Registering --lv makes it an animatable <number>, which is what lets the settings
   previews fake a voice from pure CSS where a look needs no canvas. Unregistered, a
   custom property is a string and can't be interpolated. */
@property --lv { syntax: '<number>'; inherits: true; initial-value: 0; }
/* inherits:true is load-bearing, not tidiness. The driver sets --rot on .spkfx, but the
   elements that READ it (.sfx-arc, .sfx-dot) are its children. Registered non-inheriting
   properties don't reach descendants — the child resolves to initial-value instead, and
   the var() fallback never fires because the property IS defined, just as 0deg. With
   inherits:false the arc sat frozen at rotation zero forever, which is exactly where the
   conic gradient parks its bright head (355deg) and the dot its glow (50% 0%): the top.
   Orbit looked like a static blob at 12 o'clock lighting up, never a comet. */
@property --rot { syntax: '<angle>'; inherits: true; initial-value: 0deg; }

/* The indicator box sits exactly on the medallion, centred, with effects free to spill
   outside it. Below .phdisc (z-index 1) in paint order, so a photo medallion always
   stays crisp on top of its own ring.
   The fade is lopsided on purpose, matching the border glow this replaces: IN is fast
   (.12s) because any slower reads as the ring lagging the voice, OUT is slow (.85s) so
   a pause between words only dims it and a real stop reads as letting go. */
/* Deliberately not scoped to .feed: the settings previews are the same markup outside a
   tile, and scoping here would leave the swatches unpositioned and unsized. The class
   names are unique to this file, so an unscoped selector collides with nothing. */
/* Nothing may paint INSIDE the medallion. Every look here reaches a little past the rim
   on the inside — orbit's dot glow, sonar's rim shadow, reactor's ticks and its core
   bloom — which cost nothing for as long as the circle on top was opaque: Spotlight
   (ph-mono) and every photo medallion simply hid those pixels. Three of the four
   camera-off styles are glass, though. Themed/Color/Aurora fill the disc with
   rgba(...,.07–.15), and Themed/Aurora additionally backdrop-filter:blur() it, which
   samples what's behind and pulls it in. The ring showed straight through and read as
   painted ON TOP of the face — the one thing the z-index above is there to prevent.
   So: punch the disc out of the fx. It only removes what the opaque styles were already
   covering, which makes it a no-op on Spotlight and on photos, and makes the other three
   match them. The hole stops 1px short of the rim so the ring tucks under the medallion's
   own antialiased edge rather than leaving a hairline gap at it.
   (The picker can't show this: every fx swatch in studio.html is hardcoded ph-mono.)

   The transparent border is what makes the mask survivable, and is the whole reason this
   isn't a one-liner. mask-clip is border-box, so the mask doesn't just paint within that
   box — it CLIPS the element to it, and every look deliberately spills outside .spkfx (the
   canvases run 1.95x the disc; sonar's outer glow reaches ~1.5x the disc radius at full
   level). A 1.5x border grows the border box to 4x the disc — half-extent 2x, clear of the
   widest spill — and costs nothing else: the children are absolutely positioned, so their
   inset percentages resolve against the PADDING box, which is still exactly --dsc. Their
   geometry, and every fraction below, is untouched. mask-clip:no-clip would say all this
   directly, but it's not support the guest mirror can bet on across Safari/iOS.
   box-sizing is spelled out because both roots set border-box on * — inherited, it would
   collapse the content box to zero and take the whole indicator with it. */
.spkfx {
  position: absolute; left: 50%; top: 50%;
  box-sizing: content-box;
  width: var(--dsc, 0px); height: var(--dsc, 0px);
  border: calc(var(--dsc, 0px) * 1.5) solid transparent;
  transform: translate(-50%, -50%);
  pointer-events: none; z-index: 0;
  opacity: 0; transition: opacity .85s ease;
  -webkit-mask-image: radial-gradient(circle calc(var(--dsc, 0px) / 2) at 50% 50%, transparent calc(100% - 2px), #000 calc(100% - 1px));
  mask-image: radial-gradient(circle calc(var(--dsc, 0px) / 2) at 50% 50%, transparent calc(100% - 2px), #000 calc(100% - 1px));
}
.spk .spkfx { opacity: 1; transition-duration: .12s; }
.spkfx > i { position: absolute; inset: 0; border-radius: 50%; display: block; }
/* A tile wearing an indicator drops the border glow — two speaking cues on one tile is
   noise, and the medallion ring is the one the host chose. Camera tiles never get .fxon,
   so they keep the border exactly as before. */
.feed.fxon::after { display: none; }

/* ---- Orbit: a comet arc sweeping the rim over a faint track ---------------------- */
.sfx-orbit {
  --o-track: max(1px, calc(var(--dsc, 0px) * .053));   /* was 1.5px */
  --o-band:  max(1.5px, calc(var(--dsc, 0px) * .088)); /* was 2.5px */
}
.sfx-orbit .sfx-track { inset: -7%; box-shadow: inset 0 0 0 var(--o-track) rgba(var(--lime-rgb), .16); }
/* The arc is a conic sweep masked down to a ring band. --rot is advanced by the driver
   rather than by a CSS animation: the speed has to follow the voice, and re-timing a
   running animation jumps its phase (elapsed time remaps against the new duration),
   which shows up as the comet stuttering every time someone gets louder. */
.sfx-orbit .sfx-arc {
  inset: -7%;
  background: conic-gradient(from 0deg, rgba(var(--lime-rgb), 0) 0deg, rgba(var(--lime-rgb), 0) 205deg, rgba(var(--lime-rgb), .5) 320deg, var(--lime) 355deg, rgba(var(--lime-rgb), 0) 360deg);
  /* The two stops are 1px apart, not coincident: a hard stop gets no antialiasing in
     Chromium, so the band's inner rim came out stair-stepped. A 1px ramp is below the
     eye's threshold as a blur but gives the rasteriser the gradient it needs to blend.
     The arc's OUTER rim doesn't need this — that edge comes from border-radius:50%
     clipping the background, which is antialiased already. */
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - var(--o-band)), #000 calc(100% - var(--o-band) + 1px));
  mask: radial-gradient(closest-side, transparent calc(100% - var(--o-band)), #000 calc(100% - var(--o-band) + 1px));
  transform: rotate(var(--rot, 0deg));
  filter: drop-shadow(0 0 max(1px, calc(var(--dsc, 0px) * (.035 + var(--lv) * .316))) rgba(var(--lime-rgb), .8));
}
/* The comet head: a bright bead riding the leading edge of the arc. Parked at the top of
   the ring and rotated with it, so it tracks the sweep without any second animation. */
.sfx-orbit .sfx-dot {
  inset: -7%; transform: rotate(var(--rot, 0deg));
  background:
    radial-gradient(circle at 50% 0%, var(--lime) 0,
      rgba(var(--lime-rgb), .55) max(1px, calc(var(--dsc, 0px) * (.053 + var(--lv) * .070))),
      rgba(var(--lime-rgb), 0)   max(3px, calc(var(--dsc, 0px) * (.175 + var(--lv) * .281))));
}

/* ---- Sonar: rings blooming off the rim ------------------------------------------- */
.sfx-sonar .sfx-rim {
  inset: -5%;
  box-shadow: inset 0 0 0 max(1.5px, calc(var(--dsc, 0px) * .070)) rgba(var(--lime-rgb), calc(.35 + var(--lv) * .65)),
              0 0 max(4px, calc(var(--dsc, 0px) * (.210 + var(--lv) * .772))) rgba(var(--lime-rgb), calc(.12 + var(--lv) * .38));
}
/* Ripple scale/opacity are written per frame by the driver (each ring carries the level
   it was thrown at, so a loud syllable's ring stays a loud ring for its whole life —
   a CSS animation would rewrite history as the voice changed under it). */
/* The ripples already TRAVELLED correctly at any size (the driver expands them with a
   scale() transform, which is proportional by nature) — it was only their stroke that
   was pinned, so a big tile threw hairlines instead of rings. */
.sfx-sonar .sfx-rip {
  inset: -5%; opacity: 0;
  box-shadow: inset 0 0 0 max(1px, calc(var(--dsc, 0px) * .053)) rgba(var(--lime-rgb), .9);
  will-change: transform, opacity;
}

/* ---- Reactor: a segmented tick gauge that fills with loudness -------------------- */
/* The ticks and the lit sweep are drawn on canvas by the driver — see reactor() in
   osfx.js for why, and for the geometry. Only the bloom is left here: it's a smooth
   radial gradient with no hard edge anywhere in it, so it has nothing to alias and
   costs nothing to leave in CSS. */
/* A soft core bloom under the medallion that breathes with the level — stops the look
   from being purely an outline. */
.sfx-reactor .sfx-core {
  inset: -9%;
  background: radial-gradient(closest-side, rgba(var(--lime-rgb), calc(var(--lv) * .3)) 0, rgba(var(--lime-rgb), 0) 72%);
}

/* ---- Canvas looks: Spectrum and Reactor ------------------------------------------ */
/* Each canvas is a square centred on the medallion, sized to a multiple of the disc —
   the SPREAD constants in osfx.js, which must match these or the driver's geometry
   lands at the wrong radius. Spectrum needs room for bars to radiate off the rim;
   Reactor needs less ring but has to fit its glow (see reactor() for the arithmetic). */
.spkfx .sfx-cv {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
}
.sfx-spectrum .sfx-cv { width: calc(var(--dsc, 0px) * 1.95); height: calc(var(--dsc, 0px) * 1.95); }
.sfx-reactor .sfx-cv  { width: calc(var(--dsc, 0px) * 1.9);  height: calc(var(--dsc, 0px) * 1.9); }

/* Reduced motion: the indicators still SHOW who's talking (that's information, not
   decoration) but stop moving. Orbit/sonar are handled in the driver, which skips their
   motion entirely; reactor's gauge is a level readout rather than an animation, so it
   stays. What goes is the ambient breathing. */
@media (prefers-reduced-motion: reduce) {
  .spkfx { transition: none; }
  .sfx-orbit .sfx-arc, .sfx-orbit .sfx-dot { filter: none; }
  .sfx-sonar .sfx-rip { display: none; }
}

/* ---- settings previews ----------------------------------------------------------- */
/* The picker's swatches are real tiles: same .spkfx markup, same driver, fed a synthetic
   voice (OSFx.demos) only while the modal is open. So what the host sees in the picker is
   the look itself, not a drawing of it. */
.phprev .spkfx { opacity: 1; }   /* a swatch is always "speaking" — that's the whole point of it */
