diff --git a/apps/desktop/src/components/CallParticipantTile.tsx b/apps/desktop/src/components/CallParticipantTile.tsx index 763092f..d70e754 100644 --- a/apps/desktop/src/components/CallParticipantTile.tsx +++ b/apps/desktop/src/components/CallParticipantTile.tsx @@ -75,7 +75,13 @@ export function CallParticipantTile(props: ParticipantTileProps) { } = props; const small = size === 'small'; - const borderClass = speaking + // Split the speaking indicator per-mode so we don't stack a tile border + // + inset glow on top of the avatar pulse (visual double-chrome). Video + // tiles get the border (the avatar is hidden behind the stream so the + // pulse wouldn't be visible anyway); audio tiles rely on the avatar + // pulse rendered inside AudioContent. + const videoSpeaking = speaking && video; + const borderClass = videoSpeaking ? 'border-emerald-500 shadow-[0_0_0_2px_rgba(22,163,74,0.25)] dark:border-emerald-400' : focused ? 'border-accent' @@ -98,9 +104,10 @@ export function CallParticipantTile(props: ParticipantTileProps) { )} - {/* Speaking indicator visible regardless of content type (video or - audio). z-10 ensures it sits above the video element. */} - {speaking && ( + {/* Video-only speaking indicator. Audio tiles use the avatar pulse + from AudioContent so we don't double-render chrome. z-10 keeps + it above the video element. */} + {videoSpeaking && (