diff --git a/app.js b/app.js index 3bc5810..17d7f0f 100644 --- a/app.js +++ b/app.js @@ -134,7 +134,19 @@ let sendAbort=new Map(),fileSeq=0; // One send() per chunk (no separate control frame). JSON carries seq/last flags. function packChunk(seq,offset,ivBuf,ctBuf,last){const hdr=JSON.stringify({t:'c',s:seq,o:offset,l:last?1:0});const h=enc.encode(hdr);const frame=new ArrayBuffer(4+h.length+12+ctBuf.byteLength);const v=new DataView(frame);v.setUint32(0,h.length);new Uint8Array(frame,4,h.length).set(h);new Uint8Array(frame,4+h.length,12).set(ivBuf);new Uint8Array(frame,4+h.length+12).set(ctBuf);return frame} const enc=new TextEncoder(),dec=new TextDecoder(); -function setStatus(text,on=false){statusText.textContent=text;$('.connection').classList.toggle('connected',on);if(connectCard)connectCard.hidden=on;if(addFriendBtn)addFriendBtn.disabled=on; if(on){const negotiated=pc?.sctp?.maxMessageSize||16*1024*1024;CHUNK=Math.min(1024*1024,Math.max(16*1024,negotiated-4096));messageInput.disabled=false;messageForm.querySelector('.send').disabled=false;fileInput.disabled=false;$('#leaveRoom').hidden=false;$('#hostRoom').hidden=true;$('#joinRoom').hidden=true;callBtn.disabled=false;if(!connectSoundDone){playSound('connect');connectSoundDone=true}}else{messageInput.disabled=true;messageForm.querySelector('.send').disabled=true;fileInput.disabled=true;callBtn.disabled=true;endCall(true)}} +function setStatus(text,on=false){ + statusText.textContent=text;$('.connection').classList.toggle('connected',on); + if(connectCard)connectCard.hidden=on;if(addFriendBtn)addFriendBtn.disabled=on; + const sideAdd=$('#sideAddFriend');if(sideAdd)sideAdd.disabled=on; + const dmStatus=$('#dmFriendStatus'); + if(dmStatus){ + if(on)dmStatus.textContent='Online'; + else if(/local test/i.test(text))dmStatus.textContent='Test mode'; + else dmStatus.textContent='Offline'; + } + if(on){const negotiated=pc?.sctp?.maxMessageSize||16*1024*1024;CHUNK=Math.min(1024*1024,Math.max(16*1024,negotiated-4096));messageInput.disabled=false;messageForm.querySelector('.send').disabled=false;fileInput.disabled=false;$('#leaveRoom').hidden=false;$('#hostRoom').hidden=true;$('#joinRoom').hidden=true;callBtn.disabled=false;if(!connectSoundDone){playSound('connect');connectSoundDone=true}}else{messageInput.disabled=true;messageForm.querySelector('.send').disabled=true;fileInput.disabled=true;callBtn.disabled=true;endCall(true)} + if(typeof syncVoiceChrome==='function')syncVoiceChrome(); +} const MAX_SIGNAL_SIZE=1024*1024,MAX_MESSAGE_SIZE=64*1024,SIGNAL_COMPRESSED_PREFIX='pair1.',SIGNAL_RAW_PREFIX='pair0.'; // Dot is used instead of base64url's underscore. Discord treats underscores // as Markdown emphasis, but dots and hyphens copy cleanly in ordinary chat. @@ -387,33 +399,32 @@ function renderItem(item,resultsEl){ gifAttachment=document.createElement('div');gifAttachment.className='gif-attachment';gifAttachment.hidden=true;gifAttachment.innerHTML='GIF attachment'; gifAttachment.querySelector('.gif-attachment-remove').onclick=()=>setPendingGif(null); composer.prepend(gifAttachment); - // Plus button - const plusWrap=document.createElement('div');plusWrap.style.cssText='position:relative;display:inline-flex'; + // Plus · input · GIF · emoji · send + const plusWrap=document.createElement('div');plusWrap.className='plus-wrap'; const plusBtn=document.createElement('button');plusBtn.type='button';plusBtn.className='composer-btn plus-btn';plusBtn.textContent='+';plusBtn.title='Attach'; const plusPopup=document.createElement('div');plusPopup.className='plus-popup';plusPopup.classList.add('hidden'); - const fileOpt=document.createElement('button');fileOpt.className='plus-opt';fileOpt.textContent='📎 Send file'; + const fileOpt=document.createElement('button');fileOpt.className='plus-opt';fileOpt.textContent='Send file'; fileOpt.onclick=()=>{plusPopup.classList.add('hidden');fileInput.click()}; - plusPopup.append(fileOpt);plusWrap.append(plusBtn,plusPopup);composer.insertBefore(plusWrap,sendBtn.nextSibling); + plusPopup.append(fileOpt);plusWrap.append(plusBtn,plusPopup);composer.insertBefore(plusWrap,messageInput); plusBtn.onclick=e=>{e.preventDefault();plusPopup.classList.toggle('hidden');emojiPicker&&emojiPicker.classList.add('hidden');gifPicker&&gifPicker.classList.add('hidden')}; document.addEventListener('click',e=>{if(!plusWrap.contains(e.target))plusPopup.classList.add('hidden')}); - emojiBtn=document.createElement('button');emojiBtn.type='button';emojiBtn.className='composer-btn emoji-btn';emojiBtn.textContent='😊';emojiBtn.title='Emoji'; - emojiPicker=buildEmojiPicker();emojiPicker.style.position='absolute';emojiPicker.style.bottom='100%';emojiPicker.style.right='60px'; - composer.append(emojiPicker); - emojiBtn.onclick=e=>{e.preventDefault();emojiPicker.classList.toggle('hidden');gifPicker&&gifPicker.classList.add('hidden');plusPopup&&plusPopup.classList.add('hidden')}; - composer.insertBefore(emojiBtn,sendBtn.nextSibling); - // GIF button gifBtn=document.createElement('button');gifBtn.type='button';gifBtn.className='composer-btn gif-btn';gifBtn.textContent='GIF';gifBtn.title='GIF'; gifPicker=buildGifPicker();gifPicker.style.position='absolute';gifPicker.style.bottom='100%';gifPicker.style.right='0'; composer.append(gifPicker); gifBtn.onclick=e=>{e.preventDefault();const show=gifPicker.classList.contains('hidden');gifPicker.classList.toggle('hidden');emojiPicker&&emojiPicker.classList.add('hidden');plusPopup&&plusPopup.classList.add('hidden');if(show){const r=gifPicker.querySelector('.gif-results');const tabs=gifPicker.querySelectorAll('.gif-picker-tab');if(tabs[2]?.classList.contains('active'))renderFavs(r);else{tabs[0]?.click()}}}; - composer.insertBefore(gifBtn,sendBtn.nextSibling); + composer.insertBefore(gifBtn,sendBtn); + emojiBtn=document.createElement('button');emojiBtn.type='button';emojiBtn.className='composer-btn emoji-btn';emojiBtn.textContent='😊';emojiBtn.title='Emoji'; + emojiPicker=buildEmojiPicker();emojiPicker.style.position='absolute';emojiPicker.style.bottom='100%';emojiPicker.style.right='60px'; + composer.append(emojiPicker); + emojiBtn.onclick=e=>{e.preventDefault();emojiPicker.classList.toggle('hidden');gifPicker&&gifPicker.classList.add('hidden');plusPopup&&plusPopup.classList.add('hidden')}; + composer.insertBefore(emojiBtn,sendBtn); // Enable input/button on connect const orig=messageInput.disabled; Object.defineProperty(messageInput,'disabled',{set(v){this._disabled=v;if(v){this.setAttribute('disabled','')}else{this.removeAttribute('disabled')}sendBtn.disabled=v;emojiBtn.disabled=v;gifBtn.disabled=v;plusBtn.disabled=v},get(){return this._disabled!==false}}); messageInput.disabled=orig; })(); function isEncryptedMessage(value){return !!value&&Array.isArray(value.iv)&&value.iv.length===12&&Array.isArray(value.data)&&value.data.length>0&&value.data.length<=MAX_MESSAGE_SIZE+32&&value.iv.every(Number.isInteger)&&value.data.every(Number.isInteger)} -function setupChannels(){chat=pc.createDataChannel('chat');files=pc.createDataChannel('files');wire()}function wire(){if(chat){chat.onopen=()=>{setStatus('Connected directly',true);announceProfile()};chat.onmessage=async e=>{try{if(typeof e.data!=='string'||e.data.length>MAX_MESSAGE_SIZE*3)return;const o=JSON.parse(e.data);if(o.t==='msg'&&isEncryptedMessage(o.v)){const message=readChatPayload(dec.decode(await open(o.v)));addMessage(message.text,false,message.gif)}else if(o.t==='profile'){const p=typeof o.v==='string'?{image:o.v}:o.v;if(validProfileIdentity(p?.identity))setAvatarIdentity(friendAvatar,p.identity);if(typeof p?.image==='string'&&p.image.length<=MAX_PROFILE_DATA){setAvatar(friendAvatar,p.image);setAvatarFrame(friendAvatar,p.frame)}} else if(o.t==='call-ring'){// Reset the leave-chime flag when the friend rings again, so a second +function setupChannels(){chat=pc.createDataChannel('chat');files=pc.createDataChannel('files');wire()}function wire(){if(chat){chat.onopen=()=>{setStatus('Connected directly',true);announceProfile()};chat.onmessage=async e=>{try{if(typeof e.data!=='string'||e.data.length>MAX_MESSAGE_SIZE*3)return;const o=JSON.parse(e.data);if(o.t==='msg'&&isEncryptedMessage(o.v)){const message=readChatPayload(dec.decode(await open(o.v)));addMessage(message.text,false,message.gif)}else if(o.t==='profile'){const p=typeof o.v==='string'?{image:o.v}:o.v;const dmFriendAvatar=$('#dmFriendAvatar');if(validProfileIdentity(p?.identity)){setAvatarIdentity(friendAvatar,p.identity);if(dmFriendAvatar)setAvatarIdentity(dmFriendAvatar,p.identity)}if(typeof p?.image==='string'&&p.image.length<=MAX_PROFILE_DATA){setAvatar(friendAvatar,p.image);setAvatarFrame(friendAvatar,p.frame);if(dmFriendAvatar){setAvatar(dmFriendAvatar,p.image);setAvatarFrame(dmFriendAvatar,p.frame)}}} else if(o.t==='call-ring'){// Reset the leave-chime flag when the friend rings again, so a second // call→leave cycle still plays the leave tone instead of going silent. friendLeftNotified=false;setParticipant(participantFriend,true);logCallEvent('Friend joined the call');playSound('ring');setRemoteCallAudio(callActive);}else if(o.t==='call-end'){setParticipant(participantFriend,false);if(!friendLeftNotified){friendLeftNotified=true;playSound('leave')}logCallEvent('Friend left the call');callStatus.textContent='Friend left the call';callStatus.className='call-status';/* Keep this person's microphone and call state alive. The connection is a room, not a one-shot call: only the peer that pressed End Call leaves. */}else if(o.t==='screen-start'){remoteScreenExpected=true;logCallEvent('Friend started screen sharing');remoteScreen.hidden=false;screenStatus.textContent='Friend sharing';}else if(o.t==='screen-end'){remoteScreenExpected=false;logCallEvent('Friend stopped screen sharing');remoteScreen.srcObject=null;remoteScreen.hidden=true;screenStatus.textContent='Not sharing';}else if(o.t==='reneg-offer'&&typeof o.sdp==='string'&&pc){// Same glare rule as the signaling path: only the join/answer side yields. if(renegPending&&(role==='join'||role==='answer')){renegotiating++;renegPending=false}await pc.setRemoteDescription({type:'offer',sdp:o.sdp});const a=await pc.createAnswer();await pc.setLocalDescription({type:'answer',sdp:patchSdp(a.sdp)});await waitIce();send({t:'reneg-answer',sdp:pc.localDescription.sdp});}else if(o.t==='reneg-answer'&&typeof o.sdp==='string'&&pc){await pc.setRemoteDescription({type:'answer',sdp:o.sdp});}}catch(e){console.warn('direct renegotiation error',e)}}}if(files){files.binaryType='arraybuffer';files.bufferedAmountLowThreshold=Math.max(1*1024*1024,SEND_WINDOW-4*1024*1024); files.onmessage=e=>{receiveQueue=receiveQueue.then(()=>onFileFrame(e)).catch(()=>{})};files.onopen=()=>setStatus('Connected directly',true)}if(streamWs){streamWs.binaryType='arraybuffer';try{streamWs.bufferedAmountLowThreshold=SEND_WINDOW*0.75}catch{};streamWs.onmessage=e=>onStreamFrame(e);}} @@ -657,7 +668,7 @@ async function processIncoming(t){const POOL=8;const queue=t.writeQueue;let acti }finally{clearInterval(watchdog);try{await flushBatch()}catch{}} } setStatus('Not connected'); -function enableLocalTestControls(){if(!LOCAL_TEST_MODE)return;messageInput.disabled=false;messageForm.querySelector('.send').disabled=false;fileInput.disabled=false;callBtn.disabled=false;screenBtn.disabled=false;screenPreset.disabled=false;statusText.textContent='Local test mode';pairHint.textContent='Test mode is on — messages stay on this device until you pair with a friend.'} +function enableLocalTestControls(){if(!LOCAL_TEST_MODE)return;messageInput.disabled=false;messageForm.querySelector('.send').disabled=false;fileInput.disabled=false;callBtn.disabled=false;screenBtn.disabled=false;screenPreset.disabled=false;statusText.textContent='Local test mode';pairHint.textContent='Test mode is on — messages stay on this device until you pair with a friend.';const dmStatus=$('#dmFriendStatus');if(dmStatus)dmStatus.textContent='Test mode';if(typeof syncVoiceChrome==='function')syncVoiceChrome()} enableLocalTestControls(); async function ss(key){if(window.pairSettings){try{return await window.pairSettings.get(key)}catch{}}try{return localStorage.getItem('pair.'+key)}catch{}} @@ -689,11 +700,16 @@ function releasePushToTalk(){pushToTalkReleaseTimer=null;pushToTalkHeld=false;ap voiceInputMode.onchange=()=>{voiceInputModeValue=voiceInputMode.value==='ptt'?'ptt':'voice';ssSet('voiceInputMode',voiceInputModeValue);if(voiceInputModeValue!=='ptt'){pushToTalkHeld=false;if(pushToTalkReleaseTimer){clearTimeout(pushToTalkReleaseTimer);pushToTalkReleaseTimer=null}}updatePushToTalkUI();applyMicTransmission()};pushToTalkKeyButton.onclick=()=>{pushToTalkCapturing=true;updatePushToTalkUI();deviceHint.textContent='Press the key you want to hold for push to talk.'};pushToTalkDelayInput.oninput=()=>{pushToTalkDelay=Math.max(0,Math.min(1000,Number(pushToTalkDelayInput.value)||0));ssSet('pushToTalkDelay',String(pushToTalkDelay));updatePushToTalkUI()}; window.addEventListener('keydown',event=>{if(pushToTalkCapturing){if(event.code==='Escape'){pushToTalkCapturing=false;updatePushToTalkUI();return}event.preventDefault();pushToTalkKey=event.code;pushToTalkCapturing=false;ssSet('pushToTalkKey',pushToTalkKey);updatePushToTalkUI();return}if(voiceInputModeValue!=='ptt'||event.code!==pushToTalkKey||event.repeat)return;if(/^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName||''))return;event.preventDefault();if(pushToTalkReleaseTimer){clearTimeout(pushToTalkReleaseTimer);pushToTalkReleaseTimer=null}pushToTalkHeld=true;applyMicTransmission()});window.addEventListener('keyup',event=>{if(voiceInputModeValue!=='ptt'||event.code!==pushToTalkKey)return;event.preventDefault();if(pushToTalkReleaseTimer)clearTimeout(pushToTalkReleaseTimer);pushToTalkReleaseTimer=setTimeout(releasePushToTalk,pushToTalkDelay)});window.addEventListener('blur',()=>{if(pushToTalkReleaseTimer){clearTimeout(pushToTalkReleaseTimer);pushToTalkReleaseTimer=null}pushToTalkHeld=false;applyMicTransmission()}); inputDevice.onchange=()=>{inputDeviceId=inputDevice.value;ssSet('inputDevice',inputDeviceId);if(micTestStream)stopMicrophoneTest()};outputDevice.onchange=()=>{outputDeviceId=outputDevice.value;ssSet('outputDevice',outputDeviceId);applyOutputDevice()};voiceProcessing.onchange=()=>{voiceProcessingEnabled=voiceProcessing.checked;ssSet('voiceProcessing',voiceProcessingEnabled?'on':'off');if(micTestStream)stopMicrophoneTest();deviceHint.textContent=voiceProcessingEnabled?'Echo cancellation enabled; noise suppression and auto-gain stay off.':'Raw stereo microphone mode enabled for the cleanest raw sound.'};$('#refreshDevices').onclick=()=>refreshAudioDevices();$('#testSound').onclick=()=>playSound('ring');testMicrophone.onclick=()=>toggleMicrophoneTest();navigator.mediaDevices?.addEventListener?.('devicechange',refreshAudioDevices); -const THEMES=new Set(['midnight','violet','forest','ember']); -function applyTheme(theme,persist=true){const selected=THEMES.has(theme)?theme:'midnight';document.documentElement.dataset.theme=selected;document.querySelectorAll('.theme-option').forEach(button=>{const active=button.dataset.theme===selected;button.classList.toggle('selected',active);button.setAttribute('aria-pressed',String(active))});if(persist)ssSet('theme',selected)} +const THEMES=new Set(['obsidian','midnight','violet','forest','ember']); +function applyTheme(theme,persist=true){const selected=THEMES.has(theme)?theme:'obsidian';document.documentElement.dataset.theme=selected;document.querySelectorAll('.theme-option').forEach(button=>{const active=button.dataset.theme===selected;button.classList.toggle('selected',active);button.setAttribute('aria-pressed',String(active))});if(persist)ssSet('theme',selected)} function syncPanelBackdrop(){panelBackdrop.hidden=!!settingsPanel.hidden&&!connectCard.open} function closePanels(){if(micTestStream)stopMicrophoneTest();connectCard.open=false;settingsPanel.hidden=true;document.body.classList.remove('settings-open');syncPanelBackdrop()} -$('#addFriend').onclick=()=>{settingsPanel.hidden=true;document.body.classList.remove('settings-open');connectCard.open=true;syncPanelBackdrop();setTimeout(()=>signalIn.focus(),0)}; +const openAddFriend=()=>{settingsPanel.hidden=true;document.body.classList.remove('settings-open');connectCard.open=true;syncPanelBackdrop();setTimeout(()=>signalIn.focus(),0)}; +$('#addFriend').onclick=openAddFriend; +['#sideAddFriend','#dmAddInline','#emptyAddFriend'].forEach(sel=>{const el=$(sel);if(el)el.onclick=openAddFriend}); +const headerCall=$('#headerCall');if(headerCall)headerCall.onclick=()=>{if(!callBtn.disabled)callBtn.click();else openAddFriend()}; +const headerScreen=$('#headerScreen');if(headerScreen)headerScreen.onclick=()=>{if(!screenBtn.disabled)screenBtn.click()}; +profileBtn.onclick=()=>openSettings(true);profileAdjust.onclick=()=>openSettings(true); connectCard.addEventListener('toggle',syncPanelBackdrop);panelBackdrop.onclick=closePanels; document.querySelectorAll('.theme-option').forEach(button=>button.onclick=()=>applyTheme(button.dataset.theme)); (async()=>{applyTheme(await ss('theme'),false)})(); @@ -704,21 +720,36 @@ function setAvatar(el,data){if(!el)return;const safe=validProfileData(data)?data function normalizeFrame(frame){return {zoom:Math.max(40,Math.min(180,Number(frame?.zoom)||100)),x:Math.max(0,Math.min(100,Number(frame?.x??50))),y:Math.max(0,Math.min(100,Number(frame?.y??50)))}} function validProfileIdentity(value){return typeof value==='string'&&/^[a-z0-9]{12,32}$/i.test(value)} function normalizeProfileName(value,fallback){if(typeof value!=='string')return fallback;const name=value.replace(/[\u0000-\u001f\u007f]/g,'').replace(/\s+/g,' ').trim().slice(0,32);return name||fallback} -function renderParticipantNames(){yourNameEl.textContent=profileName;friendNameEl.textContent=friendName;displayNameInput.value=profileName} +function renderParticipantNames(){ + yourNameEl.textContent=profileName;friendNameEl.textContent=friendName;displayNameInput.value=profileName; + const userBarName=$('#userBarName');if(userBarName)userBarName.textContent=profileName; + const dmFriendName=$('#dmFriendName');if(dmFriendName)dmFriendName.textContent=friendName; + const channelTitle=$('#channelTitle');if(channelTitle)channelTitle.textContent=friendName||'Friend'; + const emptyTitle=$('#emptyTitle');if(emptyTitle)emptyTitle.textContent=friendName||'Friend'; + const emptyCopy=$('#emptyCopy');if(emptyCopy)emptyCopy.textContent=`This is the beginning of your direct message history with ${friendName||'Friend'}. Messages travel peer to peer.`; + const messagePlaceholder=friendName?`Message @${friendName}`:'Message @Friend'; + if(messageInput){messageInput.placeholder=messagePlaceholder;messageInput.setAttribute('aria-label',messagePlaceholder)} +} function updateProfileName(value,{persist=true,share=true}={}){profileName=normalizeProfileName(value,'You');renderParticipantNames();if(persist)ssSet('profileName',profileName);if(share)announceProfile()} function handleProfileNameMessage(event){try{if(typeof event.data!=='string')return;const message=JSON.parse(event.data);if(message?.t!=='profile-name')return;friendName=normalizeProfileName(message.v,'Friend');renderParticipantNames()}catch{}} function makeProfileIdentity(){const bytes=crypto.getRandomValues(new Uint8Array(9));return [...bytes].map(v=>v.toString(36).padStart(2,'0')).join('')} function avatarHue(identity){let hash=0;for(const ch of identity)hash=(hash*31+ch.charCodeAt(0))>>>0;return hash%360} function setAvatarIdentity(el,identity){if(!el)return;const safe=validProfileIdentity(identity)?identity:'';if(safe)el.style.setProperty('--avatar-hue',String(avatarHue(safe)));else el.style.removeProperty('--avatar-hue');} function setAvatarFrame(el,frame){if(!el)return;const f=normalizeFrame(frame);el.style.backgroundSize=f.zoom+'% auto';el.style.backgroundPositionX=f.x+'%';el.style.backgroundPositionY=f.y+'%';} -function renderProfile(){[profileBtn,settingsAvatar].forEach(el=>{setAvatar(el,profileAvatar);setAvatarFrame(el,profileFrame);setAvatarIdentity(el,profileIdentity)});renderParticipantNames();const hasPhoto=!!profileAvatar;profileAdjust.hidden=!hasPhoto;settingsAdjustPhoto.hidden=!hasPhoto;settingsRemovePhoto.hidden=!hasPhoto;} +function renderProfile(){ + const targets=[profileBtn,settingsAvatar,$('#userBarAvatar')].filter(Boolean); + targets.forEach(el=>{setAvatar(el,profileAvatar);setAvatarFrame(el,profileFrame);setAvatarIdentity(el,profileIdentity)}); + renderParticipantNames(); + const hasPhoto=!!profileAvatar;profileAdjust.hidden=!hasPhoto;settingsAdjustPhoto.hidden=!hasPhoto;settingsRemovePhoto.hidden=!hasPhoto; +} function announceProfile(){if(profileIdentity){send({t:'profile',v:{image:profileSharing?profileAvatar:'',frame:profileFrame,identity:profileIdentity}});send({t:'profile-name',v:profileSharing?profileName:'Friend'})}} async function readProfileData(blob){return new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(String(r.result));r.onerror=()=>reject(new Error('Could not read image'));r.readAsDataURL(blob)})} async function resizeProfile(file){if(file.type==='image/gif'){if(file.size>5*1024*1024)throw new Error('Choose a GIF smaller than 5 MB');const data=await readProfileData(file);if(!validProfileData(data))throw new Error('Choose a valid GIF smaller than 5 MB');return data}const bitmap=await createImageBitmap(file);const size=480,scale=Math.min(size/bitmap.width,size/bitmap.height,1);const canvas=document.createElement('canvas');canvas.width=Math.max(1,Math.round(bitmap.width*scale));canvas.height=Math.max(1,Math.round(bitmap.height*scale));canvas.getContext('2d').drawImage(bitmap,0,0,canvas.width,canvas.height);bitmap.close?.();const blob=await new Promise(resolve=>canvas.toBlob(resolve,'image/jpeg',.72));if(!blob)throw new Error('Could not read image');const data=await readProfileData(blob);if(!validProfileData(data))throw new Error('Choose a smaller image');return data;} function updateProfileFrame(sendUpdate=false){profileFrame=normalizeFrame({zoom:profileZoom.value,x:profileX.value,y:profileY.value});renderProfile();ssSet('profileFrame',JSON.stringify(profileFrame));if(sendUpdate)announceProfile()} function openSettings(showPhotoEditor=false){connectCard.open=false;settingsPanel.hidden=false;document.body.classList.add('settings-open');syncPanelBackdrop();if(showPhotoEditor){openSettingsTab('profile');profileEditor.hidden=false}} $('#openSettings').onclick=()=>openSettings();$('#closeSettings').onclick=closePanels; -profileBtn.onclick=()=>openSettings(true);profileAdjust.onclick=()=>openSettings(true);settingsChangePhoto.onclick=()=>profileInput.click();settingsAdjustPhoto.onclick=()=>{profileEditor.hidden=!profileEditor.hidden};settingsRemovePhoto.onclick=async()=>{profileAvatar='';renderProfile();profileEditor.hidden=true;await ssSet('profileAvatar',null);await ssSet('profilePhotoMode','none');announceProfile()};profileDone.onclick=()=>{profileEditor.hidden=true;updateProfileFrame(true)};[profileZoom,profileX,profileY].forEach(input=>input.oninput=()=>updateProfileFrame(false));[profileZoom,profileX,profileY].forEach(input=>input.onchange=()=>updateProfileFrame(true));profileInput.onchange=async()=>{const file=profileInput.files?.[0];profileInput.value='';if(!file)return;try{profileAvatar=await resizeProfile(file);renderProfile();await ssSet('profileAvatar',profileAvatar);await ssSet('profilePhotoMode','custom');announceProfile()}catch(e){alert(e.message||'Could not set profile photo')}}; +const userBarSettings=$('#userBarSettings');if(userBarSettings)userBarSettings.onclick=()=>openSettings(); +const userBarProfile=$('#userBarProfile');if(userBarProfile)userBarProfile.onclick=()=>openSettings(true);settingsChangePhoto.onclick=()=>profileInput.click();settingsAdjustPhoto.onclick=()=>{profileEditor.hidden=!profileEditor.hidden};settingsRemovePhoto.onclick=async()=>{profileAvatar='';renderProfile();profileEditor.hidden=true;await ssSet('profileAvatar',null);await ssSet('profilePhotoMode','none');announceProfile()};profileDone.onclick=()=>{profileEditor.hidden=true;updateProfileFrame(true)};[profileZoom,profileX,profileY].forEach(input=>input.oninput=()=>updateProfileFrame(false));[profileZoom,profileX,profileY].forEach(input=>input.onchange=()=>updateProfileFrame(true));profileInput.onchange=async()=>{const file=profileInput.files?.[0];profileInput.value='';if(!file)return;try{profileAvatar=await resizeProfile(file);renderProfile();await ssSet('profileAvatar',profileAvatar);await ssSet('profilePhotoMode','custom');announceProfile()}catch(e){alert(e.message||'Could not set profile photo')}}; (async()=>{updateProfileName(await ss('profileName'),{persist:false,share:false})})();displayNameInput.onchange=()=>updateProfileName(displayNameInput.value); (async()=>{inputDeviceId=(await ss('inputDevice'))||'default';outputDeviceId=(await ss('outputDevice'))||'default';voiceProcessingEnabled=(await ss('voiceProcessing'))==='on';voiceInputModeValue=(await ss('voiceInputMode'))==='ptt'?'ptt':'voice';const savedPttKey=await ss('pushToTalkKey');pushToTalkKey=typeof savedPttKey==='string'&&savedPttKey.length<32?savedPttKey:'Space';const savedPttDelay=Number(await ss('pushToTalkDelay'));pushToTalkDelay=Number.isFinite(savedPttDelay)?Math.max(0,Math.min(1000,savedPttDelay)):0;soundEnabled=(await ss('soundEffects'))!=='off';profileSharing=(await ss('shareProfile'))!=='off';rememberInviteCode=(await ss('rememberInvite'))!=='off';const motion=(await ss('reduceMotion'))==='on';const hardware=(await ss('hardwareAcceleration'))!=='off';if(!rememberInviteCode){signalIn.value='';ssSet('savedInviteCode',null)}voiceProcessing.checked=voiceProcessingEnabled;updatePushToTalkUI();soundEffects.checked=soundEnabled;shareProfile.checked=profileSharing;rememberInvite.checked=rememberInviteCode;reduceMotion.checked=motion;hardwareAcceleration.checked=hardware;document.documentElement.dataset.reduceMotion=String(motion);hardwareHint.textContent='Hardware acceleration is '+(hardware?'enabled':'disabled')+' for the next start.';await restoreScreenShareSettings();await refreshAudioDevices();await applyOutputDevice()})();signalIn.addEventListener('input',()=>{if(!rememberInviteCode)ssSet('savedInviteCode',null)}); (async()=>{const savedServer=await ss('signalServer');const savedRoom=await ss('roomCode');const savedInvite=await ss('savedInviteCode');if(savedServer)$('#signalServer').value=savedServer;if(savedRoom)$('#roomCode').value=savedRoom;if(typeof savedInvite==='string'&&savedInvite.length<=MAX_SIGNAL_SIZE)signalIn.value=savedInvite;['signalServer','roomCode'].forEach(id=>$('#'+id).addEventListener('input',()=>ssSet(id==='signalServer'?'signalServer':'roomCode',$('#'+id).value.trim())));signalIn.addEventListener('input',()=>ssSet('savedInviteCode',signalIn.value.trim()));const savedVol=await ss('volume');if(savedVol!==null){const v=parseFloat(savedVol);if(v>=0&&v<=1)setCallVolume(Math.round(v*100),false)}const savedFrame=await ss('profileFrame');try{if(savedFrame)profileFrame=normalizeFrame(JSON.parse(savedFrame))}catch{};profileZoom.value=profileFrame.zoom;profileX.value=profileFrame.x;profileY.value=profileFrame.y;const savedAvatar=await ss('profileAvatar');if(validProfileData(savedAvatar)){profileAvatar=savedAvatar;renderProfile();announceProfile()}})(); @@ -819,7 +850,7 @@ function disconnectRoom(){if(pc&&pc._connectTimer){clearTimeout(pc._connectTimer screenBtn.textContent='Share screen';screenBtn.title='Share screen';screenBtn.disabled=true; screenStatus.textContent='Not sharing'; remoteScreenExpected=false; - try{if(chat){chat.onmessage=null;chat.close()}}catch{}try{if(files){files.onmessage=null;files.close()}}catch{}try{if(pc)pc.close()}catch{}if(pc&&pc._silentAudioCtx)try{pc._silentAudioCtx.close()}catch{}pc=chat=files=null;if(signaling){try{signaling.onopen=null;signaling.onerror=null;signaling.onmessage=null;signaling.close()}catch{}signaling=null}if(streamWs){try{streamWs.onopen=null;streamWs.onerror=null;streamWs.onmessage=null;streamWs.onclose=null;streamWs.close()}catch{}streamWs=null}streamServer=streamRoom=null;sharedKey=null;setAvatar(friendAvatar,'');setAvatarIdentity(friendAvatar,'');try{remoteAudio.srcObject=null}catch{};try{if(audioCtx&&audioCtx.audioSink){audioCtx.audioSink.disconnect();delete audioCtx.audioSink}}catch{}try{remoteScreen.srcObject=null}catch{};remoteScreen.hidden=true; + try{if(chat){chat.onmessage=null;chat.close()}}catch{}try{if(files){files.onmessage=null;files.close()}}catch{}try{if(pc)pc.close()}catch{}if(pc&&pc._silentAudioCtx)try{pc._silentAudioCtx.close()}catch{}pc=chat=files=null;if(signaling){try{signaling.onopen=null;signaling.onerror=null;signaling.onmessage=null;signaling.close()}catch{}signaling=null}if(streamWs){try{streamWs.onopen=null;streamWs.onerror=null;streamWs.onmessage=null;streamWs.onclose=null;streamWs.close()}catch{}streamWs=null}streamServer=streamRoom=null;sharedKey=null;setAvatar(friendAvatar,'');setAvatarIdentity(friendAvatar,'');const dmFriendAvatar=$('#dmFriendAvatar');if(dmFriendAvatar){setAvatar(dmFriendAvatar,'');setAvatarIdentity(dmFriendAvatar,'')}try{remoteAudio.srcObject=null}catch{};try{if(audioCtx&&audioCtx.audioSink){audioCtx.audioSink.disconnect();delete audioCtx.audioSink}}catch{}try{remoteScreen.srcObject=null}catch{};remoteScreen.hidden=true; // Release any pending backpressure waiters so in-flight sends don't hang // forever after the bus is closed. They'll re-check fileBus(), find it gone, // and the send loop will abort cleanly. @@ -849,7 +880,7 @@ async function startLocalTestCall(){ try{ callStatus.textContent='Requesting mic…';callStatus.className='call-status ringing'; localStream=await navigator.mediaDevices.getUserMedia(microphoneConstraints()); - callActive=true;callStart=Date.now();callBtn.textContent='End call';callBtn.title='End local mic test';callBtn.disabled=false;muteBtn.hidden=false;micMuted=false;muteBtn.textContent='Mute';applyMicTransmission();setParticipant(participantYou,true);playSound('ring');callStatus.textContent='Testing microphone locally';callStatus.className='call-status live'; + callActive=true;callStart=Date.now();callBtn.textContent='Leave';callBtn.title='End local mic test';callBtn.disabled=false;muteBtn.hidden=false;micMuted=false;muteBtn.textContent='Mute';applyMicTransmission();setParticipant(participantYou,true);playSound('ring');callStatus.textContent='Testing microphone locally';callStatus.className='call-status live';syncVoiceChrome(); callTimerId=setInterval(()=>{const s=Math.floor((Date.now()-callStart)/1000);callTimerEl.textContent=Math.floor(s/60)+':'+String(s%60).padStart(2,'0')},1000); }catch(e){callStatus.textContent='Mic test unavailable';callStatus.className='call-status'}finally{callStarting=false} } @@ -883,12 +914,13 @@ async function startCall(){ if(gen!==callGen||!pc){try{sender.replaceTrack(null)}catch{};if(localStream){localStream.getTracks().forEach(t=>t.stop());localStream=null}return} // endCall/disconnectRoom may have run during a nested await; if pc is gone bail. if(!pc){try{sender.replaceTrack(null)}catch{};if(localStream){localStream.getTracks().forEach(t=>t.stop());localStream=null}return} - callActive=true;callStart=Date.now();setRemoteCallAudio(true);callBtn.textContent='End call';callBtn.title='End voice call';callBtn.disabled=false;muteBtn.hidden=false;micMuted=false;muteBtn.textContent='Mute';muteBtn.title='Mute microphone';applyMicTransmission(); - try{remoteAudio.volume=0}catch{};setCallVolume(volumeSlider.value,false);volumeSlider.hidden=false;volumeValue.hidden=false; + callActive=true;callStart=Date.now();setRemoteCallAudio(true);callBtn.textContent='Leave';callBtn.title='Leave voice call';callBtn.disabled=false;muteBtn.hidden=false;micMuted=false;muteBtn.textContent='Mute';muteBtn.title='Mute microphone';applyMicTransmission(); + try{remoteAudio.volume=0}catch{};setCallVolume(volumeSlider.value,false); setParticipant(participantYou,true);logCallEvent('You joined the call'); playSound('ring');try{send({t:'call-ring'})}catch{} callStatus.textContent='Voice live';callStatus.className='call-status live'; - callTimerId=setInterval(()=>{const s=Math.floor((Date.now()-callStart)/1000);const m=Math.floor(s/60),sec=s%60;callTimerEl.textContent=m+':'+String(sec).padStart(2,'0')},1000); + callTimerId=setInterval(()=>{const s=Math.floor((Date.now()-callStart)/1000);const m=Math.floor(s/60),sec=s%60;callTimerEl.textContent=m+':'+String(sec).padStart(2,'0');if(voiceConnectionDetail)voiceConnectionDetail.textContent=callTimerEl.textContent+' · Pair Voice'},1000); + syncVoiceChrome(); }catch(e){try{send({t:'call-end'})}catch{};endCall(true);const m=String(e?.message||e||'');if(/not\s*found/i.test(m))callStatus.textContent='No mic found — check your microphone connection';else if(/permission|denied|not\s*allowed/i.test(m))callStatus.textContent='Mic access blocked — allow microphone in browser/app settings';else callStatus.textContent='Mic error — '+(e?.message||e);callStatus.className='call-status'; }finally{callStarting=false} } @@ -912,19 +944,84 @@ async function endCall(silent){ // null the srcObject and ontrack never fires again for the same transceiver, // permanently killing audio for the session. callActive=false;micMuted=false;setRemoteCallAudio(false); - callBtn.textContent='Start call';callBtn.title='Start voice call';muteBtn.hidden=true;volumeSlider.hidden=true;volumeValue.hidden=true;callStatus.textContent='Voice off';callStatus.className='call-status'; + callBtn.textContent='Join';callBtn.title='Join voice call';muteBtn.hidden=true;callStatus.textContent='Voice off';callStatus.className='call-status'; if(!silent){callBtn.disabled=!pc&&!LOCAL_TEST_MODE;playSound('leave');try{send({t:'call-end'})}catch{}} + syncVoiceChrome(); } function toggleMute(){ if(!localStream)return; micMuted=!micMuted; applyMicTransmission(); if(micMuted){muteBtn.textContent='Unmute';muteBtn.title='Unmute microphone'}else if(voiceInputModeValue!=='ptt'){muteBtn.textContent='Mute';muteBtn.title='Mute microphone'} + syncVoiceChrome(); } enableRangeDrag(volumeSlider); callBtn.onclick=()=>{if(callActive)endCall(false);else{warmAudio();startCall()}}; muteBtn.onclick=toggleMute; -volumeSlider.oninput=()=>setCallVolume(volumeSlider.value); +const userBarMute=$('#userBarMute'); +const userBarDeafen=$('#userBarDeafen'); +const userBarShare=$('#userBarShare'); +const voiceConnection=$('#voiceConnection'); +const voiceDisconnect=$('#voiceDisconnect'); +const voiceConnectionDetail=$('#voiceConnectionDetail'); +let deafened=false,volumeBeforeDeafen=50; +function syncVoiceChrome(){ + const sharing=!!(typeof screenActive!=='undefined'&&screenActive)||!remoteScreen?.hidden; + voicePanel?.classList.toggle('is-live',!!callActive); + voicePanel?.classList.toggle('is-sharing',sharing); + if(voiceConnection){ + voiceConnection.hidden=!callActive; + if(voiceConnectionDetail){ + const timer=callTimerEl?.textContent; + voiceConnectionDetail.textContent=timer?(timer+' · Pair Voice'):(callStatus?.textContent||'Pair Voice'); + } + } + if(userBarMute){ + userBarMute.disabled=!callActive||!localStream; + userBarMute.classList.toggle('is-muted',!!micMuted); + userBarMute.title=muteBtn.title||(micMuted?'Unmute':'Mute'); + userBarMute.setAttribute('aria-label',userBarMute.title); + } + if(userBarDeafen){ + userBarDeafen.classList.toggle('is-deafened',!!deafened); + userBarDeafen.title=deafened?'Undeafen':'Deafen'; + userBarDeafen.setAttribute('aria-label',userBarDeafen.title); + } + if(userBarShare){ + userBarShare.disabled=!!screenBtn?.disabled; + userBarShare.classList.toggle('is-sharing',!!(typeof screenActive!=='undefined'&&screenActive)); + userBarShare.title=screenBtn?.title||'Share screen'; + userBarShare.setAttribute('aria-label',userBarShare.title); + } + if(headerCall){ + headerCall.disabled=false; + headerCall.title=callActive?'Leave voice':(callBtn?.disabled?'Add a friend to call':'Join voice'); + headerCall.setAttribute('aria-label',headerCall.title); + } + if(headerScreen){ + headerScreen.disabled=!!screenBtn?.disabled; + headerScreen.title=screenBtn?.title||'Share screen'; + headerScreen.setAttribute('aria-label',headerScreen.title); + } +} +if(userBarMute){ + muteBtn.addEventListener('click',()=>setTimeout(syncVoiceChrome,0)); + const muteObserver=new MutationObserver(syncVoiceChrome); + muteObserver.observe(muteBtn,{attributes:true,attributeFilter:['hidden','title']}); + userBarMute.onclick=()=>{if(!userBarMute.disabled)toggleMute()}; +} +if(userBarDeafen){ + userBarDeafen.onclick=()=>{ + deafened=!deafened; + if(deafened){volumeBeforeDeafen=Number(volumeSlider.value)||50;setCallVolume(0,false)} + else setCallVolume(volumeBeforeDeafen,false); + syncVoiceChrome(); + }; +} +if(userBarShare)userBarShare.onclick=()=>{if(!screenBtn.disabled)screenBtn.click()}; +if(voiceDisconnect)voiceDisconnect.onclick=()=>{if(callActive)endCall(false)}; +syncVoiceChrome(); +volumeSlider.oninput=()=>{if(deafened){deafened=false;syncVoiceChrome()}setCallVolume(volumeSlider.value)}; // --- Screen share ------------------------------------------------------------- // Either peer can start/stop screen share, so either peer can drive a @@ -1250,10 +1347,11 @@ const screenViewBar=document.createElement('div');screenViewBar.className='scree const fsBtn=screenViewBar.querySelector('[data-screen-fullscreen]'),screenStage=screenVideos.parentElement;screenStage.classList.add('screen-stage');let nativeShareFullscreen=false; const screenAudioBadge=document.createElement('span');screenAudioBadge.className='screen-audio-badge';screenStage.appendChild(screenAudioBadge);const syncScreenAudioBadge=()=>{screenAudioBadge.textContent=screenStatus.textContent||'Sharing';screenAudioBadge.hidden=!screenIsActive()};new MutationObserver(syncScreenAudioBadge).observe(screenStatus,{childList:true,characterData:true,subtree:true}); function screenIsActive(){return !screenPreview.hidden||!remoteScreen.hidden} -function updateScreenLayout(){const hasLocal=!screenPreview.hidden,hasRemote=!remoteScreen.hidden,fullscreen=!!document.fullscreenElement||screenStage.classList.contains('fs');if(!hasRemote)focusedScreen='local';if(!hasLocal)focusedScreen='remote';if(!hasLocal&&!hasRemote)screenExpanded=false;voicePanel.classList.toggle('screen-sharing',hasLocal||hasRemote);voicePanel.classList.toggle('screen-expanded',screenExpanded&&(hasLocal||hasRemote));screenStage.classList.toggle('screen-expanded-local',focusedScreen==='local');localScreenTile.hidden=!hasLocal;remoteScreenTile.hidden=!hasRemote;fsBtn.hidden=!screenExpanded||!screenIsActive();fsBtn.textContent=fullscreen?'✕':'⛶';fsBtn.title=fullscreen?'Exit fullscreen':'Fullscreen';syncScreenAudioBadge()} +function updateScreenLayout(){const hasLocal=!screenPreview.hidden,hasRemote=!remoteScreen.hidden,fullscreen=!!document.fullscreenElement||screenStage.classList.contains('fs');if(!hasRemote)focusedScreen='local';if(!hasLocal)focusedScreen='remote';if(!hasLocal&&!hasRemote)screenExpanded=false;voicePanel.classList.toggle('screen-sharing',hasLocal||hasRemote);voicePanel.classList.toggle('screen-expanded',screenExpanded&&(hasLocal||hasRemote));voicePanel.classList.toggle('is-sharing',hasLocal||hasRemote||!!screenActive);screenStage.classList.toggle('screen-expanded-local',focusedScreen==='local');localScreenTile.hidden=!hasLocal;remoteScreenTile.hidden=!hasRemote;fsBtn.hidden=!screenExpanded||!screenIsActive();fsBtn.textContent=fullscreen?'✕':'⛶';fsBtn.title=fullscreen?'Exit fullscreen':'Fullscreen';syncScreenAudioBadge();if(typeof syncVoiceChrome==='function')syncVoiceChrome()} function returnToSharePreview(){screenStage.classList.remove('fs');document.body.classList.remove('screen-fullscreen');screenExpanded=false;updateScreenLayout()} function toggleRemoteFs(){if(screenStage.classList.contains('fs')||document.fullscreenElement){if(document.fullscreenElement)document.exitFullscreen().catch(()=>{});else if(nativeShareFullscreen)window.pairEnv?.toggleFullscreen?.();nativeShareFullscreen=false;returnToSharePreview();return}screenStage.classList.add('fs');document.body.classList.add('screen-fullscreen');nativeShareFullscreen=!!window.pairEnv?.toggleFullscreen;window.pairEnv?.toggleFullscreen?.();updateScreenLayout()} screenVideos.addEventListener('click',event=>{if(event.target.closest('input,button,label'))return;const tile=event.target.closest('[data-screen-tile]');if(!tile)return;focusedScreen=tile.dataset.screenTile;if(!screenExpanded)screenExpanded=true;try{if(remoteScreen.volume>0)remoteScreen.muted=false;remoteScreen.play().catch(()=>{})}catch{}updateScreenLayout()}); screenViewBar.onclick=event=>{if(event.target.closest('[data-screen-fullscreen]'))toggleRemoteFs()}; + const screenLayoutObserver=new MutationObserver(updateScreenLayout);screenLayoutObserver.observe(screenPreview,{attributes:true,attributeFilter:['hidden']});screenLayoutObserver.observe(remoteScreen,{attributes:true,attributeFilter:['hidden']});updateScreenLayout(); window.pairEnv?.onFullscreenChange?.(isFullscreen=>{nativeShareFullscreen=!!isFullscreen;if(!isFullscreen)returnToSharePreview()});document.addEventListener('fullscreenchange',()=>{const is=!!document.fullscreenElement;document.body.classList.toggle('screen-fullscreen',is);if(!is)returnToSharePreview();else updateScreenLayout()});document.addEventListener('keydown',e=>{if(e.key!=='Escape')return;if(document.fullscreenElement||screenStage.classList.contains('fs'))toggleRemoteFs();else if(screenExpanded)returnToSharePreview()}); diff --git a/index.html b/index.html index 382ffa8..0aa688d 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + @@ -9,131 +9,308 @@
- - -
-
-
-

