{{define "head"}} {{end}} {{/* The call window (2026-09-28). Paul: "Can the web UI for a call get an update so it more closely matches the Mac experience, with the whole call UI in a rounded rect 'window' with a bit of padding within the viewport?" So the room replaces the page shell: one rounded window, inset from the viewport's edges (edge to edge on a phone), with the picture filling it and everything else over the picture, as the Mac's full-window call has it (MacFullWindowCall). No title pill (2026-10-02, Paul approved): how the call is going is said by the notices, and a pill saying it again was clutter. A title someone chose is shown the way a Mac window shows its title — plain, centred in the strip across the top, over a dark scrim, fading with the controls. A title Calls wrote ("Call, 2 Oct 15:40") is not shown at all; it stays the page's heading for a screen reader and its . What the pill's ☰ menu carried — who you are, Meetings, Settings, Help, Sign out — is in the bar's ⋯ More menu. With no script the header is a plain block instead (noscript.css): the title whichever kind it is, the standing line, the floor notice and the page's navigation, all server-rendered (AGENTS.md). */}} {{define "body"}} <main id="main" class="calls-window" aria-labelledby="call-title"> <header id="call-top" class="calls-top calls-fades{{if .Named}} calls-top-named{{end}}"> <h1 id="call-title">{{.Title}}</h1> <noscript> <p class="calls-top-line">You’re in this call as {{.Name}}{{if eq .Standing "guest"}}, a guest{{end}}.{{if .Starts}} Starts {{.Starts}}.{{end}}</p> {{if .Floor}}<p class="calls-top-line calls-floor">Calls outside your network may not connect yet.</p>{{end}} <nav class="calls-top-nav" aria-label="Calls"> {{with .Menu.Identity}}<p class="calls-top-line">Signed in as {{.}}</p>{{end}} <p class="calls-top-links">{{range .Menu.Nav}}<a href="{{.Href}}">{{.Label}}</a> {{end}}{{range .Menu.Help}}<a href="{{.Href}}">{{.Label}}</a> {{end}}{{with .Menu.SignOut}}<form method="post" action="{{.Action}}"><button type="submit">{{.Label}}</button></form>{{end}}</p> </nav> </noscript> </header> {{/* The Share form appears twice on purpose, and never at the same time. The bar's copy below is inside #room, which room.js un-hides, so it is the scripted path and matches the native call bar. This copy is the scriptless one: <noscript> is parsed as markup only when scripting is off, so exactly one of the two is ever real. Without it the sentence below is false, which is how it stood between c80a506 and a6c2ac8. "End meeting for everyone" is here for the same reason: with script it is in the bar's More menu, inside #room. */}} <noscript><div class="calls-scriptless"> <p>The call needs JavaScript to reach your camera and the other participants; everything else on this page works without it. <a href="/m/{{.Sqid}}/join">Back to the holding screen</a>.</p> {{if .IsMember}}<form method="post" action="/m/{{.Sqid}}/share" target="_blank" rel="noopener" data-busy="false"><button type="submit" rst-btn aria-label="Share meeting link">Share link</button></form> <p><a href="/meetings/{{.Sqid}}/end" target="_blank" rel="noopener">End meeting for everyone</a></p>{{end}} </div></noscript> <div id="room" class="calls-room" data-sqid="{{.Sqid}}" data-boot="{{.Boot}}" data-self-name="{{.Name}}" data-is-member="{{if .IsMember}}1{{end}}" data-relay="{{if .Relay}}1{{end}}" data-peer="" hidden> {{/* The notices: what stays on screen whatever the controls do, in a column under the title (CallNotices on the Mac). The sharing notice with its own Stop sharing (Paul, 2026-09-28: "I would expect this notification to have a 'Stop sharing' button inline"), and under it whose view of the screen is being recovered (spec §15.21); "Click anywhere to turn on sound", which room.js puts here; the call's own status line, with its way out when the call is over; the relay warning room.js puts after it; and each person waiting, with Admit. */}} <div id="notices" class="calls-notices"> <div id="share-notice" class="calls-share-notice" hidden> <p class="calls-share-notice-row">{{icon "screen-share"}}<span>You are sharing your screen</span><button type="button" id="share-stop" class="calls-share-stop" aria-label="Stop sharing your screen">Stop sharing</button></p> <p id="share-recovery" class="calls-share-recovery" role="status" hidden></p> </div> <p id="status" class="calls-status" role="status"></p> {{/* §5.5's relay notice, once a session: call-window.js shows it on the first visit of a tab's session and it stays until it is put away. */}} {{if .Floor}}<div id="floor-notice" class="calls-floor-notice" hidden><p>Calls outside your network may not connect yet.</p><button type="button" id="floor-dismiss" aria-label="Dismiss the relay notice" title="Dismiss">{{icon "x"}}</button></div>{{end}} {{/* iPhone and iPad (2026-10-03): Safari stops the camera and the microphone, and lets the call's sound lapse, when the tab goes to the background or the screen locks. call-window.js shows this once a session on those devices, with its own Dismiss. */}} <div id="keep-open-notice" class="calls-floor-notice" hidden><p>Keep this tab open and the screen on during the call.</p><button type="button" id="keep-open-dismiss" aria-label="Dismiss the keep-open notice" title="Dismiss">{{icon "x"}}</button></div> <div id="queue" class="calls-queue" hidden><h2>Waiting to join</h2><ul id="knocks"></ul></div> </div> {{/* Unread chat, for anyone who cannot see the badge on the button. The panel's own log is aria-live, but closing chat hides it, which takes the whole region out of the accessibility tree — so a message arriving while chat is closed announced nothing at all. The badge is aria-hidden and a changed aria-label does not announce either, so this was silent three ways. This region is outside the panel and survives the closing. It carries the count and never the message: someone who closed chat has said they do not want it read out, and the count is exactly what the badge beside the button shows. call-layout.js writes it only while closed — with the panel open its own log announces arrivals, and saying it twice is worse than once. */}} <p id="chat-unread-live" class="calls-sr-only" role="status" aria-live="polite"></p> {{/* Speaker view and the viewer's order (0.4.0): what call-layout.js says when the person pins, unpins or moves someone. An automatic change of speaker is never said: in a lively call it would talk over the talking, and the stage tile says "On stage" itself. */}} <p id="layout-live" class="calls-sr-only" role="status" aria-live="polite"></p> {{/* Spec §15.19. Someone muting or turning their camera off, said politely and once. A region of its own for the reason #device-note has one: renderStatus rewrites #status on every render, and would take the sentence away before it was read. Present from the first paint, empty, so a reader is already watching it when room.js first writes to it. */}} <p id="media-live" class="calls-sr-only" role="status" aria-live="polite"></p> <div id="stage" class="calls-stage"> <div id="tiles" class="calls-grid"></div> </div> {{/* The bar, in the Mac bar's order: microphone, camera, share, devices, layout, share link, then chat, More and Leave at the right. Glyphs, each with its name for a screen reader and a tooltip; the word under each is shown in a phone's controls sheet, where the bar is only More controls and Leave, as the iPhone app has it. */}} <div id="controls" class="calls-controls calls-fades" role="group" aria-label="Call controls"> <button type="button" id="more-controls" class="calls-bar-phone" aria-expanded="false" aria-controls="call-sheet" aria-label="More controls">{{icon "ellipsis"}}</button> <div id="call-sheet" class="calls-sheet"> <button type="button" id="mute" rst-btn aria-label="Microphone" aria-pressed="false" data-tip="Mute microphone" data-tip-on="Unmute microphone"><span class="calls-g calls-g-rest">{{icon "mic"}}</span><span class="calls-g calls-g-pressed">{{icon "mic-off"}}</span><span class="calls-bar-label">Microphone<span id="mute-state" class="calls-toggle-state" aria-hidden="true">On</span></span></button> <button type="button" id="camera" rst-btn aria-label="Camera" aria-pressed="false" data-tip="Turn camera off" data-tip-on="Turn camera on"><span class="calls-g calls-g-rest">{{icon "video"}}</span><span class="calls-g calls-g-pressed">{{icon "video-off"}}</span><span class="calls-bar-label">Camera<span id="camera-state" class="calls-toggle-state" aria-hidden="true">On</span></span></button> <button type="button" id="share" rst-btn aria-pressed="false" aria-label="Share screen" data-tip="Share your screen" data-tip-on="Stop sharing your screen"><span class="calls-g calls-g-rest">{{icon "screen-share"}}</span><span class="calls-g calls-g-pressed">{{icon "screen-share-off"}}</span><span class="calls-bar-label">Share screen<span id="share-state" class="calls-toggle-state" aria-hidden="true">Off</span></span></button> <button type="button" id="devices-toggle" rst-btn aria-expanded="false" aria-controls="room-devices" hidden aria-label="Devices" data-tip="Camera, microphone and speaker">{{icon "sliders-horizontal"}}<span class="calls-bar-label">Devices</span></button> <button type="button" id="layout-toggle" rst-btn aria-expanded="false" aria-controls="layout-panel" aria-label="Video layout" data-tip="Video layout: Speaker or Grid" data-mode="speaker"><span class="calls-g calls-g-speaker">{{icon "panel-bottom"}}</span><span class="calls-g calls-g-grid">{{icon "layout-grid"}}</span><span class="calls-bar-label">Layout</span></button> {{if .IsMember}}<form method="post" action="/m/{{.Sqid}}/share" target="_blank" rel="noopener" data-busy="false"><button type="submit" rst-btn aria-label="Share meeting link" data-tip="Share meeting link">{{icon "share"}}<span class="calls-bar-label">Share link</span></button></form>{{end}} <span class="calls-bar-spacer" aria-hidden="true"></span> <button type="button" id="toggle-chat" rst-btn aria-expanded="false" aria-controls="chat-overlay" hidden data-tip="Chat">{{icon "messages-square"}}<span class="calls-bar-label">Chat</span><span id="chat-unread" class="calls-badge" aria-hidden="true"></span></button> <button type="button" id="call-more-toggle" rst-btn aria-expanded="false" aria-controls="call-more" aria-label="More" data-tip="More">{{icon "ellipsis"}}<span class="calls-bar-label">More</span></button> </div> <button type="button" id="leave" rst-btn="danger" data-tip="Leave meeting">{{icon "phone-down"}}<span class="calls-bar-label">Leave</span></button> </div> {{/* The bar's popovers, each over the picture above the button that opened it: the layout, the devices, and More. call-window.js places them and closes them (Escape, a click elsewhere). */}} <div id="layout-panel" class="calls-popover" role="group" aria-label="Video layout" hidden> <p class="calls-popover-title">Video layout</p> <div id="video-layout" estilo-call-segmented role="group" aria-label="Video layout" hidden> <button type="button" id="layout-speaker" aria-pressed="true">Speaker</button> <button type="button" id="layout-grid" aria-pressed="false">Grid</button> </div> <p class="calls-popover-help">Speaker puts whoever is talking on the stage. Pin anyone from their tile.</p> </div> {{/* The device panel, closed until asked for. A call's own screen is the video and the controls; three selects permanently over them would be furniture the whole call pays for so that someone can change a microphone once. room.js reveals the button — the panel is useless in a browser that cannot enumerate devices — and fills the selects from the same enumeration the holding screen uses. The ids are devices.js's own on purpose: the two fill routines are mirrors, and a reader comparing them should not also have to translate names. */}} <div id="room-devices" class="calls-pickers calls-popover calls-devices" role="group" aria-label="Devices" hidden> <p class="calls-popover-title">Devices</p> <div id="camera-row">{{template "field-select" dict "ID" "pick-camera" "Name" "camera" "Label" "Camera"}}</div> <div id="mic-row">{{template "field-select" dict "ID" "pick-mic" "Name" "mic" "Label" "Microphone"}}</div> <div id="speaker-row" hidden>{{template "field-select" dict "ID" "pick-speaker" "Name" "speaker" "Label" "Speaker"}}</div> <div id="raw-row" class="calls-raw-row" hidden>{{template "field-check" dict "Name" "raw-audio" "Label" "Studio audio"}} <p id="raw-help" class="calls-raw-help">Sends your microphone unprocessed and at a higher bitrate, for an audio interface in a room you have treated. It turns off echo cancellation, noise suppression and automatic gain, so leave it off on a laptop.</p></div> {{/* §5.4: what a shared screen gives up first when the connection cannot carry all of it. room.js fills the options, remembers the choice and reveals the row in a browser that can share. */}} <div id="share-mode-row" hidden>{{template "field-select" dict "ID" "pick-share-mode" "Name" "share-mode" "Label" "Optimise shared screen for" "Help" "Balanced keeps text readable and keeps up with scrolling. Text and detail keeps small print sharp; movement may look jerky. Motion and video keeps movement smooth; text may look softer."}}</div> {{/* Device messages get a live region of their own, like the relay warning: renderStatus owns #status and rewrites it on every render, so a sentence about a camera that would not open has to live somewhere that is not about to be overwritten. */}} <p id="device-note" class="calls-device-note" role="status" hidden></p> {{/* What the Mac panel's "Copy call diagnostics" copies, for the browser: the connections, and the screen's recovery timeline and decode statistics on both sides of a share (spec §15.22). Names, states and counts only; room.js says in #device-note whether it was copied. */}} <button type="button" id="copy-diagnostics" rst-btn="ghost" class="calls-copy-diagnostics">Copy call diagnostics</button> {{/* Spec §15.26: the same diagnostics, with the people named by short peer id, sent to the server log with a note (report.js's sheet); and the switch for the automatic anonymous quality reports, on unless turned off in this browser. */}} <button type="button" id="report-problem" rst-btn="ghost" class="calls-copy-diagnostics calls-report-problem">Report a problem</button> <div id="quality-row" class="calls-quality-row">{{template "field-check" dict "Name" "quality-reports" "Label" "Send anonymous quality reports" "Checked" true}} <p id="quality-help" class="calls-raw-help">Connection statistics every thirty seconds, to help fix call problems: no names, no audio or video.</p></div> </div> {{/* More: who you are in this call, then the account menu's items in its order and its words (chrome.go, roomMenu) — Meetings, Settings, Help → Open source, the one Sign out — which the title's ☰ menu carried until 2026-10-02. Ending the meeting for everyone is last, for a member: destructive, so a link to its own confirm page (AGENTS.md), opened in a new tab so that following it does not leave the call. With no script the header above has the same navigation. */}} <div id="call-more" class="calls-popover calls-menu" role="group" aria-label="More" hidden> <p class="calls-menu-standing">You’re in this call as {{.Name}}{{if eq .Standing "guest"}}, a guest{{end}}.{{if .Starts}} Starts {{.Starts}}.{{end}}</p> {{with .Menu.Identity}}<hr><p class="calls-menu-who"><span class="calls-menu-heading">Signed in as</span><span class="calls-menu-address">{{.}}</span></p>{{end}} {{if .Menu.Nav}}<hr>{{range .Menu.Nav}}<a href="{{.Href}}">{{.Label}}</a>{{end}}{{end}} <hr><p class="calls-menu-heading">Help</p> {{range .Menu.Help}}<a href="{{.Href}}">{{.Label}}</a>{{end}} {{/* §15.26: report.js's sheet, opened by room.js. */}} <button type="button" id="report-problem-menu">Report a problem</button> {{with .Menu.SignOut}}<hr><form method="post" action="{{.Action}}"><button type="submit">{{.Label}}</button></form>{{end}} {{if .IsMember}}<hr><a href="/meetings/{{.Sqid}}/end" target="_blank" rel="noopener" class="calls-menu-danger">End meeting for everyone</a>{{end}} </div> {{/* Chat, as a sidebar sliding over the picture's right-hand side (the Mac's chat sidebar): closed until asked for, and closing it keeps the draft. The panel itself (spec §6) is one partial and one script, and one comment in partials/chat.html; chat.js loads before room.js in the head block, and two deferred scripts run in document order, so window.Chat is there when room.js mounts it. */}} {{/* One tile's menu (2026-09-29), opened from the ⋯ on a tile: Pin, the three moves — the way to do without a drag what a drag does, named for the person ("Move Ana earlier") — and, for a member, Remove, a link to its own confirm page in a new tab. A screen keeps only Pin. call-layout.js fills and places it. */}} <div id="tile-menu" class="calls-menu calls-tile-menu" role="group" hidden> <button type="button" data-act="pin">Pin</button> <button type="button" data-move="first">Move to first</button> <button type="button" data-move="earlier">Move earlier</button> <button type="button" data-move="later">Move later</button> <a class="calls-tile-remove calls-menu-danger" target="_blank" rel="noopener" hidden>Remove from the call</a> </div> <div id="chat-overlay" class="calls-chat-overlay" hidden> <button type="button" id="chat-close" class="calls-chat-close" aria-label="Hide chat" title="Hide chat">{{icon "x"}}</button> {{template "chat-panel" .}} </div> {{/* The two tile marks room.js clones onto each tile, rendered here so they are the vendored badge and this app's own icons rather than a copy of either in script. The badge's word is the state, for a reader and for anyone who cannot tell a slashed microphone from a plain one; the icon is on top of it, never instead. */}} <template id="tile-muted">{{icon "mic-off"}}{{template "badge" dict "Label" "Muted"}}</template> <template id="tile-camera-off">{{icon "video-off"}}<span>Camera off</span></template> {{/* Each tile's arrangement buttons, which call-layout.js clones and names: a Pinned badge while the tile is pinned, and ⋯, which opens #tile-menu for that tile. */}} <template id="tile-tools"><button type="button" class="calls-tool calls-tool-pin" data-act="pin" aria-pressed="true">{{icon "pin"}}<span class="calls-tool-text">Pinned</span></button><button type="button" class="calls-tool calls-tool-more" aria-expanded="false" aria-controls="tile-menu">{{icon "ellipsis"}}</button></template> </div> {{template "report-sheet" .}} </main> {{end}}