DIRECT MESSAGES

-

Pair.

+ +
- - - + -
-
- Add a friendDirect pairing -

01 / CONNECT

Connect directly

E2EE
-

No account or server is needed. Exchange the two pairing codes with your friend; chat, calls, and files then travel peer to peer.

-
-

RECOMMENDED

Direct pairing

NO SERVER
-
-
- 1 -

Create an invite, then send the code to your friend.

- - - -
+ + + + + + +
+
+
+ +

Friend

+ +
Not connected
+
+ +
-
- - -
-

02 / CALL

Voice

-
-
- - + + + + + + +
+
+ Add a FriendDirect pairing +

Connect

Pair directly

E2EE
+

Exchange pairing codes with your friend. Chat, calls, and files travel peer to peer.

+
+
+
+ 1 +

Send this code to your friend.

+ + + +
+
+
+ 2 +

Paste your friend’s code here.

+ + +
+
+

Create an invite, or paste one from your friend.

-
-
+
+ Optional signaling server +

Only when you deliberately run a signaling service.

+
+
+
+ + + +
+
+ + +
+
+
+
+
P
+

Friend

+

This is the beginning of your direct message history with Friend. Messages travel peer to peer.

+ +
+
+
+ + +
+ + Keys generated on connect +
+
-
- -
-
-

03 / CHAT

Private room

DIRECT
-

Messages travel directly between your devices.

-
- + + -
-
WebRTC data channel · AES-GCM payload encryptionKeys generated on connect
-
-
diff --git a/styles.css b/styles.css index 3f56f00..98d20a0 100644 --- a/styles.css +++ b/styles.css @@ -1,366 +1,925 @@ -:root { - --canvas: #0f1115; - --surface: #181b21; - --surface-raised: #20242c; - --surface-hover: #282d36; - --field: #11141a; - --stroke: #303641; - --stroke-quiet: #252a33; - --text: #f5f7fa; - --muted: #a8b0bd; - --subtle: #747d8b; - --accent: #6473f5; - --accent-hover: #7885f8; - --success: #43b581; - --danger: #e0676b; - --focus: #aeb6ff; - --background: radial-gradient(900px 560px at 92% -12%, rgba(100, 115, 245, .18), transparent 62%), radial-gradient(680px 500px at -12% 104%, rgba(53, 181, 129, .08), transparent 64%), #0f1115; +/* Pair — true-black DM shell (Obsidian) */ + +:root, +html[data-theme="obsidian"] { + --bg-floating: #0c0c0e; + --bg-tertiary: #000000; + --bg-secondary: #0a0a0c; + --bg-secondary-alt: #111114; + --bg-primary: #000000; + --bg-mod-hover: rgba(255, 255, 255, 0.04); + --bg-mod-active: rgba(168, 85, 247, 0.12); + --bg-mod-selected: transparent; + --bg-input: #1a1a1f; + --text: #e8e8ed; + --text-strong: #ffffff; + --text-muted: #8e8e99; + --channels-default: #8e8e99; + --accent: #a855f7; + --accent-hot: #e879f9; + --accent-hover: #9333ea; + --accent-soft: rgba(168, 85, 247, 0.22); + --accent-glow: rgba(168, 85, 247, 0.45); + --dm-selected: linear-gradient(90deg, rgba(168, 85, 247, 0.42) 0%, rgba(168, 85, 247, 0.12) 48%, rgba(0, 0, 0, 0) 100%); + --link: #c084fc; + --success: #23a559; + --danger: #f23f43; + --focus: #c084fc; + --rail: #000000; + --sidebar: #0a0a0c; + --chat: #000000; + --composer: #1a1a1f; + --userbar: #08080a; + --shadow: 0 16px 48px rgba(0, 0, 0, 0.65); + --r: 10px; --r-sm: 8px; - --r: 12px; - --r-lg: 16px; + --font: "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif; + --mono: ui-monospace, "Cascadia Mono", Consolas, monospace; + --header-h: 48px; + --rail-w: 72px; + --side-w: 248px; + --user-h: 52px; } -html[data-theme="violet"] { --canvas: #121017; --surface: #1d1924; --surface-raised: #282132; --surface-hover: #322a3e; --field: #15121c; --stroke: #3c3348; --stroke-quiet: #2c2635; --accent: #9a68e8; --accent-hover: #ae7cf2; --focus: #d1b4ff; --background: radial-gradient(860px 530px at 92% -12%, rgba(160, 102, 232, .21), transparent 63%), radial-gradient(650px 520px at -8% 106%, rgba(98, 67, 186, .13), transparent 64%), #121017; } -html[data-theme="forest"] { --canvas: #0d1514; --surface: #16201e; --surface-raised: #1f2b28; --surface-hover: #293733; --field: #101918; --stroke: #32433e; --stroke-quiet: #24322e; --accent: #3da97b; --accent-hover: #55bd8e; --focus: #a5eccd; --background: radial-gradient(900px 560px at 92% -12%, rgba(63, 169, 123, .19), transparent 62%), radial-gradient(720px 560px at -10% 105%, rgba(32, 113, 93, .15), transparent 65%), #0d1514; } -html[data-theme="ember"] { --canvas: #17110f; --surface: #231a18; --surface-raised: #30221e; --surface-hover: #3b2b26; --field: #1b1412; --stroke: #4c3831; --stroke-quiet: #362822; --accent: #dc7655; --accent-hover: #ec8a67; --focus: #ffc4ae; --background: radial-gradient(860px 550px at 90% -12%, rgba(220, 106, 70, .2), transparent 61%), radial-gradient(650px 520px at -10% 105%, rgba(184, 122, 49, .12), transparent 63%), #17110f; } +html[data-theme="midnight"] { + --bg-floating: #0a0c12; + --bg-tertiary: #0f131c; + --bg-secondary: #171c28; + --bg-secondary-alt: #121722; + --bg-primary: #1c2333; + --bg-input: #252d40; + --accent: #6d7cff; + --accent-hot: #8490ff; + --accent-hover: #8490ff; + --accent-soft: rgba(109, 124, 255, 0.18); + --accent-glow: rgba(109, 124, 255, 0.4); + --dm-selected: linear-gradient(90deg, rgba(109, 124, 255, 0.38) 0%, rgba(109, 124, 255, 0.1) 50%, transparent 100%); + --rail: #0f131c; + --sidebar: #171c28; + --chat: #1c2333; + --composer: #252d40; + --userbar: #121722; + --link: #8490ff; + --focus: #8490ff; +} +html[data-theme="violet"] { + --bg-floating: #100e16; + --bg-tertiary: #171320; + --bg-secondary: #201a2c; + --bg-secondary-alt: #1a1524; + --bg-primary: #271f35; + --bg-input: #322744; + --accent: #9a68e8; + --accent-hot: #c4a0f5; + --accent-hover: #ae7cf2; + --accent-soft: rgba(154, 104, 232, 0.18); + --accent-glow: rgba(154, 104, 232, 0.42); + --dm-selected: linear-gradient(90deg, rgba(154, 104, 232, 0.4) 0%, rgba(154, 104, 232, 0.1) 50%, transparent 100%); + --rail: #171320; + --sidebar: #201a2c; + --chat: #271f35; + --composer: #322744; + --userbar: #1a1524; + --link: #c4a0f5; + --focus: #ae7cf2; +} +html[data-theme="forest"] { + --bg-floating: #0b1210; + --bg-tertiary: #101916; + --bg-secondary: #16211d; + --bg-secondary-alt: #121c18; + --bg-primary: #1c2a24; + --bg-input: #25362e; + --accent: #23a559; + --accent-hot: #4ade80; + --accent-hover: #2ec56a; + --accent-soft: rgba(35, 165, 89, 0.18); + --accent-glow: rgba(35, 165, 89, 0.4); + --dm-selected: linear-gradient(90deg, rgba(35, 165, 89, 0.35) 0%, rgba(35, 165, 89, 0.1) 50%, transparent 100%); + --rail: #101916; + --sidebar: #16211d; + --chat: #1c2a24; + --composer: #25362e; + --userbar: #121c18; + --link: #4ade80; + --focus: #2ec56a; +} +html[data-theme="ember"] { + --bg-floating: #140e0c; + --bg-tertiary: #1b1311; + --bg-secondary: #261a17; + --bg-secondary-alt: #1f1512; + --bg-primary: #2d201c; + --bg-input: #3a2923; + --accent: #e06b4a; + --accent-hot: #f0a080; + --accent-hover: #ef7d5c; + --accent-soft: rgba(224, 107, 74, 0.18); + --accent-glow: rgba(224, 107, 74, 0.4); + --dm-selected: linear-gradient(90deg, rgba(224, 107, 74, 0.38) 0%, rgba(224, 107, 74, 0.1) 50%, transparent 100%); + --rail: #1b1311; + --sidebar: #261a17; + --chat: #2d201c; + --composer: #3a2923; + --userbar: #1f1512; + --link: #f0a080; + --focus: #ef7d5c; +} -* { box-sizing: border-box; } -html, body { min-height: 100%; margin: 0; background: var(--background); background-attachment: fixed; color: var(--text); font: 14px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } -body { overflow-x: hidden; } -button, input, textarea, select { font: inherit; } +*, *::before, *::after { box-sizing: border-box; } +html, body { height: 100%; margin: 0; background: var(--bg-tertiary); color: var(--text); font: 16px/1.375 var(--font); } +body { overflow: hidden; } +button, input, textarea, select { font: inherit; color: inherit; } button { - display: inline-flex; - min-height: 40px; - align-items: center; - justify-content: center; - padding: 0 14px; - border: 1px solid transparent; - border-radius: var(--r-sm); - background: var(--surface-raised); - color: var(--text); - font-size: 13px; - font-weight: 720; - letter-spacing: .01em; - line-height: 1; - cursor: pointer; - transition: background .15s ease, border-color .15s ease, color .15s ease, transform .15s ease; -} -button:hover { border-color: #404755; background: var(--surface-hover); } -button:active { transform: translateY(1px); } -button:disabled { cursor: not-allowed; opacity: .45; } -button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } -input, textarea, select { width: 100%; min-width: 0; border: 1px solid var(--stroke); border-radius: var(--r-sm); background: var(--field); color: var(--text); transition: border-color .15s ease, background .15s ease; } -input { height: 40px; padding: 0 12px; } -textarea { min-height: 98px; padding: 11px 12px; resize: vertical; line-height: 1.45; } -input::placeholder, textarea::placeholder { color: #67707e; } -input:hover, textarea:hover, select:hover { border-color: #444c5a; } -input:focus, textarea:focus, select:focus { border-color: #7c89fa; background: #151821; outline: 0; } + display: inline-flex; align-items: center; justify-content: center; gap: 6px; + min-height: 32px; padding: 0 12px; border: 0; border-radius: var(--r-sm); + background: var(--bg-input); color: var(--text-strong); font-size: 14px; font-weight: 500; + cursor: pointer; transition: background .12s ease, color .12s ease, opacity .12s ease, box-shadow .12s ease; +} +button:hover { background: #25252c; } +button:disabled { opacity: .4; cursor: not-allowed; } +button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { + outline: 2px solid var(--focus); outline-offset: 2px; +} +/* Never let display:flex rules un-hide [hidden] nodes (caused Mute/share UI leaking). */ +[hidden], .hidden { display: none !important; } +input, textarea, select { + width: 100%; min-width: 0; border: none; border-radius: var(--r-sm); + background: var(--bg-input); color: var(--text); +} +input { height: 36px; padding: 0 10px; } +textarea { min-height: 82px; padding: 10px; resize: vertical; font-family: var(--mono); font-size: 12px; } +input::placeholder, textarea::placeholder { color: var(--text-muted); } +input:focus, textarea:focus, select:focus { outline: 0; box-shadow: 0 0 0 1px var(--accent); } +.primary, .send { background: var(--accent); color: #fff; } +.primary:hover, .send:hover { background: var(--accent-hover); } +.text-button { background: transparent; color: var(--text-muted); } +.text-button:hover { background: var(--bg-mod-hover); color: var(--text-strong); } +.icon-button { + width: 36px; min-width: 36px; height: 36px; padding: 0; border-radius: 50%; + background: var(--bg-input); color: var(--text-muted); font-size: 22px; line-height: 1; +} +.icon-button:hover { color: #fff; background: #2a2a32; } +.sr-only, .control-label { + position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; + overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; +} +.app-shell { + display: grid; + grid-template-columns: var(--rail-w) var(--side-w) minmax(320px, 1fr); + min-width: 720px; + height: 100vh; + height: 100dvh; + overflow: hidden; + background: var(--bg-tertiary); +} -.shell { width: min(1160px, 100%); margin: 0 auto; padding: 26px 24px 30px; } -.topbar { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 0 3px 20px; } -.eyebrow, .lock, .pill { margin: 0; color: #aeb6ff; font-size: 10px; font-weight: 800; letter-spacing: 1.05px; text-transform: uppercase; } -.topbar h1 { margin: 3px 0 0; color: #fff; font-size: 30px; line-height: 1; letter-spacing: -1.5px; } -.topbar h1 span { color: var(--accent); } -.app-actions { display: flex; align-items: center; gap: 9px; }.connection { display: inline-flex; align-items: center; gap: 8px; padding-right: 5px; color: var(--muted); font-size: 12px; font-weight: 650; } -.connection i { width: 8px; height: 8px; border-radius: 50%; background: #777f8e; } +/* ----- Rail ----- */ +.app-rail { + position: relative; display: flex; flex-direction: column; align-items: center; + width: var(--rail-w); padding: 12px 0; overflow-y: auto; overflow-x: hidden; + background: var(--rail); scrollbar-width: none; +} +.app-rail::-webkit-scrollbar { display: none; } +.rail-top { display: flex; flex-direction: column; align-items: center; gap: 8px; width: 100%; } +.rail-brand, .rail-pill { + position: relative; width: 48px; height: 48px; min-height: 48px; padding: 0; border-radius: 50%; + background: var(--bg-secondary-alt); color: var(--text-muted); font-size: 18px; font-weight: 800; + transition: border-radius .15s ease, background .15s ease, color .15s ease, box-shadow .15s ease; +} +.rail-brand { background: var(--accent); color: #fff; box-shadow: 0 0 18px var(--accent-glow); } +.rail-brand.is-active { border-radius: 16px; } +.rail-brand.is-active::before { + content: ""; position: absolute; left: -14px; top: 50%; width: 4px; height: 40px; + border-radius: 0 4px 4px 0; background: #fff; transform: translateY(-50%); +} +.rail-brand:hover, .rail-pill:hover { border-radius: 16px; color: #fff; } +.rail-add { font-size: 28px; font-weight: 400; color: var(--success); background: var(--bg-secondary-alt); } +.rail-add:hover { background: var(--success); color: #fff; box-shadow: 0 0 14px rgba(35, 165, 89, .45); } +.rail-divider { width: 32px; height: 2px; margin: 4px 0; border-radius: 1px; background: rgba(255,255,255,.06); } + +/* ----- Sidebar ----- */ +.side-panel { + display: flex; flex-direction: column; width: var(--side-w); min-width: 0; + height: 100%; overflow: hidden; background: var(--sidebar); +} +.side-header { + flex: 0 0 var(--header-h); display: flex; align-items: center; + padding: 0 16px; box-shadow: 0 1px 0 rgba(255,255,255,.04); + z-index: 2; +} +.side-header h2 { margin: 0; color: var(--text-strong); font-size: 16px; font-weight: 600; } +.side-search { + flex: 0 0 auto; display: flex; align-items: center; width: calc(100% - 20px); + height: 28px; margin: 10px 10px 8px; padding: 0 8px; border-radius: var(--r-sm); + background: var(--bg-tertiary); color: var(--text-muted); font-size: 13px; font-weight: 500; + text-align: left; justify-content: flex-start; +} +.side-search:hover { background: var(--bg-secondary-alt); color: var(--text); } +.side-scroll { + flex: 1 1 auto; min-height: 0; overflow-x: hidden; overflow-y: auto; + padding: 0 8px 10px; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.08) transparent; +} +.side-section { margin-top: 16px; } +.side-section-head { + display: flex; align-items: center; justify-content: space-between; + min-height: 24px; padding: 0 4px 4px; color: var(--channels-default); + font-size: 11px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase; +} +.side-section-head:hover { color: var(--text); } +.side-mini-btn { + width: 18px; min-width: 18px; height: 18px; min-height: 18px; padding: 0; + border-radius: 3px; background: transparent; color: inherit; font-size: 16px; line-height: 1; +} +.side-mini-btn:hover { background: var(--bg-mod-hover); color: var(--text-strong); } + +.dm-item { + position: relative; display: grid; grid-template-columns: 32px minmax(0, 1fr); align-items: center; gap: 12px; + width: 100%; min-height: 42px; margin-bottom: 2px; padding: 0 8px; border-radius: var(--r-sm); + background: transparent; color: var(--channels-default); text-align: left; font-weight: 500; + overflow: visible; +} +.dm-item:hover { background: var(--bg-mod-hover); color: var(--text); } +.dm-item.is-active { + background: var(--dm-selected); + color: var(--text-strong); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent), 0 0 22px var(--accent-glow); +} +.dm-avatar { width: 32px; height: 32px; font-size: 13px; overflow: visible; } +.dm-item.is-active .dm-avatar { + box-shadow: 0 0 0 2px var(--accent-hot), 0 0 14px var(--accent-glow); +} +.dm-meta { min-width: 0; overflow: hidden; } +.dm-meta strong { display: block; overflow: hidden; font-size: 16px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; } + +/* Voice channel — compact in sidebar; no overlapping controls */ +.voice-channel { + margin-top: 2px; padding: 6px 8px; border-radius: var(--r); + background: transparent; +} +.voice-channel.is-live { + background: rgba(35, 165, 89, 0.06); + box-shadow: inset 0 0 0 1px rgba(35, 165, 89, 0.18); +} +.voice-channel-row { + display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; gap: 8px; align-items: center; + color: var(--channels-default); +} +.voice-icon { display: grid; place-items: center; color: var(--channels-default); } +.voice-channel.is-live .voice-icon { color: var(--success); } +.voice-channel-titles { min-width: 0; display: grid; gap: 1px; } +.voice-channel-titles strong { + color: var(--text); font-size: 14px; font-weight: 600; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.voice-channel:hover .voice-icon { color: var(--text); } +.voice-channel.is-live:hover .voice-icon { color: var(--success); } +.call-status, .call-timer { + display: block; color: var(--text-muted); font-size: 11px; font-weight: 500; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.call-status.live { color: var(--success); } +.call-status.ringing { color: var(--accent); } +.call-timer:empty { display: none; } +.voice-join { + min-width: 52px; min-height: 28px; height: 28px; padding: 0 10px; + border-radius: var(--r-sm); background: var(--success); color: #fff; font-size: 12px; font-weight: 700; +} +.voice-join:hover { filter: brightness(1.08); background: var(--success); } +.voice-channel.is-live .voice-join { background: var(--danger); } +.voice-channel.is-live .voice-join:hover { background: var(--danger); filter: brightness(1.08); } + +.voice-body { display: grid; gap: 6px; margin-top: 6px; } +.voice-participants { display: grid; gap: 1px; } +.voice-participant { + display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: center; gap: 8px; + min-height: 30px; padding: 4px 6px; border-radius: var(--r-sm); color: var(--text-muted); font-size: 13px; +} +.voice-participant:hover { background: var(--bg-mod-hover); color: var(--text); } +.voice-participant .avatar { width: 24px; height: 24px; font-size: 10px; overflow: visible; } +.voice-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; } +.profile-adjust { + min-height: 20px; padding: 0 4px; background: transparent !important; color: var(--text-muted); + font-size: 10px; text-decoration: underline; +} +.friend-participant.is-absent, .self-participant.is-absent { display: none; } + +/* Screen quality/share controls only while sharing; mute lives in user bar */ +.voice-controls { display: none; gap: 6px; padding: 2px 0 0; } +.voice-channel.is-sharing .voice-controls { display: grid; } +#voicePanel.screen-sharing .voice-controls { display: none; } +#muteBtn { display: none !important; } /* kept for JS; mute chrome is userBarMute */ +.control-cluster { min-width: 0; display: grid; gap: 6px; } +.screen-cluster { display: grid; } +.screen-controls { + display: grid; grid-template-columns: 1fr; gap: 6px; align-items: stretch; +} +.voice-action { + width: 100%; min-height: 32px; height: 32px; padding: 0 10px; border-radius: var(--r-sm); + background: var(--bg-input); color: var(--text); font-size: 12px; font-weight: 600; +} +.voice-action:hover { background: #25252c; } +.voice-action.primary { background: var(--success); color: #fff; } +.voice-action.primary:hover { filter: brightness(1.06); background: var(--success); } +.preset-select { + width: 100%; height: 32px; padding: 0 8px; border-radius: var(--r-sm); + background: var(--bg-tertiary); font-size: 12px; color: var(--text); +} +.audio-toggle { + width: 100%; min-height: 28px; height: 28px; padding: 0 8px; justify-content: flex-start; + background: transparent !important; color: var(--text-muted); font-size: 12px; font-weight: 600; +} +.audio-toggle.is-on { color: var(--success); } +.screen-status { + color: var(--text-muted); font-size: 11px; line-height: 1.3; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.volume-slider { width: 100%; accent-color: var(--accent); } +.volume-value { color: var(--text-muted); font-size: 11px; } +.screen-section { position: relative; min-width: 0; } +.screen-videos { display: none; } +.screen-videos:has(video:not([hidden])) { + display: grid; gap: 6px; margin-top: 6px; padding: 6px; + border-radius: var(--r); background: #000; +} +.remote-screen, .screen-preview { display: block; width: 100%; max-height: 140px; object-fit: contain; border-radius: 4px; background: #000; } +.voice-log { display: none; } + +/* Footer: voice connection strip + user bar */ +.side-footer { + flex: 0 0 auto; display: flex; flex-direction: column; z-index: 2; + background: var(--userbar); +} +.voice-connection { + display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 8px 10px; + background: #0d1510; box-shadow: 0 -1px 0 rgba(35, 165, 89, 0.2); +} +.voice-connection[hidden] { display: none !important; } +.voice-connection-meta { flex: 1 1 auto; min-width: 0; display: grid; gap: 1px; } +.voice-connection-meta strong { + color: var(--success); font-size: 12px; font-weight: 700; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.voice-connection-meta span { + color: var(--text-muted); font-size: 11px; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.voice-volume-wrap { + display: flex; align-items: center; gap: 6px; flex: 0 1 120px; min-width: 88px; + color: var(--text-muted); cursor: pointer; +} +.voice-volume-wrap .volume-slider { + flex: 1 1 auto; width: 72px; min-width: 56px; height: 4px; padding: 0; + accent-color: var(--accent); +} +.voice-volume-wrap .volume-value { + flex: 0 0 auto; min-width: 30px; color: var(--text-muted); font-size: 10px; font-variant-numeric: tabular-nums; +} +.voice-disconnect { color: var(--danger) !important; } +.voice-disconnect:hover { background: rgba(242, 63, 67, 0.15) !important; color: #ff7b7f !important; } + +.user-bar { + display: flex; align-items: center; gap: 2px; + min-height: var(--user-h); padding: 0 6px 0 8px; background: var(--userbar); +} +.user-bar-profile { + flex: 1 1 auto; display: grid; grid-template-columns: 32px minmax(0, 1fr); align-items: center; gap: 8px; + min-width: 0; min-height: 40px; padding: 4px; border-radius: var(--r-sm); + background: transparent; text-align: left; +} +.user-bar-profile:hover { background: rgba(255,255,255,.04); } +.user-bar-avatar { width: 32px; height: 32px; font-size: 12px; } +.user-bar-meta { min-width: 0; display: grid; } +.user-bar-meta strong { + overflow: hidden; color: var(--text-strong); font-size: 14px; font-weight: 600; + text-overflow: ellipsis; white-space: nowrap; +} +.user-bar-meta small { + overflow: hidden; color: var(--text-muted); font-size: 12px; + text-overflow: ellipsis; white-space: nowrap; +} +.user-bar-actions { display: flex; flex: 0 0 auto; gap: 0; } +.ubar-btn { + position: relative; width: 32px; min-width: 32px; height: 32px; min-height: 32px; padding: 0; + border-radius: var(--r-sm); background: transparent; color: var(--text-muted); +} +.ubar-btn:hover:not(:disabled) { background: rgba(255,255,255,.06); color: var(--text-strong); } +.ubar-btn:disabled { opacity: .35; } +.ubar-btn.is-muted, .ubar-btn.is-deafened { color: var(--danger); } +.ubar-btn.is-muted::after, .ubar-btn.is-deafened::after, .ubar-btn.is-sharing::after { + content: ""; position: absolute; left: 7px; right: 7px; top: 50%; height: 2px; + background: var(--danger); border-radius: 1px; transform: rotate(-45deg); + box-shadow: 0 0 0 1px var(--userbar); +} +.ubar-btn.is-sharing { color: var(--success); } +.ubar-btn.is-sharing::after { display: none; } + +/* ----- Main chat ----- */ +.app-content { + display: flex; flex-direction: column; min-width: 0; min-height: 0; + height: 100%; overflow: hidden; background: var(--chat); +} +.channel-header { + flex: 0 0 var(--header-h); display: flex; align-items: center; justify-content: space-between; + gap: 12px; padding: 0 8px 0 16px; + box-shadow: 0 1px 0 rgba(255,255,255,.04); + background: var(--chat); z-index: 3; +} +.channel-title { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; } +.channel-hash { color: var(--text-muted); font-size: 22px; font-weight: 700; line-height: 1; } +.channel-header h1 { + margin: 0; color: var(--text-strong); font-size: 16px; font-weight: 600; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.header-divider { width: 1px; height: 24px; background: rgba(255,255,255,.08); flex: 0 0 auto; } +.connection { + display: inline-flex; align-items: center; gap: 6px; min-width: 0; + color: var(--text-muted); font-size: 13px; font-weight: 500; +} +.connection span { + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; +} +.connection i { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: var(--text-muted); } .connection.connected i { background: var(--success); } -.add-friend { gap: 7px; border-color: rgba(255,255,255,.08); background: var(--surface-raised); }.add-friend span { display: grid; width: 18px; height: 18px; place-items: center; border-radius: 50%; background: var(--accent); color: #fff; font-size: 17px; font-weight: 400; line-height: 1; }.add-friend:hover { border-color: var(--accent); background: var(--surface-hover); }.icon-button { width: 40px; min-width: 40px; padding: 0; border-color: rgba(255,255,255,.08); background: var(--surface-raised); color: #c9ced7; font-size: 19px; }.icon-button:hover { border-color: var(--stroke); color: #fff; } +.header-toolbar { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; padding-right: 4px; } +.header-tool { + width: 36px; min-width: 36px; height: 36px; min-height: 36px; padding: 0; + border-radius: var(--r-sm); background: transparent; color: var(--text-muted); +} +.header-tool:hover:not(:disabled) { background: var(--bg-mod-hover); color: var(--text-strong); } +.header-tool:disabled { opacity: .35; } + +.workspace-view { + position: relative; display: flex; flex: 1 1 auto; flex-direction: column; + min-height: 0; overflow: hidden; +} +.workspace { display: flex; flex: 1 1 auto; min-height: 0; overflow: hidden; } +.chat { + display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; min-height: 0; + overflow: hidden; background: transparent; +} + +.messages { + flex: 1 1 auto; min-height: 0; margin: 0; padding: 16px 0 0; + overflow-x: hidden; overflow-y: auto; scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,.08) transparent; +} +.empty { + display: grid; justify-items: start; gap: 8px; max-width: 560px; + margin-top: auto; margin-bottom: 16px; padding: 0 16px; +} +.empty-mark { + display: grid; place-items: center; width: 80px; height: 80px; margin-bottom: 4px; + border-radius: 50%; background: var(--accent); color: #fff; font-size: 42px; font-weight: 800; + box-shadow: 0 0 28px var(--accent-glow); +} +.empty h3 { margin: 0; color: var(--text-strong); font-size: 32px; font-weight: 700; letter-spacing: -.4px; } +.empty p { margin: 0; max-width: 48ch; color: var(--text-muted); font-size: 16px; line-height: 1.375; } +.empty-cta { margin-top: 8px; min-height: 36px; border-radius: var(--r-sm); } + +.message, .message.mine { + display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 0 16px; + max-width: none; margin: 0; padding: 3px 16px 3px 16px; text-align: left; +} +.message:hover { background: rgba(255, 255, 255, 0.03); } +.message-avatar { width: 40px; height: 40px; margin-top: 2px; font-size: 15px; } +.message-content { min-width: 0; padding-top: 1px; } +.message-header { display: flex; align-items: baseline; gap: 8px; min-height: 22px; } +.message-header strong { color: var(--text-strong); font-size: 16px; font-weight: 500; } +.message-header time { color: var(--text-muted); font-size: 12px; font-weight: 500; } +.bubble, .mine .bubble { + display: block; max-width: none; padding: 0; border: 0; border-radius: 0; + background: transparent !important; color: var(--text); font-size: 16px; line-height: 1.375; + white-space: pre-wrap; overflow-wrap: anywhere; text-align: left; +} +.bubble.emoji-only { margin-top: 2px; font-size: 2.6em; } +.meta { margin-top: 4px; color: var(--text-muted); font-size: 12px; } +.message:has(.accept-card), .message:has(.transfer) { display: block; max-width: 560px; } +.message:has(.accept-card) .bubble, .message:has(.transfer) .bubble { + padding: 12px; background: var(--bg-secondary-alt) !important; border-radius: var(--r); +} +.message a { color: var(--link); } + +/* Plump composer: + left · input · GIF/emoji · send */ +.composer { + position: relative; z-index: 4; display: flex; flex-wrap: wrap; align-items: center; gap: 2px; + flex: 0 0 auto; margin: 0 16px 24px; padding: 0 8px 0 10px; min-height: 48px; + border-radius: 12px; background: var(--composer); +} +.composer:focus-within { background: #222228; box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.25); } +.composer .plus-wrap { order: 1; position: relative; flex: 0 0 auto; display: inline-flex; } +.composer input { + order: 2; flex: 1 1 140px; height: 48px; border: 0; background: transparent; + box-shadow: none; font-size: 15px; color: var(--text); +} +.composer input:focus { box-shadow: none; background: transparent; } +.composer .gif-btn { order: 3; } +.composer .emoji-btn { order: 4; } +.composer .send { + order: 5; min-height: 32px; height: 32px; margin-right: 2px; padding: 0 14px; + border-radius: var(--r-sm); font-size: 14px; +} +.composer-btn { + width: 34px; min-width: 34px; min-height: 34px; height: 34px; padding: 0; + border-radius: var(--r-sm); background: transparent; color: var(--text-muted); font-size: 16px; +} +.composer-btn:hover { background: transparent; color: var(--text-strong); } +.composer .plus-btn { + width: 36px; min-width: 36px; height: 36px; border-radius: 50%; + background: transparent; color: var(--text-muted); font-size: 22px; font-weight: 400; +} +.composer .plus-btn:hover { background: var(--accent); color: #fff; } +.gif-attachment { + order: 0; display: flex; flex: 0 0 100%; align-items: center; gap: 8px; + padding: 8px 4px 0; color: var(--text-muted); font-size: 12px; +} +.gif-attachment[hidden] { display: none; } +.gif-attachment img { width: 42px; height: 32px; object-fit: cover; border-radius: var(--r-sm); } +.gif-attachment-name { flex: 1; } +.gif-attachment-remove { + width: 28px; min-width: 28px; min-height: 28px; padding: 0; + background: transparent; color: var(--text-muted); font-size: 20px; +} -.settings-panel { display: grid; grid-template-columns: minmax(190px, .58fr) minmax(0, 1fr) minmax(0, 1fr); gap: 20px; align-items: start; margin: 0 0 16px; padding: 20px; border: 1px solid var(--stroke); border-radius: var(--r-lg); background: rgba(24,27,33,.92); }.settings-panel[hidden] { display: none; }.settings-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }.settings-header h2 { margin: 3px 0 0; font-size: 21px; letter-spacing: -.5px; }.settings-section { min-width: 0; }.settings-section h3 { margin: 1px 0 3px; font-size: 14px; }.settings-section > div > p { margin: 0 0 12px; color: var(--muted); font-size: 12px; }.theme-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }.theme-option { min-height: 70px; align-items: flex-start; justify-content: center; flex-direction: column; gap: 7px; padding: 10px; border-color: var(--stroke-quiet); background: var(--field); color: var(--muted); text-align: left; }.theme-option:hover { background: var(--surface-raised); }.theme-option.selected { border-color: var(--accent); background: var(--surface-raised); color: var(--text); }.theme-option i { display: block; width: 100%; height: 10px; border-radius: 99px; background: #343b52; }.theme-option[data-theme="violet"] i { background: #9a68e8; }.theme-option[data-theme="forest"] i { background: #3da97b; }.theme-option[data-theme="ember"] i { background: #dc7655; }.profile-settings-row { display: flex; align-items: center; gap: 12px; }.settings-avatar { width: 62px; height: 62px; flex: 0 0 auto; font-size: 18px; }.profile-settings-actions { display: flex; min-width: 0; flex-wrap: wrap; gap: 5px; }.profile-settings-actions .text-button { min-height: 30px; }.settings-panel .profile-editor { position: static; width: auto; margin-top: 14px; padding: 12px; border: 1px solid var(--stroke); border-radius: var(--r); background: var(--field); }.settings-panel .profile-editor[hidden] { display: none; }.settings-panel .profile-editor label { margin: 9px 0 4px; }.settings-panel .profile-editor input { height: 5px; padding: 0; accent-color: var(--accent); }.settings-panel .profile-editor button { width: 100%; margin-top: 12px; } +/* Avatars */ +.avatar { + position: relative; display: grid; place-items: center; overflow: visible; + width: 40px; height: 40px; border: 0; border-radius: 50%; + background: hsl(var(--avatar-hue, 280) 48% 42%) center / cover no-repeat; + color: #fff; font-weight: 600; +} +.avatar.has-image .avatar-letter { display: none; } +.avatar.has-image { background-color: var(--bg-tertiary); background-repeat: no-repeat; overflow: hidden; } +.profile-avatar { padding: 0; } +.indicator { + position: absolute; right: -1px; bottom: -1px; width: 10px; height: 10px; + border: 3px solid var(--sidebar); border-radius: 50%; background: #6d6f78; box-sizing: content-box; +} +.indicator.on { background: var(--success); } +.user-bar .indicator { border-color: var(--userbar); } +.dm-item .indicator { border-color: var(--sidebar); } +.dm-item.is-active .indicator { border-color: #1a1024; } +.message-avatar .indicator { display: none; } -.update-banner { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 20px 0 0; padding: 12px 14px; border: 1px solid #424b77; border-radius: var(--r); background: #1d2235; } +/* Update banner */ +.update-banner { + flex: 0 0 auto; display: flex; align-items: center; gap: 12px; + margin: 8px 16px 0; padding: 10px 12px; border-radius: var(--r); + border: 1px solid rgba(168,85,247,.35); background: rgba(168,85,247,.12); +} .update-banner[hidden] { display: none !important; } .update-text { display: flex; min-width: 0; flex-direction: column; gap: 2px; } -.update-text strong { font-size: 13px; } -.update-notes { overflow: hidden; color: #bec5de; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } -.update-actions { display: flex; flex: 0 0 auto; align-items: center; gap: 6px; } -.update-banner.update-downloading,.update-banner.update-installing,.update-banner.update-restarting{border-color:#6677df;background:#202849}.update-banner.update-failed{border-color:#a84d55;background:#351e24}.update-banner.update-failed strong{color:#ffc0c6} +.update-text strong { font-size: 14px; } +.update-notes { overflow: hidden; color: var(--text-muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } +.update-banner.update-failed { border-color: rgba(242,63,67,.4); background: rgba(242,63,67,.12); } -.card { border: 1px solid var(--stroke); border-radius: var(--r-lg); background: var(--surface); } -.connect-drawer { margin-top: 18px; overflow: hidden; } -.connect-drawer > summary { display: flex; align-items: center; min-height: 56px; padding: 0 18px; cursor: pointer; list-style: none; color: #e9edf3; font-size: 14px; font-weight: 760; } -.connect-drawer > summary::-webkit-details-marker { display: none; } -.connect-drawer > summary::before { width: 20px; margin-right: 9px; color: #b5bcff; content: "+"; font-size: 20px; font-weight: 400; text-align: center; } -.connect-drawer > summary span { margin-right: auto; } -.connect-drawer > summary small { color: var(--subtle); font-size: 10px; font-weight: 800; letter-spacing: .85px; text-transform: uppercase; } -.connect-drawer > summary:hover { background: var(--surface-raised); } -.connect-drawer[open] > summary { border-bottom: 1px solid var(--stroke); } -.connect-drawer[open] > summary::before { content: "−"; } -.connect-drawer[open] > :not(summary) { margin-right: 20px; margin-left: 20px; } -.connect-drawer[open] > .section-heading { margin-top: 22px; } -.connect-drawer[open] > .manual-pair { margin-bottom: 20px; } +/* Backdrop + connect modal */ +.panel-backdrop { + position: fixed; inset: 0; z-index: 50; width: 100%; height: 100%; padding: 0; + border: 0; border-radius: 0; background: rgba(0,0,0,.75); cursor: default; +} +.panel-backdrop[hidden] { display: none; } +.connect-drawer:not([open]) { display: none; } +.connect-drawer[open] { + position: fixed; z-index: 60; top: 50%; left: 50%; + width: min(820px, calc(100vw - 40px)); max-height: calc(100vh - 40px); + margin: 0; overflow: auto; transform: translate(-50%, -50%); + border-radius: 12px; background: var(--bg-floating); box-shadow: var(--shadow), 0 0 0 1px rgba(168,85,247,.2); +} +.connect-drawer[open] > summary { + position: sticky; top: 0; z-index: 2; display: flex; align-items: center; + min-height: 52px; padding: 0 18px; cursor: pointer; list-style: none; + border-bottom: 1px solid rgba(255,255,255,.06); background: var(--bg-floating); font-weight: 600; +} +.connect-drawer[open] > summary::-webkit-details-marker { display: none; } +.connect-drawer[open] > summary::before { + display: grid; place-items: center; width: 28px; height: 28px; margin-right: 10px; + border-radius: 50%; background: var(--bg-secondary-alt); color: var(--text); content: "×"; font-size: 20px; +} +.connect-drawer[open] > summary small { + margin-left: auto; color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; +} +.connect-drawer[open] > :not(summary) { margin: 18px 22px; } .section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } -.section-heading h2 { margin: 2px 0 0; font-size: 18px; letter-spacing: -.35px; } -.lock, .pill { align-self: center; padding: 5px 8px; border: 1px solid #3c446d; border-radius: 99px; background: #222741; color: #c6cbff; } -.muted { margin: 10px 0 16px; color: var(--muted); font-size: 13px; } - -.primary-pair { padding: 16px; border: 1px solid var(--stroke); border-radius: var(--r); background: var(--surface-raised); } -.auto-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; } -.auto-title h3 { margin: 2px 0 0; font-size: 16px; } -.status-note { color: var(--subtle); font-size: 10px; font-weight: 800; letter-spacing: .8px; } -.pair-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 14px; } -label { display: block; margin: 0 0 6px; color: #b0b8c5; font-size: 10px; font-weight: 800; letter-spacing: .7px; text-transform: uppercase; } -.pair-step { position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 10px; padding: 14px; border: 1px solid var(--stroke); border-radius: 10px; background: var(--field); } -.step-number { display: grid; grid-row: span 5; width: 24px; height: 24px; margin-top: 1px; place-items: center; border-radius: 50%; background: #30385f; color: #edf0ff; font-size: 12px; font-weight: 800; } -.pair-step label { margin-bottom: 3px; } -.step-copy { margin: 0 0 11px; color: var(--muted); font-size: 12px; line-height: 1.4; } +.section-heading h2 { margin: 2px 0 0; color: var(--text-strong); font-size: 22px; } +.eyebrow { + margin: 0; color: var(--text-muted); font-size: 11px; font-weight: 700; + letter-spacing: .04em; text-transform: uppercase; +} +.lock { + align-self: center; padding: 4px 8px; border-radius: 999px; + background: var(--accent-soft); color: #e9d5ff; font-size: 10px; font-weight: 700; + letter-spacing: .04em; text-transform: uppercase; +} +.muted { margin: 10px 0 16px; color: var(--text-muted); font-size: 14px; } +.primary-pair { padding: 14px; border-radius: var(--r); background: var(--bg-secondary-alt); } +.pair-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } +label { + display: block; margin: 0 0 6px; color: var(--text-muted); font-size: 12px; font-weight: 700; + letter-spacing: .02em; text-transform: uppercase; +} +.pair-step { + display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 10px; + padding: 14px; border-radius: var(--r); background: var(--bg-tertiary); +} +.step-number { + display: grid; place-items: center; grid-row: span 5; width: 24px; height: 24px; + border-radius: 50%; background: var(--accent); color: #fff; font-size: 12px; font-weight: 700; +} +.step-copy { margin: 0 0 10px; color: var(--text-muted); font-size: 13px; text-transform: none; letter-spacing: 0; font-weight: 400; } .pair-step .primary, .pair-step .wide { grid-column: 2; margin: 0 0 10px; } -.pair-step textarea { grid-column: 2; min-height: 78px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 10px; letter-spacing: 0; } -.outgoing-label { margin-top: 1px !important; } -.pair-code-actions { display: flex; grid-column: 2; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; } -.pair-code-actions span { color: var(--subtle); font-size: 10px; } -.primary, .send { border-color: var(--accent); background: var(--accent); color: #fff; } -.primary:hover, .send:hover { border-color: var(--accent-hover); background: var(--accent-hover); } +.pair-step textarea { grid-column: 2; } +.pair-code-actions { display: flex; grid-column: 2; align-items: center; justify-content: space-between; gap: 8px; } +.pair-code-actions span { color: var(--text-muted); font-size: 11px; } .wide { width: 100%; } -.text-button { min-height: 34px; padding: 0 8px; background: transparent; color: #c0c6ff; font-size: 12px; } -.text-button:hover { border-color: transparent; background: #292e38; color: #edf0ff; } -.text-button:disabled { background: transparent; color: var(--subtle); } -.pair-hint { margin: 13px 0 0; padding: 10px 11px; border-left: 3px solid #7581f8; border-radius: 0 var(--r-sm) var(--r-sm) 0; background: #202640; color: #d9def8; font-size: 12px; } -.manual-pair { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--stroke); } -.manual-pair summary { cursor: pointer; color: #d4d9e3; font-size: 12px; font-weight: 700; } -.hint { margin: 8px 0 0; color: var(--subtle); font-size: 12px; } -.auto-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(130px, .65fr) auto auto; gap: 9px; margin-top: 12px; } +.pair-hint { + margin: 12px 0 0; padding: 10px 12px; border-left: 3px solid var(--accent); + border-radius: 0 var(--r-sm) var(--r-sm) 0; background: var(--accent-soft); color: var(--text); font-size: 13px; +} +.manual-pair { margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.06); } +.manual-pair summary { cursor: pointer; color: var(--text-muted); font-size: 13px; font-weight: 600; } +.hint { margin: 8px 0 0; color: var(--text-muted); font-size: 12px; } +.auto-grid { + display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(120px, .7fr) auto auto; gap: 8px; margin-top: 12px; +} .auto-grid button { align-self: end; } -/* Call stage: people stay central; each task gets its own calm, predictable control group. */ -.voice.card { position: relative; min-height: 500px; margin-top: 14px; overflow: hidden; background: var(--surface); } -.voice.card .section-heading { position: absolute; z-index: 2; top: 20px; left: 22px; } -.voice.card .section-heading .eyebrow { display: none; } -.voice.card .section-heading h2 { color: #c8cfda; font-size: 13px; } -.call-timer { position: absolute; z-index: 2; top: 20px; right: 22px; color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; } -.voice-body { position: relative; min-height: 500px; } -.voice-participants { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: min(620px, 100%); margin: 0 auto; padding: 74px 20px 175px; gap: 68px; } -.voice-participant { display: flex; align-items: center; justify-self: center; flex-direction: column; gap: 9px; color: #e8ebf0; font-size: 13px; font-weight: 680; } -.avatar { position: relative; display: grid; width: 142px; height: 142px; overflow: visible; place-items: center; border: 1px solid #464d59; border-radius: 50%; background: #303640 center / cover no-repeat; color: #f8f9fb; font-size: 32px; font-weight: 800; } -.avatar.has-image .avatar-letter { display: none; } -.avatar.has-image { background-repeat: no-repeat; background-color: #242832; } -.avatar:not(.has-image) { background: hsl(var(--avatar-hue, 232) 35% 34%) center / cover no-repeat; } -.profile-avatar { padding: 0; } -.profile-avatar:hover { border-color: #a8b0ff; background-color: #303746; } -.indicator { position: absolute; right: 3px; bottom: 3px; width: 19px; height: 19px; border: 3px solid var(--surface); border-radius: 50%; background: #78818f; } -.indicator.on { background: var(--success); } -.profile-adjust { min-height: 28px; padding: 0 7px; background: transparent !important; color: #c4c9ff; font-size: 11px; text-decoration: underline; } -.voice-controls { position: absolute; z-index: 3; right: 20px; bottom: 20px; left: 20px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; width: min(760px, calc(100% - 40px)); margin: 0 auto; } -.control-cluster { min-width: 0; min-height: 104px; padding: 13px; border: 1px solid var(--stroke); border-radius: 11px; background: var(--surface-raised); } -.control-label { display: block; margin-bottom: 9px; color: #aeb6c4; font-size: 10px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; } -.control-row, .screen-controls { display: flex; min-width: 0; align-items: center; gap: 8px; } -.control-row button, .screen-controls button { flex: 0 0 auto; min-height: 38px; height: 38px; white-space: nowrap; } -.call-cluster #callBtn.primary { min-width: 116px; background: var(--accent); } -.call-cluster #callBtn.primary:hover { background: var(--accent-hover); } -.call-status { display: block; overflow: hidden; margin-top: 8px; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } -.call-status.live { color: #9be3b5; } -.call-status.ringing { color: #c9ceff; } -.screen-controls { position: relative; flex-wrap: nowrap; padding-bottom: 19px; } -.screen-controls #screenBtn { min-width: 112px; } -.preset-select { width: 116px; min-width: 0; height: 38px; padding: 0 8px; color: #e2e5eb; font-size: 11px; } -.audio-toggle { min-width: 78px; color: #cbd1dc; } -.audio-toggle.is-on { color: #b4efc7; } -.screen-status { position: absolute; bottom: 0; left: 0; display: block; overflow: hidden; max-width: 100%; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } -.volume-slider { width: 86px; accent-color: var(--accent); } -.volume-value { min-width: 32px; color: var(--muted); font-size: 11px; } -.screen-section { position: absolute; z-index: 4; top: 54px; right: 18px; } -.screen-videos { position: relative; width: min(360px, calc(100vw - 54px)); padding: 8px; border: 1px solid var(--stroke); border-radius: 10px; background: #0b0d11; } -.screen-videos:not(:has(video:not([hidden]))) { display: none; } -.remote-screen { display: block; width: 100%; max-height: 260px; border-radius: 6px; background: #07080a; } -.screen-preview { position: absolute; right: 15px; bottom: 15px; width: 92px; border: 1px solid #747d8b; border-radius: 5px; } -.fs-btn { position: absolute; top: 13px; right: 13px; min-height: 31px; background: #292e38; } -.profile-editor { position: absolute; z-index: 8; bottom: 18px; left: 20px; width: 220px; padding: 14px; border: 1px solid #424a58; border-radius: var(--r); background: #242832; } -.profile-editor[hidden] { display: none; } -.profile-editor strong { display: block; margin-bottom: 10px; font-size: 13px; } -.profile-editor label { margin: 9px 0 4px; font-size: 9px; } -.profile-editor input { height: 5px; padding: 0; accent-color: var(--accent); } -.profile-editor button { width: 100%; min-height: 34px; margin-top: 11px; } -.voice-log { display: none; } - -.workspace .chat { min-height: 430px; margin-top: 14px; padding: 22px 24px 23px; background: var(--surface); } -.chat .section-heading { align-items: center; } -.chat .section-heading h2 { font-size: 17px; } -.messages { height: 370px; margin: 18px 0 13px; padding: 6px 4px; overflow-y: auto; border-top: 1px solid var(--stroke-quiet); scrollbar-color: #515a68 transparent; } -.empty { padding-top: 96px; color: var(--subtle); text-align: center; font-size: 12px; } -.empty span { display: block; margin-bottom: 6px; color: #a4adff; font-size: 22px; } -.message { max-width: 80%; margin: 0 0 14px; } -.message.mine { margin-left: auto; text-align: right; } -.bubble { display: inline-block; max-width: 100%; padding: 10px 12px; border: 1px solid var(--stroke); border-radius: 11px; background: var(--surface-raised); color: var(--text); font-size: 13px; line-height: 1.45; text-align: left; white-space: pre-wrap; overflow-wrap: anywhere; } -.mine .bubble { border-color: var(--accent); background: var(--accent); } -.emoji-only { padding: 0 !important; border: 0 !important; background: transparent !important; font-size: 2.45em; line-height: 1.15; } -.meta { margin-top: 4px; color: var(--subtle); font-size: 10px; } -.composer { position: relative; display: flex; gap: 6px; padding: 5px 5px 5px 12px; border: 1px solid var(--stroke); border-radius: 10px; background: var(--field); } -.composer { flex-wrap: wrap; } -.gif-attachment { display: flex; flex: 0 0 100%; align-items: center; gap: 8px; min-width: 0; padding: 5px 4px 1px; color: var(--muted); font-size: 12px; } -.gif-attachment[hidden] { display: none; } -.gif-attachment img { width: 42px; height: 32px; object-fit: cover; border-radius: 5px; } -.gif-attachment-name { flex: 1; } -.gif-attachment-remove { min-width: 28px; min-height: 28px; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--muted); font-size: 20px; } -.gif-attachment-remove:hover { background: var(--surface-hover); color: var(--text); } -.composer .gif-attachment ~ input { flex: 1 1 180px; min-width: 0; } -.composer:focus-within { border-color: #7682f8; } -.composer input { height: 38px; border: 0; background: transparent; font-size: 14px; } -.composer input:focus { background: transparent; } -.composer .send { min-height: 38px; border-radius: 7px; } -.composer-btn { min-width: 38px; min-height: 38px; padding: 0; background: transparent; color: #c6ccd6; font-size: 16px; } -.composer-btn:hover { border-color: transparent; background: #2a2f39; } +/* Settings modal */ +.settings-panel { + position: fixed; z-index: 60; top: 50%; left: 50%; + display: grid; grid-template-columns: 200px minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); + gap: 0; width: min(920px, calc(100vw - 48px)); height: min(720px, calc(100vh - 48px)); + margin: 0; padding: 0; overflow: hidden; transform: translate(-50%, -50%); + border-radius: 12px; background: var(--bg-floating); box-shadow: var(--shadow), 0 0 0 1px rgba(168,85,247,.18); +} +.settings-panel[hidden] { display: none; } +.settings-header { + grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; + padding: 16px 20px; background: var(--bg-floating); border-bottom: 1px solid rgba(255,255,255,.06); +} +.settings-header h2 { margin: 2px 0 0; font-size: 20px; color: var(--text-strong); } +.settings-tabs { + display: flex; flex-direction: column; gap: 2px; padding: 12px 12px 16px; + background: var(--bg-secondary); overflow: auto; +} +.settings-tab { + min-height: 34px; justify-content: flex-start; padding: 0 10px; border-radius: var(--r-sm); + background: transparent; color: var(--text-muted); font-size: 15px; font-weight: 500; text-align: left; +} +.settings-tab:hover { background: var(--bg-mod-hover); color: var(--text); } +.settings-tab.active { background: var(--accent-soft); color: var(--text-strong); } +.settings-pages { min-width: 0; overflow: auto; padding: 20px 24px 28px; background: var(--bg-tertiary); } +.settings-page { display: block; } +.settings-page[hidden] { display: none; } +.settings-page h3 { margin: 0 0 4px; color: var(--text-strong); font-size: 20px; } +.settings-page > div > p { margin: 0 0 4px; color: var(--text-muted); font-size: 14px; } +.settings-field { display: grid; gap: 6px; margin-top: 16px; color: var(--text-muted); font-size: 12px; font-weight: 700; } +.settings-field select { height: 36px; } +.settings-inline-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; } +.settings-hint { margin: 12px 0 0; color: var(--text-muted); font-size: 12px; line-height: 1.4; } +.settings-toggle { + display: grid; grid-template-columns: 18px minmax(0, 1fr); column-gap: 10px; + margin-top: 18px; color: var(--text-strong); font-size: 14px; font-weight: 500; + text-transform: none; letter-spacing: 0; +} +.settings-toggle input { width: 16px; height: 16px; margin-top: 2px; accent-color: var(--accent); } +.settings-toggle small { grid-column: 2; margin-top: 2px; color: var(--text-muted); font-size: 12px; font-weight: 400; } +.theme-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 16px; } +.theme-option { + min-height: 72px; flex-direction: column; align-items: flex-start; justify-content: center; + gap: 8px; padding: 12px; border: 1px solid transparent; border-radius: var(--r); + background: var(--bg-secondary-alt); color: var(--text-muted); text-align: left; +} +.theme-option:hover { background: #16161a; } +.theme-option.selected { border-color: var(--accent); color: var(--text-strong); box-shadow: 0 0 16px var(--accent-glow); } +.theme-option i { display: block; width: 100%; height: 10px; border-radius: 999px; background: #404249; } +.theme-option[data-theme="obsidian"] i { background: linear-gradient(90deg, #000, #1a1a1f, #a855f7, #e879f9); } +.theme-option[data-theme="midnight"] i { background: #6d7cff; } +.theme-option[data-theme="violet"] i { background: #9a68e8; } +.theme-option[data-theme="forest"] i { background: #23a559; } +.theme-option[data-theme="ember"] i { background: #e06b4a; } +.profile-name-field { display: grid; gap: 6px; margin-top: 16px; color: var(--text-muted); font-size: 12px; font-weight: 700; } +.profile-settings-row { display: flex; align-items: center; gap: 14px; margin-top: 14px; } +.settings-avatar { width: 80px; height: 80px; font-size: 28px; } +.profile-settings-actions { display: flex; flex-wrap: wrap; gap: 8px; } +.settings-panel .profile-editor { + margin-top: 14px; padding: 12px; border-radius: var(--r); background: var(--bg-secondary-alt); +} +.settings-panel .profile-editor[hidden] { display: none; } +.settings-panel .profile-editor label { margin: 8px 0 4px; } +.settings-panel .profile-editor input { height: 5px; padding: 0; accent-color: var(--accent); } +.settings-panel .profile-editor button { width: 100%; margin-top: 12px; } +.keybind-button { justify-content: flex-start; } +.settings-field input[type=range] { + --range-fill: 15%; width: 100%; height: 20px; padding: 0; border: 0; border-radius: 999px; + appearance: none; -webkit-appearance: none; + background: linear-gradient(to right, var(--accent) 0 var(--range-fill), var(--bg-input) var(--range-fill) 100%); +} +.settings-field input[type=range]::-webkit-slider-thumb { + width: 16px; height: 16px; border: 0; border-radius: 50%; background: #fff; + appearance: none; -webkit-appearance: none; +} -.emoji-picker, .gif-picker, .plus-popup { position: absolute; z-index: 15; border: 1px solid #424a57; border-radius: var(--r); background: #232731; } -.emoji-picker { right: 0; bottom: calc(100% + 9px); width: 310px; overflow: hidden; } -.emoji-tabs, .gif-picker-tabs { display: flex; gap: 3px; padding: 8px 9px 0; border-bottom: 1px solid var(--stroke); } -.emoji-tab, .gif-picker-tab { min-height: 31px; padding: 0 9px; border-radius: 6px 6px 0 0; background: transparent; color: var(--muted); font-size: 11px; } -.emoji-tab.active, .gif-picker-tab.active { border-color: transparent; background: #343943; color: #fff; } -.emoji-body { display: grid; grid-template-columns: repeat(8, 1fr); gap: 3px; max-height: 220px; padding: 9px; overflow-y: auto; } -.emoji-item { min-height: 31px; padding: 0; border-color: transparent; background: transparent; font-size: 18px; } -.gif-picker { right: 0; bottom: calc(100% + 9px); width: 360px; overflow: hidden; } -.gif-search-row { padding: 9px; border-bottom: 1px solid var(--stroke); } -.gif-search-input { height: 35px; } -.gif-results { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; max-height: 300px; padding: 8px; overflow-y: auto; } -.gif-result { position: relative; aspect-ratio: 1; min-height: 0; padding: 0; overflow: hidden; border-radius: 7px; background: #303640; } +/* Pickers */ +.emoji-picker, .gif-picker, .plus-popup { + position: absolute; z-index: 20; right: 0; bottom: calc(100% + 8px); + border-radius: 12px; background: var(--bg-floating); box-shadow: var(--shadow); + border: 1px solid rgba(255,255,255,.06); +} +.emoji-picker { width: 360px; overflow: hidden; } +.emoji-tabs, .gif-picker-tabs { + display: flex; gap: 4px; padding: 8px; overflow-x: auto; + border-bottom: 1px solid rgba(255,255,255,.06); +} +.emoji-tab, .gif-picker-tab { + min-height: 32px; padding: 0 10px; border-radius: var(--r-sm); + background: transparent; color: var(--text-muted); font-size: 13px; +} +.emoji-tab.active, .gif-picker-tab.active { background: var(--accent-soft); color: #fff; } +.emoji-body { max-height: 300px; overflow-y: auto; } +.emoji-page { padding: 12px; } +.emoji-page.hidden { display: none; } +.emoji-category-title { margin: 0 0 8px; color: var(--text-muted); font-size: 12px; font-weight: 700; text-transform: uppercase; } +.emoji-grid { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 2px; } +.emoji-item { min-height: 36px; padding: 0; border-radius: var(--r-sm); background: transparent; font-size: 20px; } +.emoji-item:hover { background: var(--bg-mod-hover); } +.gif-picker { width: 360px; overflow: hidden; } +.gif-search-row { padding: 8px; border-bottom: 1px solid rgba(255,255,255,.06); } +.gif-search-input { height: 34px; } +.gif-results { + display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; + max-height: 300px; padding: 8px; overflow-y: auto; +} +.gif-result { + position: relative; aspect-ratio: 1; min-height: 0; padding: 0; overflow: hidden; + border-radius: var(--r-sm); background: var(--bg-secondary-alt); +} .gif-result img { width: 100%; height: 100%; object-fit: cover; } -.gif-star { position: absolute; top: 5px; right: 5px; display: grid; width: 22px; height: 22px; min-height: 0; padding: 0; place-items: center; border: 1px solid rgba(255,255,255,.36); border-radius: 50%; background: rgba(12,14,18,.88); color: #c7ced8; font-size: 16px; line-height: 1; text-shadow: 0 1px 2px #000; cursor: pointer; } -.gif-star:hover, .gif-star:focus-visible { border-color: #ffd15a; color: #ffe28a; background: rgba(58,45,13,.96); outline: none; transform: scale(1.08); } -.gif-star.on { border-color: #ffd15a; background: #f6bd35; color: #352300; text-shadow: none; box-shadow: 0 0 0 2px rgba(18,20,26,.75), 0 2px 8px rgba(0,0,0,.45); } -.gif-hint { grid-column: 1 / -1; padding: 12px; color: var(--muted); font-size: 12px; text-align: center; } -.plus-popup { right: 0; bottom: calc(100% + 9px); min-width: 158px; padding: 5px; } -.plus-opt { display: block; width: 100%; min-height: 34px; border-color: transparent; background: transparent; text-align: left; } -.plus-opt:hover { background: #343943; } -.hidden { display: none !important; } +.gif-star { + position: absolute; top: 4px; right: 4px; display: grid; place-items: center; + width: 22px; height: 22px; min-height: 0; padding: 0; border-radius: 50%; + border: 1px solid rgba(255,255,255,.25); background: rgba(0,0,0,.75); color: #ddd; font-size: 13px; +} +.gif-star.on { background: #f6bd35; color: #352300; border-color: #f6bd35; } +.gif-hint { grid-column: 1 / -1; padding: 12px; color: var(--text-muted); font-size: 13px; text-align: center; } +.plus-popup { min-width: 168px; padding: 6px; left: 0; right: auto; } +.plus-opt { display: block; width: 100%; min-height: 34px; justify-content: flex-start; background: transparent; } +.plus-opt:hover { background: var(--bg-mod-hover); } + +.embed-img { display: block; max-width: min(100%, 400px); max-height: 300px; margin-top: 6px; border-radius: var(--r); } +.embed-yt { width: min(100%, 420px); margin-top: 6px; aspect-ratio: 16/9; } +.embed-yt iframe { width: 100%; height: 100%; border: 0; border-radius: var(--r); } +.gif-attachment-message { position: relative; display: block; width: min(100%, 400px); margin-top: 6px; } +.gif-attachment-message img { + display: block; width: 100%; max-height: 300px; object-fit: contain; + border-radius: var(--r); background: var(--bg-secondary-alt); +} +.gif-message-favorite { + position: absolute; top: 8px; right: 8px; display: grid; place-items: center; + width: 24px; height: 24px; padding: 0; border-radius: 50%; + border: 1px solid rgba(255,255,255,.3); background: rgba(0,0,0,.75); color: #ddd; +} +.gif-message-favorite.on { background: #f6bd35; color: #352300; border-color: #f6bd35; } -.embed-img { display: block; max-width: min(100%, 380px); max-height: 280px; margin-top: 7px; border-radius: 8px; } -.embed-yt { width: min(100%, 420px); margin-top: 7px; aspect-ratio: 16 / 9; } -.embed-yt iframe { width: 100%; height: 100%; border: 0; border-radius: 8px; } .transfer { min-width: 220px; text-align: left; } .transfer-top, .accept-top, .transfer-stats { display: flex; justify-content: space-between; gap: 10px; } .transfer-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.transfer-status, .transfer-stats, .transfer-peer { color: var(--muted); font-size: 10px; } +.transfer-status, .transfer-stats, .transfer-peer { color: var(--text-muted); font-size: 11px; } .transfer-status.declined, .transfer-status.failed { color: #ff979c; } -.bar { height: 5px; margin-top: 8px; overflow: hidden; border-radius: 99px; background: #12151a; } -.bar i { display: block; width: 0; height: 100%; background: #8e98ff; transition: width .2s; } +.bar { height: 5px; margin-top: 8px; overflow: hidden; border-radius: 999px; background: var(--bg-input); } +.bar i { display: block; width: 0; height: 100%; background: var(--accent); transition: width .2s; } .transfer-stats { margin-top: 5px; } -.transfer-btns, .accept-btns { display: flex; justify-content: flex-end; gap: 7px; margin-top: 8px; } -.accept-hint { margin: 8px 0; color: var(--muted); font-size: 12px; } -.accept-btns button { min-height: 33px; } +.transfer-btns, .accept-btns { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; } +.accept-hint { margin: 8px 0; color: var(--text-muted); font-size: 13px; } -.screen-source-modal { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; padding: 22px; background: rgba(0, 0, 0, .65); } -.screen-source-dialog { width: min(720px, 100%); max-height: min(80vh, 760px); padding: 20px; overflow-y: auto; border: 1px solid #444c59; border-radius: var(--r-lg); background: #20242c; } -.screen-source-dialog h3 { margin: 0 0 16px; font-size: 17px; } +/* Screen share modal + overlay */ +.screen-source-modal { + position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; + padding: 24px; background: rgba(0,0,0,.8); +} +.screen-source-dialog { + width: min(720px, 100%); max-height: min(80vh, 760px); padding: 20px; overflow-y: auto; + border-radius: 12px; background: var(--bg-floating); box-shadow: var(--shadow); +} +.screen-source-dialog h3 { margin: 0 0 16px; font-size: 18px; color: var(--text-strong); } .screen-source-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; } -.screen-source-option { display: block; min-height: 0; padding: 8px; border-color: var(--stroke); background: #16191f; color: #e5e8ed; font-size: 12px; text-align: center; } -.screen-source-option:hover { border-color: #7885f8; background: #292e38; } -.screen-source-option img { display: block; width: 100%; margin-bottom: 7px; border-radius: 5px; } +.screen-source-option { + display: block; min-height: 0; padding: 8px; border-radius: var(--r); + background: var(--bg-secondary-alt); color: var(--text); font-size: 12px; text-align: center; +} +.screen-source-option:hover { background: var(--bg-input); outline: 1px solid var(--accent); } +.screen-source-option img { display: block; width: 100%; margin-bottom: 7px; border-radius: var(--r-sm); } .screen-source-cancel { margin-top: 14px; } -.screen-volume { position: absolute; z-index: 11; right: 12px; bottom: 52px; display: none; align-items: center; padding: 9px 11px; border: 1px solid #444c59; border-radius: 8px; background: #232731; } -.screen-volume span { margin-right: 8px; color: var(--muted); font-size: 11px; } +.share-start-options { display: flex; flex-wrap: wrap; gap: 10px; margin: -4px 0 16px; } +.share-start-options label { display: grid; gap: 5px; color: var(--text-muted); font-size: 12px; font-weight: 700; } +.share-start-options select { min-width: 104px; height: 34px; } +.share-start-options .share-audio-option { + display: flex; align-items: center; flex-wrap: wrap; gap: 7px; padding-top: 16px; color: var(--text); + text-transform: none; letter-spacing: 0; font-weight: 500; +} +.share-start-options .share-audio-hint { flex: 1 1 100%; margin: 0 0 0 22px; color: var(--text-muted); font-size: 11px; font-weight: 400; } +.screen-volume { + position: absolute; z-index: 11; right: 12px; top: 12px; display: none; align-items: center; + padding: 8px 10px; border-radius: var(--r-sm); background: rgba(0,0,0,.85); +} +.screen-volume span { margin-right: 8px; color: var(--text-muted); font-size: 11px; } .screen-volume input { width: 84px; height: 5px; padding: 0; accent-color: var(--accent); } -footer { display: flex; justify-content: space-between; gap: 16px; margin: 16px 2px 0; color: var(--subtle); font-size: 10px; } -.sr-only { position: absolute !important; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; } +/* Screen share expands into chat column only */ +#voicePanel.screen-sharing { + position: fixed; z-index: 40; + top: calc(var(--header-h) + 8px); right: 16px; bottom: 88px; + left: calc(var(--rail-w) + var(--side-w) + 16px); + margin: 0; padding: 12px; border-radius: 12px; + background: rgba(0,0,0,.94); box-shadow: var(--shadow); overflow: hidden; +} +#voicePanel.screen-sharing .voice-channel-row, +#voicePanel.screen-sharing .voice-participants { display: none; } +#voicePanel.screen-sharing .screen-section, +#voicePanel.screen-sharing .screen-videos { + display: grid; width: 100%; height: 100%; margin: 0; padding: 8px; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 8px; +} +.screen-tile { + position: relative; min-width: 0; min-height: 0; overflow: hidden; + border-radius: var(--r); background: #000; cursor: pointer; outline: 1px solid rgba(255,255,255,.08); +} +.screen-tile video { + position: relative !important; display: block !important; width: 100% !important; height: 100% !important; + max-height: none !important; object-fit: contain; border: 0 !important; border-radius: 0 !important; +} +.screen-tile-name { + position: absolute; z-index: 2; left: 10px; bottom: 10px; padding: 4px 8px; border-radius: 6px; + background: rgba(0,0,0,.75); color: #fff; font-size: 12px; font-weight: 600; +} +.screen-tile-avatar { + position: absolute; z-index: 2; right: 10px; bottom: 10px; display: grid; place-items: center; + width: 28px; height: 28px; border-radius: 50%; background: var(--accent); color: #fff; font-size: 11px; font-weight: 700; +} +.screen-view-bar { + display: none; gap: 6px; +} +.screen-view-bar[hidden] { display: none !important; } +#voicePanel.screen-sharing .screen-view-bar:not([hidden]) { + position: absolute; z-index: 7; right: 16px; bottom: 14px; display: flex; +} +.screen-view-bar button { + min-height: 30px; padding: 0 10px; background: rgba(20,20,24,.95); color: var(--text-muted); font-size: 12px; +} +.screen-view-bar button[hidden] { display: none !important; } +.screen-view-bar button.active, .screen-view-bar button:hover { background: var(--accent); color: #fff; } +.remote-screen.fs, .screen-preview.fs, .screen-tile.fs { + position: fixed !important; z-index: 10000 !important; inset: 0 !important; + width: 100vw !important; height: 100vh !important; border: 0 !important; border-radius: 0 !important; + background: #000 !important; object-fit: contain !important; +} -@media (max-width: 820px) { - .settings-panel { grid-template-columns: 1fr 1fr; }.settings-header { grid-column: 1 / -1; } - .voice.card, .voice-body { min-height: 530px; } - .voice-participants { padding-bottom: 205px; } - .voice-controls { grid-template-columns: 1fr; width: min(520px, calc(100% - 40px)); } +/* Motion */ +.friend-participant.is-entering, .self-participant.is-entering { animation: pair-in .28s ease both; } +.friend-participant.is-leaving, .self-participant.is-leaving { pointer-events: none; animation: pair-out .28s ease both; } +@keyframes pair-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } +@keyframes pair-out { to { opacity: 0; transform: scale(.92); } } +@keyframes pair-msg { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } +.message { animation: pair-msg .16s ease both; } +.settings-panel:not([hidden]), .connect-drawer[open] { animation: pair-in .18s ease both; } +html[data-reduce-motion="true"] *, +html[data-reduce-motion="true"] *::before, +html[data-reduce-motion="true"] *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } } -@media (max-width: 700px) { - .shell { padding: 20px 14px 24px; } - .settings-panel { grid-template-columns: 1fr; padding: 16px; }.settings-header { grid-column: auto; } +/* Responsive */ +@media (max-width: 980px) { + :root { --side-w: 220px; } + #voicePanel.screen-sharing { left: calc(var(--rail-w) + var(--side-w) + 12px); } +} +@media (max-width: 760px) { + .app-shell { grid-template-columns: var(--rail-w) minmax(0, 1fr); } + .side-panel { display: none; } .pair-grid { grid-template-columns: 1fr; } .auto-grid { grid-template-columns: 1fr 1fr; } .auto-grid .auto-field:first-child { grid-column: 1 / -1; } - .connect-drawer[open] > :not(summary) { margin-right: 15px; margin-left: 15px; } - .voice.card, .workspace .chat { border-radius: var(--r); } - .voice.card, .voice-body { min-height: 500px; } - .voice-participants { padding-top: 70px; padding-bottom: 190px; gap: 30px; } - .avatar { width: 112px; height: 112px; font-size: 27px; } - .screen-section { right: 10px; } - .screen-videos { width: min(300px, calc(100vw - 42px)); } - .messages { height: 340px; } + .settings-panel { + grid-template-columns: 1fr; width: calc(100vw - 32px); height: min(80vh, 680px); + } + .settings-tabs { flex-direction: row; overflow-x: auto; border-bottom: 1px solid rgba(255,255,255,.06); } + #voicePanel.screen-sharing { left: calc(var(--rail-w) + 8px); right: 8px; } } - -@media (max-width: 500px) { - .topbar { padding-bottom: 17px; } - .topbar h1 { font-size: 28px; } - .connection { display: none; }.app-actions { gap: 6px; }.add-friend { min-width: 40px; padding: 0 10px; font-size: 0; }.add-friend span { width: 20px; height: 20px; font-size: 19px; }.icon-button { width: 40px; min-width: 40px; } - .update-banner { align-items: flex-start; flex-direction: column; } - .primary-pair { padding: 12px; } - .pair-step { padding: 12px; } - .pair-code-actions { align-items: flex-start; flex-direction: column; } - .voice.card, .voice-body { min-height: 535px; } - .voice-participants { padding-top: 66px; padding-bottom: 235px; gap: 17px; } - .avatar { width: 94px; height: 94px; font-size: 23px; } - .voice-controls { right: 12px; bottom: 12px; left: 12px; width: auto; gap: 8px; } - .control-cluster { min-height: 92px; padding: 11px; } - .control-row, .screen-controls { gap: 6px; } - .call-cluster #callBtn.primary, .screen-controls #screenBtn { min-width: 0; flex: 1 1 auto; } - .preset-select { width: 100px; } - .audio-toggle { min-width: 72px; } - .workspace .chat { padding: 18px 15px; } - .messages { height: 320px; } - .message { max-width: 88%; } - .gif-picker { width: min(350px, calc(100vw - 30px)); } - .emoji-picker { width: min(310px, calc(100vw - 30px)); } - footer { align-items: flex-start; flex-direction: column; gap: 4px; } +@media (max-width: 520px) { + .channel-header { padding-left: 12px; } + .composer { margin: 0 10px 16px; } + .message, .message.mine { padding-left: 10px; padding-right: 10px; gap: 0 12px; } + .empty h3 { font-size: 24px; } + .header-divider, .connection span { display: none; } } -/* ------------------------------------------------------------------------- - Workspace redesign: a single conversation canvas with a slim navigation - rail. Pairing and settings are focused views, not permanent card sections. - ------------------------------------------------------------------------- */ -html,body{min-height:100%;height:100%;background:var(--canvas)} -body{background:var(--background);background-attachment:fixed}.app-shell{display:grid;grid-template-columns:72px minmax(0,1fr);min-height:100vh}.app-rail{position:sticky;top:0;display:flex;height:100vh;min-height:520px;flex-direction:column;justify-content:space-between;padding:14px 11px;border-right:1px solid var(--stroke-quiet);background:rgba(14,16,20,.76)}.rail-top,.rail-bottom{display:flex;align-items:center;flex-direction:column;gap:12px}.rail-brand,.rail-button{display:grid;width:48px;height:48px;min-height:48px;padding:0;place-items:center;border:0;border-radius:16px;background:#252a33;color:#cdd3de;font-size:20px;font-weight:800;transition:border-radius .16s ease,background .16s ease,transform .16s ease}.rail-brand{background:var(--accent);color:#fff;font-size:18px}.rail-button:hover{border:0;border-radius:13px;background:var(--surface-hover);color:#fff}.rail-add{background:var(--surface-raised);font-size:27px;font-weight:400}.rail-add:hover{background:var(--accent)}.app-content{display:flex;min-width:0;min-height:100vh;flex-direction:column;padding:22px 34px 14px}.topbar{min-height:44px;padding:0 0 17px;border-bottom:1px solid var(--stroke-quiet)}.topbar h1{font-size:23px;letter-spacing:-.9px}.brand-lockup .eyebrow{font-size:9px}.app-actions{gap:0}.connection{padding:6px 0;color:var(--subtle);font-size:11px}.update-banner{margin:0 0 14px;border-radius:10px}.workspace-view{display:flex;min-height:0;flex:1;flex-direction:column}.connect-drawer:not([open]){display:none}.connect-drawer[open]{position:fixed;z-index:60;top:50%;left:50%;width:min(880px,calc(100vw - 48px));max-height:calc(100vh - 48px);margin:0!important;overflow:auto;transform:translate(-50%,-50%);border-color:var(--stroke);border-radius:18px;background:var(--surface)}.connect-drawer[open]>summary{position:sticky;top:0;z-index:2;background:var(--surface);border-bottom:1px solid var(--stroke)}.connect-drawer[open]>:not(summary){margin-right:24px;margin-left:24px}body:has(.connect-drawer[open])::before{position:fixed;z-index:50;inset:0;background:rgba(4,5,8,.72);content:""}.primary-pair{border:0;background:var(--field)}.pair-step{background:var(--surface)}.manual-pair{padding-bottom:24px}.settings-panel{grid-template-columns:minmax(220px,.55fr) minmax(0,1fr) minmax(0,1fr);max-width:1120px;min-height:calc(100vh - 112px);margin:0;padding:36px 20px 20px;border:0;border-radius:0;background:transparent}.settings-panel::before{position:absolute;content:""}.settings-header{padding-right:22px}.settings-header h2{font-size:28px}.settings-section{padding:17px;border:1px solid var(--stroke-quiet);border-radius:14px;background:rgba(24,27,33,.48)}body.settings-open .workspace-view{display:none}.theme-option{min-height:86px;border-radius:10px}.profile-settings-row{align-items:flex-start;flex-direction:column}.profile-settings-actions{width:100%}.profile-settings-actions button:first-child{width:100%;justify-content:center}.settings-avatar{width:76px;height:76px}.settings-panel .profile-editor{background:var(--surface)} - -/* The call area is a compact contextual dock. The conversation stays primary. */ -.voice.card{min-height:104px;margin:0;border:0;border-bottom:1px solid var(--stroke-quiet);border-radius:0;background:transparent}.voice.card .section-heading{display:none}.voice-body{display:flex;min-height:104px;align-items:center;gap:20px}.voice-participants{display:flex;width:auto;margin:0;padding:0;gap:14px}.voice-participant{display:grid;grid-template-columns:42px auto;align-items:center;justify-self:auto;gap:0 9px;color:var(--muted);font-size:12px}.voice-participant .avatar{grid-row:span 2;width:42px;height:42px;font-size:14px}.voice-participant>span:not(.avatar){font-weight:750}.profile-adjust{grid-column:2;justify-content:flex-start;min-height:20px;padding:0;color:var(--subtle);font-size:10px}.indicator{right:-1px;bottom:-1px;width:13px;height:13px;border-width:2px;border-color:var(--canvas)}.voice-controls{position:static;display:flex;width:auto;margin-left:auto;gap:6px}.control-cluster{display:block;min-height:0;padding:0;border:0;border-radius:0;background:transparent}.control-label,.call-status,.screen-status{display:none}.control-row,.screen-controls{gap:6px;padding:0}.screen-controls{padding-bottom:0}.control-row button,.screen-controls button{min-height:38px;height:38px}.call-cluster #callBtn.primary,.screen-controls #screenBtn{min-width:0;background:var(--surface-raised);border-color:transparent;color:#e3e7ef}.call-cluster #callBtn.primary:hover,.screen-controls #screenBtn:hover{background:var(--accent);color:#fff}.preset-select{display:none}.audio-toggle{min-width:0;padding:0 11px;background:transparent!important;color:var(--muted)}.audio-toggle.is-on{color:#9ce3b8}.volume-slider{width:72px}.screen-section{top:112px;right:0}.screen-videos{border-radius:12px}.profile-editor{position:static} - -/* Chat owns the canvas rather than living in a floating box. */ -.workspace{display:flex;min-height:0;flex:1}.workspace .chat{display:flex;min-height:0;flex:1;flex-direction:column;margin:0;padding:20px 0 0;border:0;border-radius:0;background:transparent}.chat .section-heading{flex:0 0 auto;padding:0 2px 14px;border-bottom:1px solid var(--stroke-quiet)}.chat .section-heading .eyebrow{font-size:9px}.chat .section-heading h2{font-size:16px}.messages{height:auto;min-height:0;flex:1;margin:0;padding:18px 3px}.empty{padding-top:clamp(80px,20vh,240px)}.composer{flex:0 0 auto;margin:0 0 1px;border-radius:12px;background:var(--surface)}.composer:focus-within{background:var(--surface-raised)}.composer input{height:42px}.composer .send{min-width:62px}.bubble{border-color:transparent;background:var(--surface-raised)}.mine .bubble{background:var(--accent)}footer{flex:0 0 auto;margin:10px 2px 0;padding-top:8px;border-top:1px solid var(--stroke-quiet)} - -@media(max-width:780px){.app-shell{grid-template-columns:60px minmax(0,1fr)}.app-rail{padding:10px 6px}.rail-brand,.rail-button{width:46px;height:46px;min-height:46px}.app-content{padding:18px 20px 12px}.settings-panel{grid-template-columns:1fr 1fr;padding:22px 0}.settings-header{grid-column:1/-1}.voice-controls{gap:4px}.audio-toggle{font-size:0}.audio-toggle::before{content:"♫";font-size:16px}.connect-drawer[open]{width:min(760px,calc(100vw - 28px));max-height:calc(100vh - 28px)}} -@media(max-width:570px){.app-shell{grid-template-columns:56px minmax(0,1fr)}.app-rail{padding:8px 4px}.rail-brand,.rail-button{width:44px;height:44px;min-height:44px;border-radius:14px}.app-content{padding:16px 14px 10px}.topbar h1{font-size:21px}.settings-panel{grid-template-columns:1fr;min-height:0;padding:18px 0}.settings-header{grid-column:auto}.voice-body{min-height:96px;gap:8px}.voice-participants{gap:8px}.voice-participant{grid-template-columns:36px auto;gap:0 6px;font-size:11px}.voice-participant .avatar{width:36px;height:36px;font-size:12px}.voice-controls{align-items:center;margin-left:auto}.call-cluster #callBtn.primary{min-width:38px;padding:0 11px;font-size:0}.call-cluster #callBtn.primary::before{content:"☎";font-size:15px}.screen-controls #screenBtn{min-width:38px;padding:0 11px;font-size:0}.screen-controls #screenBtn::before{content:"↗";font-size:18px}.volume-slider,.volume-value,.profile-adjust{display:none}.workspace .chat{padding-top:16px}.composer{padding-left:7px}.composer .send{min-width:48px;padding:0 9px}.connect-drawer[open]>:not(summary){margin-right:14px;margin-left:14px}} - -/* Focused panels have a real click-away layer, not a decorative dim overlay. */ -.panel-backdrop{position:fixed;z-index:50;inset:0;width:100%;height:100%;min-height:0;padding:0;border:0;border-radius:0;background:rgba(4,6,10,.66);cursor:default}.panel-backdrop[hidden]{display:none}body:has(.connect-drawer[open])::before{display:none}.connect-drawer[open]{z-index:60}.connect-drawer[open]>summary::before{display:grid;width:28px;height:28px;margin-right:10px;place-items:center;border:1px solid #7d535a;border-radius:8px;background:#45272c;color:#ffb8bd;font-size:24px;line-height:1}.settings-panel{position:fixed;z-index:60;top:18px;bottom:18px;left:92px;display:grid;max-width:none;width:min(860px,calc(100vw - 126px));min-height:0;margin:0;overflow:auto;padding:30px 28px;border:1px solid var(--stroke);border-radius:18px;background:var(--surface)}.settings-panel[hidden]{display:none}body.settings-open .workspace-view{display:flex}.settings-header{padding-right:0}.settings-header .icon-button{background:var(--surface-raised)} - -/* Larger, centered people are the focus; actions sit directly beneath them. */ -.voice.card{min-height:264px}.voice-body{min-height:264px;position:relative}.voice-participants{position:absolute;top:22px;left:50%;margin:0;transform:translateX(-50%);gap:40px}.voice-participant{position:relative;grid-template-columns:104px auto;gap:0 13px;font-size:14px}.voice-participant .avatar{width:104px;height:104px;font-size:30px}.voice-controls{position:absolute;right:auto;bottom:22px;left:50%;width:auto;margin:0;transform:translateX(-50%);white-space:nowrap}.indicator{width:18px;height:18px;border-width:2px}.profile-adjust{font-size:10px}.profile-name-field{display:grid;gap:6px;margin:14px 0 12px;color:var(--muted);font-size:11px;font-weight:700}.profile-name-field input{height:38px} -.friend-participant.is-absent{display:none}.friend-participant.is-entering{animation:friend-arrive .46s cubic-bezier(.16,1,.3,1) both}.friend-participant.is-entering .avatar{animation:friend-avatar-arrive .46s cubic-bezier(.16,1,.3,1) both}.friend-participant.is-leaving{pointer-events:none;animation:friend-depart .42s cubic-bezier(.55,0,1,.45) both}.friend-participant.is-leaving .avatar{animation:friend-combust .42s cubic-bezier(.55,0,1,.45) both}.friend-participant.is-leaving .avatar::before,.friend-participant.is-leaving .avatar::after{position:absolute;z-index:3;top:50%;left:50%;width:5px;height:5px;border-radius:50%;background:#c8cfff;content:"";opacity:0;pointer-events:none}.friend-participant.is-leaving .avatar::before{box-shadow:-31px -18px #9da8ff,26px -24px #f1c9ff,36px 4px #8cf0c2,-25px 26px #afb7ff,9px 34px #d7dcff;animation:friend-sparks-a .42s ease-out both}.friend-participant.is-leaving .avatar::after{box-shadow:-38px 5px #f5b7db,-15px -36px #c8cfff,19px -32px #91e8c5,38px 21px #b4c0ff,-5px 39px #f3d3ff;animation:friend-sparks-b .42s ease-out both} -.self-participant.is-absent{display:none}.self-participant.is-entering{animation:friend-arrive .46s cubic-bezier(.16,1,.3,1) both}.self-participant.is-entering .avatar{animation:friend-avatar-arrive .46s cubic-bezier(.16,1,.3,1) both}.self-participant.is-leaving{pointer-events:none;animation:friend-depart .42s cubic-bezier(.55,0,1,.45) both}.self-participant.is-leaving .avatar{animation:friend-combust .42s cubic-bezier(.55,0,1,.45) both}.self-participant.is-leaving .avatar::before,.self-participant.is-leaving .avatar::after{position:absolute;z-index:3;top:50%;left:50%;width:5px;height:5px;border-radius:50%;background:#c8cfff;content:"";opacity:0;pointer-events:none}.self-participant.is-leaving .avatar::before{box-shadow:-31px -18px #9da8ff,26px -24px #f1c9ff,36px 4px #8cf0c2,-25px 26px #afb7ff,9px 34px #d7dcff;animation:friend-sparks-a .42s ease-out both}.self-participant.is-leaving .avatar::after{box-shadow:-38px 5px #f5b7db,-15px -36px #c8cfff,19px -32px #91e8c5,38px 21px #b4c0ff,-5px 39px #f3d3ff;animation:friend-sparks-b .42s ease-out both} -@keyframes friend-arrive{0%{opacity:0;transform:translateY(12px) scale(.7);filter:blur(5px)}62%{opacity:1;transform:translateY(-2px) scale(1.05);filter:blur(0)}100%{opacity:1;transform:none}}@keyframes friend-avatar-arrive{0%{box-shadow:0 0 0 0 rgba(133,145,255,0)}55%{box-shadow:0 0 0 11px rgba(133,145,255,.18)}100%{box-shadow:0 0 0 18px rgba(133,145,255,0)}}@keyframes friend-depart{0%{opacity:1;transform:none;filter:blur(0)}54%{opacity:1;transform:scale(1.07);filter:brightness(1.35)}100%{opacity:0;transform:translateY(-9px) scale(.42);filter:blur(5px)}}@keyframes friend-combust{0%,35%{filter:brightness(1)}60%{filter:brightness(1.85) saturate(1.35)}100%{filter:brightness(1.4);opacity:0;transform:scale(.35)}}@keyframes friend-sparks-a{0%,35%{opacity:0;transform:translate(-50%,-50%) scale(.15)}52%{opacity:.95}100%{opacity:0;transform:translate(-50%,-50%) scale(1.5)}}@keyframes friend-sparks-b{0%,40%{opacity:0;transform:translate(-50%,-50%) scale(.1)}56%{opacity:.8}100%{opacity:0;transform:translate(-50%,-50%) scale(1.75)}} -@media(prefers-reduced-motion:reduce){.friend-participant.is-entering,.friend-participant.is-entering .avatar,.friend-participant.is-leaving,.friend-participant.is-leaving .avatar,.friend-participant.is-leaving .avatar::before,.friend-participant.is-leaving .avatar::after{animation:none!important}} -@media(prefers-reduced-motion:reduce){.self-participant.is-entering,.self-participant.is-entering .avatar,.self-participant.is-leaving,.self-participant.is-leaving .avatar,.self-participant.is-leaving .avatar::before,.self-participant.is-leaving .avatar::after{animation:none!important}} -@media(max-width:900px){.settings-panel{left:76px;width:min(760px,calc(100vw - 96px))}.voice.card,.voice-body{min-height:242px}.voice-participants{top:18px}.voice-controls{bottom:18px}.voice-participant{grid-template-columns:92px auto}.voice-participant .avatar{width:92px;height:92px;font-size:26px}} -@media(max-width:570px){.settings-panel{top:10px;bottom:10px;left:66px;width:calc(100vw - 76px);padding:22px 18px}.voice.card,.voice-body{min-height:214px}.voice-participants{top:14px;gap:12px}.voice-participant{grid-template-columns:70px auto;gap:0 8px;font-size:11px}.voice-participant .avatar{width:70px;height:70px;font-size:20px}.voice-controls{bottom:14px}} - -/* A quiet, Discord-style message timeline: people first, message second. */ -.messages{padding:14px 0}.message,.message.mine{display:grid;grid-template-columns:42px minmax(0,1fr);max-width:none;margin:0;padding:9px 12px 10px;text-align:left;border-radius:6px;transition:background .12s ease}.message:hover{background:rgba(255,255,255,.045)}.message-avatar{width:38px;height:38px;margin:1px 12px 0 0;overflow:hidden;border:0;font-size:13px}.message-content{min-width:0}.message-header{display:flex;align-items:baseline;gap:8px;min-height:19px}.message-header strong{overflow:hidden;color:#e8ebf0;font-size:14px;font-weight:750;text-overflow:ellipsis;white-space:nowrap}.message-header time{color:var(--subtle);font-size:10px}.bubble,.mine .bubble{display:block;max-width:none;padding:0;border:0;border-radius:0;background:transparent!important;color:var(--text);font-size:14px;line-height:1.42;text-align:left;white-space:pre-wrap;overflow-wrap:anywhere}.bubble.emoji-only{margin-top:2px;font-size:2.45em}.message .embed-img{margin-top:7px;border-radius:6px}.message .embed-yt{margin-top:7px}.message .meta{margin-top:4px}.message:has(.accept-card),.message:has(.transfer){display:block;max-width:560px}.message:has(.accept-card) .bubble,.message:has(.transfer) .bubble{padding:10px 12px;background:var(--surface-raised)!important;border-radius:10px}@media(max-width:570px){.messages{padding:10px 0}.message,.message.mine{grid-template-columns:36px minmax(0,1fr);padding:8px}.message-avatar{width:32px;height:32px;margin-right:9px}.message-header strong,.bubble,.mine .bubble{font-size:13px}} - -/* Emoji picker: clear category icons and an actual selectable emoji grid. */ -.emoji-picker{width:368px;overflow:hidden;border-color:var(--stroke);border-radius:14px;background:var(--surface);box-shadow:0 18px 44px rgba(0,0,0,.34)}.emoji-tabs{gap:4px;padding:8px;overflow-x:auto;border-bottom:1px solid var(--stroke-quiet);scrollbar-width:none}.emoji-tabs::-webkit-scrollbar{display:none}.emoji-tab{display:grid;min-width:34px;min-height:34px;padding:0;place-items:center;border:0;border-radius:9px;background:transparent;color:var(--muted);font-size:18px}.emoji-tab:hover{background:var(--surface-hover)}.emoji-tab.active{background:var(--surface-raised);box-shadow:inset 0 -2px 0 var(--accent)}.emoji-body{display:block;max-height:312px;padding:0;overflow-y:auto}.emoji-page{padding:12px}.emoji-page.hidden{display:none}.emoji-category-title{margin:0 0 9px;color:var(--muted);font-size:11px;font-weight:800;letter-spacing:.55px;text-transform:uppercase}.emoji-grid{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:4px}.emoji-item{display:grid;min-height:38px;padding:0;place-items:center;border:0;border-radius:8px;background:transparent;font-size:21px;line-height:1}.emoji-item:hover,.emoji-item:focus-visible{background:var(--surface-hover);transform:scale(1.12)}@media(max-width:570px){.emoji-picker{width:min(368px,calc(100vw - 76px))}.emoji-grid{grid-template-columns:repeat(7,minmax(0,1fr))}.emoji-page{padding:10px}.emoji-item{min-height:36px}} - -/* Tabbed settings keeps a complete control center compact and scannable. */ -.settings-panel{grid-template-columns:172px minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);gap:18px;padding:24px}.settings-header{grid-column:1/-1;align-items:center;padding:0 0 15px;border-bottom:1px solid var(--stroke-quiet)}.settings-header h2{font-size:23px}.settings-tabs{display:flex;align-self:start;flex-direction:column;gap:3px}.settings-tab{min-height:38px;padding:0 11px;border:0;border-radius:8px;background:transparent;color:var(--muted);font-size:12px;font-weight:700;text-align:left}.settings-tab:hover{background:var(--surface-hover);color:var(--text)}.settings-tab.active{background:var(--surface-raised);color:#fff}.settings-pages{min-width:0;overflow:auto}.settings-page{display:block;min-height:100%;padding:2px 2px 12px;border:0;background:transparent}.settings-page[hidden]{display:none}.settings-page h3{margin:0 0 5px;font-size:17px}.settings-page>div>p{margin:0;color:var(--muted);font-size:12px;line-height:1.45}.settings-field{display:grid;gap:6px;margin:16px 0 0;color:var(--muted);font-size:11px;font-weight:700}.settings-field select{width:100%;height:39px}.settings-inline-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:15px}.settings-inline-actions button{min-height:34px}.settings-hint{margin:12px 0 0;color:var(--subtle);font-size:11px;line-height:1.45}.settings-toggle{position:relative;display:grid;grid-template-columns:18px minmax(0,1fr);column-gap:10px;margin:18px 0 0;padding:0;color:var(--text);font-size:12px;font-weight:750;letter-spacing:0;text-transform:none}.settings-toggle input{width:16px;height:16px;margin:1px 0 0;accent-color:var(--accent)}.settings-toggle small{grid-column:2;margin-top:3px;color:var(--muted);font-size:11px;font-weight:400;line-height:1.4}.settings-page .theme-grid{margin-top:16px}.settings-page .profile-settings-row{margin-top:10px}.settings-page .profile-editor{margin-top:16px}.profile-name-field{margin-top:16px}html[data-reduce-motion="true"] *,html[data-reduce-motion="true"] *::before,html[data-reduce-motion="true"] *::after{scroll-behavior:auto!important;animation-duration:.01ms!important;transition-duration:.01ms!important}@media(max-width:700px){.settings-panel{grid-template-columns:1fr;grid-template-rows:auto auto minmax(0,1fr);padding:20px}.settings-tabs{flex-direction:row;overflow-x:auto;padding-bottom:3px}.settings-tab{flex:0 0 auto}.settings-pages{overflow:visible}.settings-page{min-height:0}} - -.gif-attachment-message{display:block;width:min(100%,380px);margin-top:7px}.gif-attachment-message img{display:block;width:100%;max-height:300px;object-fit:contain;border-radius:8px;background:var(--surface-raised)} -.gif-attachment-message{position:relative}.gif-message-favorite{position:absolute;top:7px;right:7px;display:grid;width:24px;height:24px;padding:0;place-items:center;border:1px solid rgba(255,255,255,.4);border-radius:50%;background:rgba(12,14,18,.86);color:#d2d9e4;font-size:17px;line-height:1;cursor:pointer}.gif-message-favorite.on{border-color:#ffd15a;background:#f6bd35;color:#352300}.gif-message-favorite:hover{transform:scale(1.08)} - -.share-start-options{display:flex;flex-wrap:wrap;gap:10px;margin:-4px 0 16px}.share-start-options label{display:grid;gap:5px;color:var(--muted);font-size:11px;font-weight:700}.share-start-options select{min-width:104px;height:34px}.share-start-options .share-audio-option{display:flex;align-items:center;flex-wrap:wrap;grid-template-columns:auto 1fr;gap:7px;padding-top:20px;color:var(--text)}.share-start-options .share-audio-hint{flex:1 1 100%;margin:0 0 0 22px;color:var(--muted);font-size:10px;font-weight:600}.share-start-options input{accent-color:var(--accent)} -#voicePanel.screen-sharing .voice-participants{opacity:.25;pointer-events:none}#voicePanel.screen-sharing .screen-section{position:absolute;z-index:6;inset:12px 14px 66px;display:block}#voicePanel.screen-sharing .screen-videos{display:grid;width:100%;height:100%;padding:8px;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:8px;border-radius:14px;background:rgba(8,10,14,.94);box-shadow:0 14px 36px rgba(0,0,0,.38)}.screen-tile{position:relative;display:block;min-width:0;min-height:0;overflow:hidden;border:1px solid var(--stroke);border-radius:10px;background:#050609;cursor:pointer}.screen-tile video{position:relative!important;right:auto!important;bottom:auto!important;display:block!important;width:100%!important;height:100%!important;max-height:none!important;min-height:0;object-fit:contain;border:0!important;border-radius:0!important;background:#050609}.screen-tile:hover{border-color:var(--accent)}.screen-tile-name{position:absolute;z-index:2;left:10px;bottom:10px;padding:5px 8px;border-radius:7px;background:rgba(0,0,0,.7);color:#fff;font-size:12px;font-weight:750}.screen-tile-avatar{position:absolute;z-index:2;right:10px;bottom:10px;display:grid;width:28px;height:28px;place-items:center;border:2px solid #151722;border-radius:50%;background:var(--accent);color:#fff;font-size:11px;font-weight:800}.screen-view-bar{position:absolute;z-index:7;right:25px;bottom:76px;display:flex;gap:6px}.screen-view-bar button{min-height:32px;padding:0 9px;border-color:var(--stroke);background:rgba(22,24,34,.94);color:var(--muted);font-size:11px}.screen-view-bar button.active,.screen-view-bar button:hover{background:var(--accent);border-color:var(--accent);color:#fff}#voicePanel.screen-focus .screen-videos{grid-template-columns:minmax(0,1fr) 180px;grid-template-rows:minmax(0,1fr)}#voicePanel.screen-focus .screen-tile.remote{grid-column:1;grid-row:1}#voicePanel.screen-focus .screen-tile.local{grid-column:2;grid-row:1}#voicePanel.screen-focus.screen-focus-local .screen-tile.local{grid-column:1;grid-row:1}#voicePanel.screen-focus.screen-focus-local .screen-tile.remote{grid-column:2;grid-row:1}.screen-tile[hidden]{display:none!important}.screen-tile .screen-volume{z-index:4}.remote-screen.fs,.screen-preview.fs{position:fixed!important;z-index:10000!important;inset:0!important;width:100vw!important;height:100vh!important;max-width:none!important;max-height:none!important;border:0!important;border-radius:0!important;background:#000!important;object-fit:contain!important}.screen-fullscreen .screen-view-bar{position:fixed;z-index:10001;right:18px;bottom:18px}@media(max-width:700px){#voicePanel.screen-focus .screen-videos{grid-template-columns:minmax(0,1fr) 110px}.screen-view-bar{right:20px;bottom:73px}.screen-view-bar button{padding:0 7px}.screen-tile-name{font-size:10px}} -.settings-field input[type=range]{--range-fill:15%;width:100%;height:20px;padding:0;border:0;background:linear-gradient(to right,var(--accent) 0 var(--range-fill),var(--stroke-quiet) var(--range-fill) 100%);border-radius:99px;appearance:none;-webkit-appearance:none}.settings-field input[type=range]::-webkit-slider-runnable-track{height:8px;border:0;border-radius:99px;background:transparent}.settings-field input[type=range]::-webkit-slider-thumb{width:20px;height:20px;margin-top:-6px;border:0;border-radius:50%;background:var(--accent);appearance:none;-webkit-appearance:none;box-shadow:0 1px 3px rgba(0,0,0,.45)}.settings-field input[type=range]::-moz-range-track{height:8px;border:0;border-radius:99px;background:var(--stroke-quiet)}.settings-field input[type=range]::-moz-range-progress{height:8px;border-radius:99px;background:var(--accent)}.settings-field input[type=range]::-moz-range-thumb{width:20px;height:20px;border:0;border-radius:50%;background:var(--accent)} -#volumeSlider,.screen-volume input,.settings-field input[type=range]{cursor:ew-resize;touch-action:none} -#voicePanel.screen-sharing .voice-participants{visibility:hidden} -/* Motion pass: short, tactile transitions that never delay chat or calls. */ -@keyframes pair-rise{from{opacity:0;transform:translateY(10px) scale(.985)}to{opacity:1;transform:none}}@keyframes pair-pop{0%{transform:scale(.92);opacity:0}70%{transform:scale(1.025)}100%{transform:scale(1);opacity:1}}@keyframes pair-glow{0%,100%{box-shadow:0 0 0 0 rgba(142,152,255,0)}50%{box-shadow:0 0 0 9px rgba(142,152,255,.14)}} -.message{animation:pair-rise .22s cubic-bezier(.2,.8,.2,1) both}.settings-panel:not([hidden]),.connect-drawer[open]{animation:pair-rise .26s cubic-bezier(.2,.8,.2,1) both}.call-status.live{animation:pair-glow 1.8s ease-in-out infinite}.screen-source-dialog,.gif-picker,.emoji-picker{animation:pair-pop .18s cubic-bezier(.2,.9,.25,1) both}.screen-video,.screen-preview,video{transition:opacity .2s ease,transform .24s cubic-bezier(.2,.8,.2,1)}button:active{transform:scale(.97)}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}} -@keyframes pair-slide{from{opacity:0;transform:translateX(-12px)}to{opacity:1;transform:none}}@keyframes pair-sheen{from{background-position:160% 0}to{background-position:-60% 0}}@keyframes pair-pulse-dot{0%,100%{transform:scale(1);opacity:.8}50%{transform:scale(1.45);opacity:1}} -.app-rail{transition:background .3s ease,border-color .3s ease}.rail-button,.composer-btn,.settings-tab,.theme-option,.screen-source-option,.gif-result,.emoji-item{transition:transform .18s cubic-bezier(.2,.9,.25,1),background .2s ease,box-shadow .2s ease,color .2s ease}.rail-button:hover,.composer-btn:hover,.theme-option:hover,.screen-source-option:hover,.gif-result:hover{transform:translateY(-2px);box-shadow:0 8px 22px rgba(0,0,0,.22)}.settings-tab.active{animation:pair-slide .22s cubic-bezier(.2,.8,.2,1)}.settings-page.active{animation:pair-rise .24s cubic-bezier(.2,.8,.2,1)}.connection.connected i{animation:pair-pulse-dot 1.7s ease-in-out infinite}.voice-participant,.screen-tile{transition:transform .26s cubic-bezier(.2,.8,.2,1),opacity .22s ease,filter .22s ease}.screen-tile:hover{transform:translateY(-3px) scale(1.01)}.transfer{animation:pair-rise .28s cubic-bezier(.2,.8,.2,1)}.transfer i{background-image:linear-gradient(105deg,#7684ff 25%,#b28cff 48%,#7684ff 70%);background-size:220% 100%;animation:pair-sheen 1.8s linear infinite}.profile-avatar,.settings-avatar{transition:transform .24s cubic-bezier(.2,.8,.2,1),box-shadow .24s ease}.profile-avatar:hover,.settings-avatar:hover{transform:scale(1.045);box-shadow:0 9px 25px rgba(0,0,0,.24)}.voice-log .log-entry{animation:pair-slide .2s ease both}.gif-star{transition:transform .16s cubic-bezier(.2,.9,.25,1),color .16s ease}.gif-star:hover{transform:scale(1.2) rotate(12deg)} - -/* Navigation and the friend panel must never run below a small window. */ -.app-rail{box-sizing:border-box;overflow-y:auto;overscroll-behavior:contain}.rail-top,.rail-bottom{flex:0 0 auto}.rail-add{display:grid!important;visibility:visible!important}@supports(height:100dvh){.connect-drawer[open]{top:50dvh;max-height:calc(100dvh - 28px)}}@media(max-height:560px){.app-rail{height:100dvh;min-height:0;padding-block:8px}.rail-top,.rail-bottom{gap:6px}.rail-bottom{margin-top:auto}} - -/* The drawer is centered with a transform, so its entrance animation must retain it. */ -@keyframes pair-drawer-rise{from{opacity:0;transform:translate(-50%,calc(-50% + 10px)) scale(.985)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}.connect-drawer[open]{animation:pair-drawer-rise .26s cubic-bezier(.2,.8,.2,1) both} - +/* Upstream screen-share layout + audio badge (kept after black DM shell). */ /* Keep share volume separate from layout controls in grid, focus, and fullscreen. */ #voicePanel.screen-sharing .screen-volume{top:12px;right:12px;bottom:auto;z-index:20}#voicePanel.screen-sharing .screen-view-bar{right:24px;bottom:16px;z-index:21}.screen-tile.fs{position:fixed!important;z-index:10000!important;inset:0!important;width:100vw!important;height:100vh!important;max-width:none!important;max-height:none!important;border:0!important;border-radius:0!important;background:#000!important}.screen-tile.fs .screen-volume{top:18px;right:18px;bottom:auto;z-index:10002} .screen-section.fs,.screen-section:fullscreen{position:fixed!important;z-index:10000!important;inset:0!important;display:block!important;width:100vw!important;height:100vh!important;max-width:none!important;max-height:none!important;background:#000!important}.screen-section.fs .screen-videos,.screen-section:fullscreen .screen-videos{display:grid!important;width:100%!important;height:100%!important;padding:12px;border:0;border-radius:0;background:#000}.screen-section.fs .screen-view-bar,.screen-section:fullscreen .screen-view-bar{position:fixed;z-index:10002;right:18px;bottom:18px}.screen-section.fs .screen-volume,.screen-section:fullscreen .screen-volume{top:18px;right:18px;bottom:auto;z-index:10003} .screen-audio-badge{position:absolute;z-index:23;left:18px;bottom:18px;max-width:calc(100% - 150px);padding:7px 10px;border:1px solid rgba(255,255,255,.16);border-radius:8px;background:rgba(10,12,17,.82);color:#dfe5ef;font-size:11px;font-weight:700;pointer-events:none}.screen-audio-badge[hidden]{display:none} /* Discord-style share flow: a compact preview opens into one focused stage. */ #voicePanel.screen-sharing:not(.screen-expanded) .screen-section{position:absolute;inset:auto 14px 14px auto;width:min(360px,calc(100% - 28px));height:210px;z-index:8}#voicePanel.screen-sharing:not(.screen-expanded) .screen-videos{display:grid;width:100%;height:100%;padding:6px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px;border-radius:12px;background:#090b10;box-shadow:0 12px 30px rgba(0,0,0,.35)}#voicePanel.screen-sharing:not(.screen-expanded) .screen-view-bar{display:none}#voicePanel.screen-expanded .screen-section{position:absolute;z-index:8;inset:12px 14px 66px;display:block}#voicePanel.screen-expanded .screen-videos{position:relative;display:block;width:100%;height:100%;padding:8px;border-radius:14px;background:#050609;box-shadow:0 14px 36px rgba(0,0,0,.4)}#voicePanel.screen-expanded .screen-tile{position:absolute;inset:8px;z-index:1}#voicePanel.screen-expanded .screen-tile.local{z-index:2;inset:auto 22px 22px auto;width:min(28vw,280px);height:min(22vh,180px);box-shadow:0 10px 28px rgba(0,0,0,.5)}#voicePanel.screen-expanded .screen-stage.screen-expanded-local .screen-tile.local{inset:8px;width:auto;height:auto;z-index:1}#voicePanel.screen-expanded .screen-stage.screen-expanded-local .screen-tile.remote{z-index:2;inset:auto 22px 22px auto;width:min(28vw,280px);height:min(22vh,180px);box-shadow:0 10px 28px rgba(0,0,0,.5)}#voicePanel.screen-expanded .screen-view-bar{position:absolute;z-index:22;right:26px;bottom:16px;display:flex}#voicePanel.screen-expanded .screen-view-bar button{display:grid;width:38px;min-height:38px;padding:0;place-items:center;font-size:19px}.screen-section.fs .screen-view-bar,.screen-section:fullscreen .screen-view-bar{display:flex!important} + +