/* calls.css: the room and the holding screen's scripted parts. Small on purpose (spec §9's three hand-written scripts get their own styling as they grow); the holding screen reads from rst-* tokens so it follows the theme rather than fixing its own colours. The room is the exception, and says why below: it is a window of video, and what is over video is one dark system whatever the theme (0.3.5's "Over the picture" on the Mac). */ /* The hidden attribute is how the scripts show and hide whole regions, and the UA's display: none for it loses to any author rule that sets display on the same element. Rather than a blanket [hidden] override with !important, which is the framework gallery's own precedent, each element that is both hidden and given a display of its own restores it with a line of its own, and says so. */ .calls-pickers[hidden] { display: none; } .calls-preview { width: 100%; max-width: 24rem; aspect-ratio: 4 / 3; background: var(--rst-surface-2); border: 1px solid var(--rst-line); border-radius: var(--rst-radius); object-fit: cover; } .calls-pickers { display: flex; flex-wrap: wrap; gap: var(--rst-sp-3); margin-block: var(--rst-sp-3); } /* Off screen, still in the accessibility tree and still announced. */ .calls-sr-only { position: absolute; inline-size: 1px; block-size: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } .calls-status { color: var(--rst-text-muted); min-height: 1.2em; } /* Studio audio takes a line of its own below the device selects: it carries a sentence of explanation, and in the pickers' flex row that sentence would otherwise read as a caption to whichever select it happened to land beside. flex-basis and not display, so the hidden attribute still hides the row — see the note at the top of this file. */ .calls-raw-row { flex-basis: 100%; } .calls-raw-help { margin-block: var(--rst-sp-1) 0; color: var(--rst-text-muted); font-size: var(--rst-fs-sm); max-inline-size: 42rem; } /* The holding screen's toggles keep one accessible name (aria-label) and move only aria-pressed, so the state is carried three ways and never by colour: the word in .calls-toggle-state, the attribute a screen reader reads, and a ring around the control from estilo's callui.css ([estilo-call-actions] [aria-pressed='true']). */ .calls-controls { display: flex; flex-wrap: wrap; gap: var(--rst-sp-2); margin: 0; } .calls-controls button[aria-pressed='true'] { font-weight: 700; } .calls-toggle-state { font-size: 0.8em; } .calls-toggle-state::before { content: ' '; } .calls-status a { margin-inline-start: var(--rst-sp-1); } /* The device panel's own live region (a camera that would not open, a microphone another app is holding). Never colour alone: the sentence is the whole signal and the weight is a second cue on top of it. No display of its own, so the hidden attribute still hides it — see the note at the top of this file. */ .calls-device-note { color: var(--rst-text); font-weight: 600; flex-basis: 100%; margin-block: var(--rst-sp-2) 0; } /* The relay warning (spec §5.3 step 1) sits under the status line in a live region of its own. Never colour alone: the sentence is the whole signal and the weight is a second cue on top of it. No display of its own, so the hidden attribute still hides it — see the note at the top of this file. */ .calls-ice-warning { color: var(--rst-text); font-weight: 600; margin-block: var(--rst-sp-2); } .calls-device-layout { display: grid; gap: var(--rst-sp-4); align-items: start; } /* devices.js hides #preview when the browser cannot reach a camera at all, and the wrap around it is a black panel with a border radius and nothing inside. Left alone it holds 60% of the lobby open beside the pickers. Hide the wrap and collapse the column with it, so a browser with no media shows the device controls at full width rather than beside a hole. */ .calls-device-layout:has(> .calls-preview-wrap > #preview[hidden]) > .calls-preview-wrap { display: none; } .calls-preview-wrap { border-radius: 20px; overflow: hidden; background: #000; } .calls-preview { display: block; max-width: none; aspect-ratio: 16 / 9; border: 0; border-radius: 0; } .calls-preview[hidden] { display: none; } .calls-pickers { margin-block-start: 0; } .calls-pickers > div { flex: 1 1 12rem; min-inline-size: 0; } .calls-pickers select { max-inline-size: 100%; } /* The rule above makes every picker a ~12rem flex item, which is right for the three selects and wrong for the studio switch: it carries a sentence of explanation, and squeezed into a select's column that sentence sets in a ribbon. More specific than the line above rather than ordered after it, so it survives the two being reordered. */ .calls-pickers > .calls-raw-row { flex: 1 1 100%; } @media (min-width: 64rem) { .calls-device-layout { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); } .calls-device-layout:has(> .calls-preview-wrap > #preview[hidden]) { grid-template-columns: minmax(0, 1fr); } } /* The holding screen's audio test (spec §15.20): a disclosure under the device choices, one row per test — the button, its meter, and the sentence that says what it found. The sentence is the result; the meter only shows it happening. */ .calls-audio-test { margin-block: var(--rst-sp-3); } .calls-audio-test[hidden] { display: none; } /* The holding screen keeps the speaker row's place while it is hidden: devices.js reveals it only after the devices are enumerated, and it is the last thing in the panel, so a row appearing there would still push the page below the panel down (r3 audit F2: the controls dropped ~180px, CLS 0.0116). A browser that cannot choose an output keeps a row's worth of empty space at the panel's foot instead. Only on the holding screen: the room's popover has no page below it to move. */ #devices #speaker-row[hidden] { display: block; visibility: hidden; } .calls-audio-test summary { cursor: pointer; font-weight: 600; } .calls-audio-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--rst-sp-2) var(--rst-sp-3); margin-block: var(--rst-sp-3); } .calls-audio-row meter { flex: 1 1 8rem; max-inline-size: 16rem; block-size: 0.75rem; } .calls-audio-row .calls-raw-help, .calls-audio-result { flex-basis: 100%; margin: 0; } .calls-audio-result { color: var(--rst-text); font-weight: 600; } /* ─── The call window (2026-09-28) ──────────────────────────────────── The whole call is one rounded window, inset from the viewport by a modest margin — 20px on a desktop, 12px in a narrow window, none on a phone, where it goes edge to edge as the iPhone app does — on the page's own quiet background, and the page never scrolls. Inside it is the Mac's full-window call: the picture fills the window, and the title, the notices, the bar, the panels and the popovers are over it. Everything over the picture is the one glass of 0.3.7: black at 62 % over a blurred, saturated backdrop, and no hairline edges. The black does the work, so white on it is 6.2:1 even over a white wall and where backdrop-filter is missing — there a near-opaque black stands in for the blur (the @supports below). The window sets color-scheme: dark, so every rst-* token inside it (the chat panel, the device selects) takes its dark half: what is under it is always video. */ html:has(> body > .calls-window), body:has(> .calls-window) { overflow: hidden; block-size: 100%; /* tokens.css reserves a scrollbar's width on every page; a page that cannot scroll has none, and the window would sit 15px off centre. */ scrollbar-gutter: auto; } body:has(> .calls-window) { background: var(--rst-bg); } .calls-window { --calls-margin: 20px; --calls-radius: 18px; /* The bar with its margin under it: what the self view, the filmstrip and a name at the bottom rise by while it shows (44 + 20 + 16, the Mac's barReserve). */ --calls-bar-reserve: 80px; --calls-lift: 0px; --calls-title-h: 52px; --calls-glass: rgb(0 0 0 / 0.62); /* Panels read rather than glanced at (the popovers, the title's menu, chat): Calma's own surface, in its dark half, as the window is. */ --calls-panel: var(--rst-surface); --calls-fade-out: 0.35s; --calls-fade-in: 0.2s; position: fixed; inset: var(--calls-margin); margin: 0; /* clip, not hidden: a hidden box can still be scrolled, and focusing something in a panel that is sliding in from off the edge would scroll the whole call sideways. */ overflow: clip; border-radius: var(--calls-radius); background: #0b0b0c; color: #fff; color-scheme: dark; box-shadow: 0 1px 3px rgb(0 0 0 / 0.12), 0 16px 48px rgb(0 0 0 / 0.24); isolation: isolate; font-size: var(--rst-fs-base, 1rem); } @media (max-width: 900px) { .calls-window { --calls-margin: 12px; --calls-radius: 16px; } } /* A phone, upright or on its side: edge to edge. */ @media (max-width: 599px), (max-height: 499px) and (pointer: coarse) { .calls-window { --calls-margin: 0px; --calls-radius: 0px; box-shadow: none; } } .calls-window[data-chrome='shown'] { --calls-lift: calc(var(--calls-bar-reserve) - 8px); } .calls-glass, .calls-window .calls-tile-name, .calls-window #controls, .calls-window .calls-notice, .calls-window .calls-status:not(:empty), .calls-window .calls-ice-warning, .calls-window .calls-share-notice-row, .calls-window .calls-sound-banner, .calls-window .calls-floor-notice, .calls-window .calls-queue { background: var(--calls-glass); color: #fff; -webkit-backdrop-filter: blur(18px) saturate(1.6); backdrop-filter: blur(18px) saturate(1.6); } @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .calls-glass, .calls-window .calls-tile-name, .calls-window #controls, .calls-window .calls-notice, .calls-window .calls-status:not(:empty), .calls-window .calls-ice-warning, .calls-window .calls-share-notice-row, .calls-window .calls-sound-banner, .calls-window .calls-floor-notice, .calls-window .calls-queue { background: rgb(18 18 18 / 0.92); } } /* The gate's fallback check turns the blur off to measure the black alone, which is what a browser without backdrop-filter draws under the @supports above. */ .calls-window[data-no-glass] * { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; } .calls-window :focus-visible { outline: 2px solid #fff; outline-offset: 2px; } .calls-window a { color: #fff; } /* What comes and goes with the controls: the title and the bar, and the quiet status line. Opacity only, never a slide, eased out: 0.35s going, 0.2s coming back (ControlsVisibility's fades). They keep their place and their pointer while faded: a press on one that is not showing shows it and does nothing else (call-window.js), and a screen reader always has them. With reduced motion they never go at all. */ .calls-fades { transition: opacity var(--calls-fade-in) ease-out; } .calls-window[data-chrome='hidden'] .calls-fades { opacity: 0; transition-duration: var(--calls-fade-out); } .calls-window[data-chrome='hidden'][data-pointer-inside], .calls-window[data-chrome='hidden'][data-pointer-inside] * { cursor: none !important; } /* ── The title: a window title, only for a title someone chose ── */ /* No pill (2026-10-02). A chosen title sits where a Mac window's title would, centred in the strip across the top of the window, plain white over a dark scrim that fades with it and the controls (the Mac's topShade). A title Calls wrote is the page's heading for a screen reader and nothing on screen, and the strip then takes no room, so the notices sit at the top. noscript.css turns all of this back into a plain block for a browser with no script. */ .calls-top { position: absolute; z-index: 4; inset-block-start: 0; inset-inline: 0; display: flex; align-items: center; justify-content: center; block-size: 44px; padding: 0 72px; pointer-events: none; } .calls-top-named::before { content: ''; position: absolute; z-index: -1; inset: 0 0 auto; block-size: 112px; /* Dark enough through the strip itself for white text over a white picture (4.5:1, the window scenario's contrast check), then fading. */ background: linear-gradient(rgb(0 0 0 / 0.72) 0, rgb(0 0 0 / 0.62) 44px, rgb(0 0 0 / 0)); pointer-events: none; } .calls-top h1 { min-inline-size: 0; max-inline-size: 100%; margin: 0; font-size: 0.875rem; font-weight: 600; line-height: 1.3; letter-spacing: 0; color: #fff; text-shadow: 0 1px 2px rgb(0 0 0 / 0.5); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .calls-top:not(.calls-top-named) { block-size: 0; padding: 0; } .calls-top:not(.calls-top-named) h1 { position: absolute; inline-size: 1px; block-size: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); } .calls-top-line { margin: 1px 0 0; font-size: 0.8125rem; line-height: 1.35; color: rgb(255 255 255 / 0.85); } /* The More menu's account part: the standing line, who is signed in, the headings and separators of the account menu (estilo menu.css), drawn for the dark panel: same order, same words. */ .calls-menu a, .calls-menu form button { display: flex; align-items: center; min-block-size: 40px; padding: 0 12px; border: 0; border-radius: 9px; background: none; color: #fff; font: inherit; font-size: 0.9375rem; text-align: start; text-decoration: none; cursor: pointer; inline-size: 100%; } .calls-menu a:hover, .calls-menu form button:hover { background: rgb(255 255 255 / 0.1); } .calls-menu form { margin: 0; } .calls-menu-standing { margin: 0; padding: 6px 12px 8px; max-inline-size: 18rem; color: rgb(255 255 255 / 0.85); font-size: 0.8125rem; line-height: 1.4; } .calls-menu hr { inline-size: 100%; margin: 4px 0; border: 0; border-block-start: 1px solid rgb(255 255 255 / 0.12); } .calls-menu-heading { display: block; margin: 0; padding: 6px 12px 2px; color: rgb(255 255 255 / 0.6); font-size: 0.75rem; font-weight: 600; } .calls-menu-who { margin: 0; padding-block-end: 6px; } .calls-menu-address { display: block; padding: 0 12px; color: #fff; font-size: 0.8125rem; overflow-wrap: anywhere; } /* ── With no script: the page's sentence where the picture would be ── */ .calls-scriptless { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: calc(var(--calls-title-h) + 40px) 24px 24px; text-align: center; } .calls-scriptless p { margin: 0; max-inline-size: 34rem; line-height: 1.5; } .calls-scriptless-links { display: flex; gap: 16px; justify-content: center; } .calls-scriptless form { margin: 0; } /* ── The room: everything scripted, over the whole window ── */ .calls-room { position: absolute; inset: 0; } .calls-room[hidden] { display: none; } /* ── Notices: under the title, and never faded ── */ .calls-notices { position: absolute; z-index: 5; inset-block-start: calc(12px + var(--calls-title-h) + 8px); inset-inline: 12px; display: flex; flex-direction: column; align-items: center; gap: 8px; pointer-events: none; } .calls-notices > * { pointer-events: auto; max-inline-size: min(40rem, 100%); } .calls-window .calls-status { margin: 0; min-block-size: 0; padding: 8px 16px; border-radius: 18px; color: #fff; font-size: 0.875rem; font-weight: 600; text-align: center; } .calls-window .calls-status:empty { display: none; } /* "You are the only one here" is how the call is going, not a problem: it goes with the controls (a notice that is not important, on the Mac, fades with them). */ .calls-window .calls-status[data-quiet] { transition: opacity var(--calls-fade-in) ease-out; } .calls-window[data-chrome='hidden'] .calls-status[data-quiet] { opacity: 0; transition-duration: var(--calls-fade-out); } .calls-window .calls-status a { color: #fff; font-weight: 600; margin-inline-start: 8px; } .calls-window .calls-ice-warning { margin: 0; padding: 8px 16px; border-radius: 18px; font-size: 0.875rem; text-align: center; } .calls-window .calls-device-note { color: #fff; } /* "Copy call diagnostics" at the foot of the Devices popover (§15.22). */ .calls-copy-diagnostics { margin-block-start: var(--rst-sp-3); align-self: flex-start; } .calls-window .calls-copy-diagnostics { color: #fff; } /* The sharing notice (2026-09-28): a dark pill with its own Stop sharing button, and the recovery line under it. */ .calls-share-notice:not([hidden]) { display: flex; flex-direction: column; align-items: center; gap: 4px; } .calls-share-notice-row { display: inline-flex; align-items: center; gap: 8px; margin: 0; padding: 6px 6px 6px 14px; border-radius: 999px; color: #ffd99a; font-size: 0.875rem; font-weight: 600; } .calls-share-notice-row svg { inline-size: 1.15em; block-size: 1.15em; flex: none; } .calls-share-stop, .calls-sound-banner button, .calls-queue li button { min-block-size: 28px; padding: 2px 12px; border: 0; border-radius: 999px; background: #fff; color: #111; font: inherit; font-size: 0.875rem; font-weight: 600; cursor: pointer; box-shadow: none; } .calls-share-stop:hover, .calls-sound-banner button:hover, .calls-queue li button:hover { background: #e8e8e8; } .calls-share-recovery { margin: 0; padding: 4px 12px; border-radius: 999px; background: var(--calls-glass); font-size: 0.8125rem; } /* §5.5's relay notice, once a session, with its own Dismiss. */ .calls-floor-notice:not([hidden]) { display: flex; align-items: center; gap: 8px; padding: 6px 6px 6px 16px; border-radius: 18px; font-size: 0.875rem; } .calls-floor-notice p { margin: 0; } .calls-floor-notice button { display: grid; place-items: center; flex: none; inline-size: 28px; block-size: 28px; border: 0; border-radius: 999px; background: rgb(255 255 255 / 0.14); color: #fff; cursor: pointer; } .calls-floor-notice button .icon { inline-size: 16px; block-size: 16px; } /* The page's own "Click anywhere to turn on sound". */ .calls-sound-banner { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px 12px; padding: 6px 6px 6px 16px; border-radius: 999px; } .calls-sound-banner[hidden] { display: none; } .calls-sound-banner p { margin: 0; font-size: 0.875rem; font-weight: 600; } /* Each person waiting, with Admit. */ .calls-queue { padding: 8px 8px 8px 16px; border-radius: 18px; } .calls-queue h2 { margin: 0 0 4px; font-size: 0.75rem; font-weight: 600; color: rgb(255 255 255 / 0.85); text-transform: none; } .calls-queue ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; } .calls-queue li { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px; font-size: 0.875rem; overflow-wrap: anywhere; } .calls-queue li button { flex: none; } /* ── The stage ── */ .calls-stage { position: absolute; inset: 0; z-index: 0; display: flex; flex-direction: column; justify-content: flex-end; container-type: size; } .calls-grid { margin: 0; } .calls-tile { position: relative; margin: 0; min-inline-size: 0; overflow: hidden; aspect-ratio: 16 / 9; border: 0; border-radius: 8px; background: #212121; container-type: inline-size; } .calls-tile video { display: block; width: 100%; height: 100%; object-fit: cover; /* A crop takes more from the bottom than the top (35:65): a face is above the middle of the picture, and a crop through the forehead reads worse than one through the chest. The native apps' VideoFrame has the same headroom. */ object-position: 50% 35%; } .calls-tile-self:not(.calls-tile-share) video, .calls-preview { /* Mirror only our camera display, never remote video or shared screens. */ transform: scaleX(-1); } /* A shared screen (spec §5.4) is not a face: the part of it that matters is usually at the edges, so the whole screen is fitted into the tile rather than cropped to fill it. */ .calls-tile-share video { position: relative; object-fit: contain; object-position: 50% 50%; background: #000; } /* On the stage, what is beside or above the fitted screen is the same picture enlarged, soft and dimmed, rather than black bars, as the native apps' ScreenBackdrop: room.js draws a frame into this 64 × 36 canvas twice a second, and the browser's upscaling and a blur do the rest. It is under the video (which is positioned, and after it). */ .calls-tile-backdrop { display: none; } .calls-window #tiles > [data-stage].calls-tile-share > .calls-tile-backdrop { display: block; position: absolute; inset: -8%; inline-size: 116%; block-size: 116%; filter: blur(18px); opacity: 0.45; pointer-events: none; } .calls-window #tiles > [data-stage].calls-tile-share video { background: transparent; } /* Speaker view (0.4.0): the stage fills the whole window, edge to edge, as FaceTime's does, and the filmstrip floats over its bottom — tiles a sixth of the window high, 72 to 120px, centred while they fit and scrolling sideways once they do not — rising clear of the bar while the controls show (2026-10-05: until then the stage stopped above the filmstrip, leaving a black band behind it and the bar). The stage's caption rides just above the filmstrip. The stage is placed against .calls-stage rather than #tiles, so it neither scrolls with the filmstrip nor is clipped by it; the tiles stay children of #tiles, whose order is the reading order call-layout.js keeps. */ .calls-window #tiles[data-layout='speaker'] { --calls-strip-h: clamp(72px, calc(100cqh / 6), 120px); --calls-band: calc(var(--calls-strip-h) + 16px); display: flex; flex-direction: row; align-items: flex-end; justify-content: safe center; gap: 8px; min-block-size: 0; padding: 8px 12px; margin-block-end: var(--calls-lift); overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; transition: margin-block-end var(--calls-fade-in) ease-out; } .calls-window #tiles[data-layout='speaker']:not(:has(> figure:not([data-stage], .calls-tile-pip))) { --calls-band: 0px; padding: 0; } .calls-window #tiles[data-layout='speaker'] > figure { flex: none; block-size: var(--calls-strip-h); inline-size: auto; aspect-ratio: 16 / 9; } .calls-window #tiles[data-layout='speaker'] > figure:not([data-stage]) { cursor: pointer; z-index: 1; } .calls-window #tiles[data-layout='speaker'] > [data-stage] { position: absolute; inset: 0; block-size: auto; inline-size: auto; aspect-ratio: auto; border-radius: 0; background: #0b0b0c; } /* The stage's caption: bottom left of the window, just above the filmstrip (StageLabel in the native apps), and rising with it, clear of the bar, while the controls show. In a call of two there is no filmstrip (--calls-band is 0): the other person fills the window, you float in a corner, and the caption clears the bar alone. */ .calls-window #tiles[data-layout='speaker'] > [data-stage] .calls-tile-name { inset-block-end: calc(12px + var(--calls-band)); inset-inline-start: 12px; } .calls-window[data-chrome='shown'] #tiles[data-layout='speaker'] > [data-stage] .calls-tile-name { translate: 0 calc(-1 * var(--calls-lift)); } /* What the stage's camera-off cover keeps clear of: that caption, lifted. */ .calls-window #tiles[data-layout='speaker'] > [data-stage] > .calls-tile-off { --calls-caption-clear: calc(12px + 2rem + var(--calls-band) + var(--calls-lift)); } /* The self view, floating (Stage.selfFloats): about a fifth of the window's width, 150 to 260px at 16:9, 12px in from the corner it was put in (call-layout.js's data-corner), kept clear of the bar while it shows, of the title in the top left, and of an open chat. Dragged, it follows the pointer and settles in the corner it was headed for. */ .calls-window #tiles > .calls-tile-pip { --calls-pip-top: calc(12px + var(--calls-title-h) * var(--calls-title-on, 0) + 4px * var(--calls-title-on, 0)); position: absolute; z-index: 2; inline-size: clamp(150px, 20cqw, 260px); block-size: auto; aspect-ratio: 16 / 9; border-radius: 10px; box-shadow: 0 3px 16px rgb(0 0 0 / 0.35); inset: auto 12px calc(12px + max(0px, var(--calls-lift) - 4px)) auto; cursor: grab; transition: inset 0.2s ease-out, translate 0s; } .calls-window[data-chrome='shown'] #tiles > .calls-tile-pip { --calls-title-on: 1; } .calls-window #tiles[data-corner='top-left'] > .calls-tile-pip { inset: var(--calls-pip-top) auto auto 12px; } .calls-window #tiles[data-corner='top-right'] > .calls-tile-pip { inset: 12px 12px auto auto; } .calls-window #tiles[data-corner='bottom-left'] > .calls-tile-pip { inset: auto auto calc(12px + max(0px, var(--calls-lift) - 4px)) 12px; } .calls-window:has(.calls-chat-overlay:not([hidden])) #tiles:not([data-corner='top-left'], [data-corner='bottom-left']) > .calls-tile-pip { inset-inline-end: calc(12px + min(320px, 100cqw - 24px) + 12px); } .calls-window #tiles > .calls-tile-pip.calls-tile-dragging { transition: none; z-index: 4; cursor: grabbing; } .calls-window #tiles > .calls-tile-pip .calls-tile-name { inset-block-end: 6px; inset-inline-start: 6px; } /* Grid: every tile as large as it can be without scrolling (GridFit, which call-layout.js runs and writes as --calls-cols and the tile's size), until they would be under 200px wide, when it scrolls. The last row's names rise clear of the bar while it shows, by as much of the bar as reaches over them (--calls-grid-lift). */ .calls-window #tiles[data-layout='grid'] { flex: 1; display: grid; grid-template-columns: repeat(var(--calls-cols, 2), var(--calls-tile-w, 320px)); grid-auto-rows: var(--calls-tile-h, 180px); gap: 4px; place-content: center; padding: 4px; overflow: auto; scrollbar-width: none; } .calls-window #tiles[data-layout='grid'] > figure { inline-size: var(--calls-tile-w, 320px); block-size: var(--calls-tile-h, 180px); aspect-ratio: auto; border-radius: 6px; } .calls-window[data-chrome='shown'] #tiles[data-layout='grid'] > [data-last-row] .calls-tile-name { translate: 0 calc(-1 * var(--calls-grid-lift, 0px)); } /* The tile caption: the name, and, when the pair has failed or the browser refused to play the track, the sentence and the control beside it — plus the removal link a member gets. A glass capsule of its own, with the muted mark inside it (shape and word). */ .calls-tile-name { position: absolute; inset-block-end: 8px; inset-inline-start: 8px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; max-inline-size: calc(100% - 16px); padding: 3px 10px; border-radius: 999px; color: #fff; font-size: var(--rst-fs-sm); line-height: 1.35; } .calls-tile-name:has(.calls-tile-hear:not([hidden]), .calls-tile-notice:not([hidden])) { border-radius: 14px; } .calls-tile-name > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .calls-tile-name button, .calls-tile-name a { min-block-size: 24px; min-inline-size: 24px; padding: 0 6px; border: 0; border-radius: 8px; background: rgb(255 255 255 / 0.14); color: #fff; font: inherit; cursor: pointer; } /* Never colour alone: a failed connection says so in words, and the weight is only a second cue on top of the sentence. */ .calls-tile-note { color: #fff; font-weight: 600; } .calls-tile-name [rst-badge] { color: #fff; border-color: rgb(255 255 255 / 0.5); } /* Spec §15.19: a muted peer's badge sits beside their name — the slashed microphone and the vendored badge's word, so the state is a shape and a word and never a colour. */ .calls-tile-muted { display: inline-flex; align-items: center; gap: 4px; flex: none; } .calls-tile-muted .icon { inline-size: 1em; block-size: 1em; } .calls-tile-muted[hidden], .calls-tile-off[hidden] { display: none; } /* A camera that is off sends black frames, or holds the last one. The cover says what that is instead of leaving a frozen face or a black rectangle to be read as a fault. It lies over the video rather than hiding it: the element is also what plays the peer's audio. Its symbol and words are centred in the whole tile, as FaceTime has them, with the caption over the bottom edge (2026-10-05; from 0.4.1 they were centred in the space above the caption, which left them high). Only on a tile so short that centred they would reach the caption do they rise, and by no more than that: the two spacers share the free space equally until the lower one would be shorter than the caption's clearance (--calls-caption-clear), when flexbox holds it at that and gives the rest to the upper one. The native apps' PlaceholderPlacement is the same rule. */ .calls-tile-off { --calls-caption-clear: 2.75rem; position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; background: #212121; color: rgb(255 255 255 / 0.8); font-size: var(--rst-fs-sm); } .calls-tile-off::before, .calls-tile-off::after { content: ''; flex: 1 1 0; } .calls-tile-off::after { min-block-size: var(--calls-caption-clear); } .calls-tile-off > * { flex: none; } .calls-tile-off > * + * { margin-block-start: 8px; } .calls-tile-off .icon { inline-size: 2rem; block-size: 2rem; } .calls-tile-camera-off .calls-tile-name { z-index: 1; } /* What a tile says about its audio when the rim cannot: no sound, sound blocked, a microphone picking nothing up. Words, with the weight as a second cue, in the caption's own pill. The ⓘ is a native disclosure, so it is a keyboard control with no script of its own, and what it opens takes a line of the caption to itself. */ .calls-tile-hear { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; font-weight: 600; } .calls-tile-hear[hidden], .calls-tile-notice[hidden] { display: none; } .calls-info summary { cursor: pointer; list-style: none; min-inline-size: 24px; min-block-size: 24px; display: inline-grid; place-items: center; font-weight: 400; } .calls-info summary::-webkit-details-marker { display: none; } .calls-info[open] { flex-basis: 100%; } .calls-info p { margin: 4px 0 0; font-weight: 400; max-inline-size: 30rem; } /* A peer's own report about hearing us: a line of its own in the caption, because it is about the call and not about the name. */ .calls-tile-notice { flex-basis: 100%; font-weight: 600; } /* §15.24: "Samuel is connecting and can't hear you yet", the words steady and three dots after them that brighten in turn — waiting, not wrong. One steady ellipsis under reduced motion. The dots are aria-hidden: a screen reader hears the sentence once. */ .calls-ellipsis { display: inline-flex; letter-spacing: 0.04em; } @media (prefers-reduced-motion: no-preference) { .calls-ellipsis > span { animation: calls-ellipsis 1.4s ease-in-out infinite both; } .calls-ellipsis > span:nth-child(2) { animation-delay: 0.2s; } .calls-ellipsis > span:nth-child(3) { animation-delay: 0.4s; } } @keyframes calls-ellipsis { 0%, 80%, 100% { opacity: 0.35; } 40% { opacity: 1; } } /* A filmstrip tile, or any small one (0.4.1). A tile too small for the camera-off words keeps the symbol alone, then a smaller one. The caption stays one line: the name gives up its end to an ellipsis, the muted mark keeps its slashed microphone and drops the word (still read out: it is only visually hidden), and there is room past the caption's end for the warm rim's breath. */ @container (max-width: 20rem) { .calls-tile-name { flex-wrap: nowrap; box-sizing: border-box; max-inline-size: calc(100% - 22px); inset-block-end: 6px; inset-inline-start: 6px; padding: 2px 8px; font-size: var(--rst-fs-xs); } .calls-tile-name > span:first-child { flex: 0 1 auto; min-inline-size: 0; } .calls-tile-off { --calls-caption-clear: 2rem; } .calls-tile-name > :not(:first-child) { flex: none; } .calls-tile-muted > :not(svg) { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } @container (max-width: 15rem) { .calls-tile-off > :not(svg) { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } @container (max-width: 9rem) { .calls-tile-off .icon { inline-size: 1.25rem; block-size: 1.25rem; } } /* The speaking light on the name, which replaced the ring round the tile and the meter beside the name (0.3.7), and since 0.4.3 is the warm rim ("T5, warm soft breath"), as the native apps draw it (WarmRim.swift). room.js sets --on (1 while lit), --lvl (0..1, how loudly they are speaking) and the transitions' durations: --lvl-t, quick rising and slower falling, so it breathes with the voice, and --on-t, so --on goes out as --lvl reaches nothing rather than at once. Over the label's glass, in its shape, three layers of warm white (hue 38°): a 1px rim inside the edge, light bleeding inwards from it, and a breath just outside it once the voice rises. A box-shadow, so nothing moves when it lights; the registered properties are what let the numbers themselves be transitioned, and a browser without @property steps five times a second instead. With reduced motion, room.js holds one steady level and nothing is transitioned. */ @property --lvl { syntax: ''; inherits: true; initial-value: 0; } @property --on { syntax: ''; inherits: true; initial-value: 0; } .calls-tile-name { --on: 0; --lvl: 0; } .calls-tile-name::after { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; box-shadow: inset 0 0 0 1px hsl(38 80% 82% / calc(var(--on) * (0.1 + var(--lvl) * 0.45))), inset 0 0 calc(3px + var(--lvl) * 6px) hsl(38 80% 72% / calc(var(--on) * (0.06 + var(--lvl) * 0.3))), 0 0 calc(1px + var(--lvl) * 7px) hsl(38 80% 66% / calc(var(--on) * var(--lvl) * 0.25)); } @media (prefers-reduced-motion: no-preference) { .calls-tile-name { transition: --lvl var(--lvl-t, 300ms) ease-out, --on var(--on-t, 0ms) step-end; } } /* A tile's own buttons (2026-09-29): ⋯, which opens the tile's menu, and a Pinned badge while the tile is pinned (pressing it unpins). In the top corner, and only while the pointer or the keyboard is in the tile or its menu is open — and never with the controls gone, so the picture is clean. On a screen with no hover they come with the controls. The Pinned badge shows with the controls whatever the pointer does, so a pinned stage says so. */ .calls-tile-tools { position: absolute; inset-block-start: 8px; inset-inline-end: 8px; display: flex; gap: 4px; z-index: 2; } .calls-tile-tools > .calls-tool { opacity: 0; transition: opacity var(--calls-fade-in, 0.2s) ease-out; } .calls-tile:hover > .calls-tile-tools > .calls-tool, .calls-tile:focus-within > .calls-tile-tools > .calls-tool, .calls-tile.calls-tile-menu-open > .calls-tile-tools > .calls-tool, .calls-tile[data-pinned] > .calls-tile-tools > .calls-tool-pin { opacity: 1; } @media (hover: none) { .calls-window[data-chrome='shown'] .calls-tile-tools > .calls-tool { opacity: 1; } } .calls-window[data-chrome='hidden'] .calls-tile:not(:focus-within, .calls-tile-menu-open) > .calls-tile-tools > .calls-tool { opacity: 0; transition-duration: var(--calls-fade-out, 0.35s); } .calls-tool[hidden] { display: none; } .calls-window #tiles[data-layout='speaker'] > [data-stage] > .calls-tile-tools { inset-block-start: 12px; inset-inline-end: 12px; } .calls-tool { display: inline-flex; align-items: center; justify-content: center; gap: 4px; min-inline-size: 30px; min-block-size: 30px; padding: 2px 8px; border: 0; border-radius: 999px; background: var(--calls-glass, rgb(0 0 0 / 0.62)); -webkit-backdrop-filter: blur(18px) saturate(1.6); backdrop-filter: blur(18px) saturate(1.6); color: #fff; font: inherit; font-size: var(--rst-fs-xs); cursor: pointer; } .calls-tool-more { padding: 2px; } .calls-tool .icon { inline-size: 16px; block-size: 16px; } .calls-tool:focus-visible { outline: 2px solid #fff; outline-offset: 1px; } .calls-tool-more[aria-expanded='true'] { background: #f0f0f0; color: #1a1a1a; } .calls-tile[data-pinned] .calls-tool-pin { background: #fff; color: #000; } /* The tile menu: one panel for every tile, over the picture beside the ⋯ that opened it (call-layout.js sets its place). */ .calls-tile-menu { position: absolute; z-index: 8; inset-block-start: var(--calls-menu-y, 0); inset-inline-start: var(--calls-menu-x, 0); display: flex; flex-direction: column; min-inline-size: 13rem; padding: 6px; border-radius: 14px; background: var(--calls-panel); color: #fff; box-shadow: 0 8px 28px rgb(0 0 0 / 0.4); } .calls-tile-menu[hidden], .calls-tile-menu [hidden] { display: none; } .calls-menu button { display: flex; align-items: center; min-block-size: 40px; padding: 0 12px; border: 0; border-radius: 9px; background: none; color: #fff; font: inherit; font-size: 0.9375rem; text-align: start; cursor: pointer; inline-size: 100%; } .calls-menu button:hover:not(:disabled) { background: rgb(255 255 255 / 0.1); } .calls-menu button:disabled { opacity: 0.45; cursor: default; } .calls-tile-menu .calls-tile-remove { margin-block-start: 4px; border-block-start: 1px solid rgb(255 255 255 / 0.12); border-radius: 0 0 9px 9px; } /* The tile being dragged: lifted over the others, with the accent edge, following the pointer (its translate is set as it moves); the others make room as it passes over their places. Reduced motion: no growth and no easing back. */ .calls-tile-lifted { z-index: 3; box-shadow: 0 10px 30px #0009, 0 0 0 3px var(--rst-accent); cursor: grabbing; } @media (prefers-reduced-motion: no-preference) { .calls-tile-lifted { scale: 1.04; } } /* ── The bar ── One glass capsule along the bottom, at most 760px wide, the buttons borderless on it: a white glyph, a light wash under the pointer, and a filled capsule only for a state — microphone or camera off in amber with the slashed glyph, sharing or a panel open in near-white with a dark glyph, Leave in red (CallOverlay's colours: amber 5.7:1, red 5.0:1 and the near-white 15:1 for their glyphs). The state is also the slashed glyph, the word in .calls-toggle-state and aria-pressed, so never colour alone. Each button is named for a screen reader and has a tooltip, above it, on hover and on keyboard focus. */ .calls-window #controls { position: absolute; z-index: 6; inset-block-end: 16px; inset-inline-start: 50%; translate: -50% 0; display: flex; flex-wrap: nowrap; align-items: center; gap: 10px; inline-size: min(760px, calc(100% - 32px)); box-sizing: border-box; padding: 10px; margin: 0; border-radius: 999px; box-shadow: 0 4px 24px rgb(0 0 0 / 0.25); } .calls-sheet { display: contents; } .calls-bar-spacer { flex: 1 1 24px; min-inline-size: 24px; } .calls-bar-phone, .calls-window #controls .calls-bar-phone { display: none; } .calls-window #controls form { margin: 0; display: flex; } .calls-window #controls button { position: relative; display: inline-grid; place-items: center; flex: none; inline-size: 44px; block-size: 44px; min-inline-size: 44px; min-block-size: 44px; padding: 0; border: 0; border-radius: 999px; background: transparent; box-shadow: none; color: #fff; font: inherit; font-weight: 500; cursor: pointer; } .calls-window #controls button[hidden] { display: none; } .calls-window #controls button .icon { inline-size: 20px; block-size: 20px; grid-area: 1 / 1; } .calls-window #controls .calls-g { display: contents; } .calls-window #controls [aria-pressed='true'] > .calls-g-rest, .calls-window #controls :not([aria-pressed='true']) > .calls-g-pressed, .calls-window #controls #layout-toggle[data-mode='grid'] > .calls-g-speaker, .calls-window #controls #layout-toggle:not([data-mode='grid']) > .calls-g-grid { display: none; } /* The words are for a screen reader on the bar, and shown in a phone's controls sheet. */ .calls-bar-label { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } .calls-window #controls button:hover { background: rgb(255 255 255 / 0.1); } .calls-window #controls button:active { background: rgb(255 255 255 / 0.18); } .calls-window #controls button:disabled { opacity: 0.45; cursor: default; background: transparent; } .calls-window #controls #mute[aria-pressed='true'], .calls-window #controls #camera[aria-pressed='true'] { background: #a64d05; color: #fff; } .calls-window #controls #share[aria-pressed='true'], .calls-window #controls button[aria-expanded='true'] { background: #f0f0f0; color: #1a1a1a; } .calls-window #controls #leave { inline-size: 62px; background: #d62924; color: #fff; } .calls-window #controls #leave:hover { background: #c42420; } .calls-window #controls #leave:active { background: #b3201c; } .calls-window #controls button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } /* The unread count on Chat: Calls' accent (Calma's hue 220, in its dark half here) with its own on-accent text. */ .calls-badge:empty { display: none; } .calls-badge { position: absolute; inset-block-start: -3px; inset-inline-end: -3px; min-inline-size: 18px; block-size: 18px; padding: 0 5px; box-sizing: border-box; border-radius: 999px; background: var(--rst-accent); color: var(--rst-on-accent); font-size: 11px; font-weight: 700; line-height: 18px; text-align: center; } /* 0.4.1: the bar's glyphs sit on the bar's centre line by their ink, not by their boxes. A Lucide glyph is drawn to its 24-unit square, and where its weight falls inside it is its own. Each glyph that needs it gets one line here, measured by the browser gate's glyphs scenario, which holds every glyph to within half a pixel of the centre line — the same rule, the same table and the same test as the native CallGlyph. */ .calls-window #controls button .icon { translate: 0 var(--calls-glyph-dy, 0px); } .calls-window #controls #mute > .calls-g-rest > .icon { --calls-glyph-dy: -0.9px; } .calls-window #controls form button .icon { --calls-glyph-dy: -1.6px; } .calls-window #controls #toggle-chat .icon { --calls-glyph-dy: 0.75px; } .calls-window #controls #leave .icon { --calls-glyph-dy: 1.1px; } /* The tooltip: the button's own words, above it, after a moment under the pointer and at once for the keyboard. */ .calls-window #controls [data-tip]::after { content: attr(data-tip); position: absolute; inset-block-end: calc(100% + 14px); inset-inline-start: 50%; translate: -50% 0; padding: 4px 10px; border-radius: 8px; background: rgb(20 20 20 / 0.94); color: #fff; font-size: 0.75rem; font-weight: 500; line-height: 1.4; white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity 0.12s ease-out; } .calls-window #controls [data-tip-on][aria-pressed='true']::after { content: attr(data-tip-on); } .calls-window #controls [data-tip]:hover::after { opacity: 1; transition-delay: 0.5s; } .calls-window #controls [data-tip]:focus-visible::after { opacity: 1; transition-delay: 0s; } .calls-window #controls [aria-expanded='true']::after, .calls-window[data-chrome='hidden'] #controls [data-tip]::after { opacity: 0; transition-delay: 0s; } /* ── The popovers: layout, devices and More ── Over the picture above the button that opened it (call-window.js sets --calls-pop-x), read rather than glanced at: opaque and dark, with the dark theme's full-contrast labels (the Mac's panelFill). A popover holds the controls up while it is open. */ .calls-popover { position: absolute; z-index: 7; inset-block-end: calc(var(--calls-bar-reserve) + 8px); inset-inline-start: var(--calls-pop-x, 16px); box-sizing: border-box; max-inline-size: calc(100% - 24px); max-block-size: calc(100% - var(--calls-bar-reserve) - 28px); overflow: auto; padding: 12px 14px; border-radius: 16px; background: var(--calls-panel); color: #fff; box-shadow: 0 8px 28px rgb(0 0 0 / 0.4); } .calls-popover[hidden] { display: none; } .calls-popover-title { margin: 0 0 8px; font-size: 0.9375rem; font-weight: 650; } .calls-popover-help { margin: 10px 0 0; max-inline-size: 16rem; font-size: 0.8125rem; color: rgb(255 255 255 / 0.8); } .calls-popover [estilo-call-segmented] { background: rgb(255 255 255 / 0.08); border-color: transparent; } .calls-popover [estilo-call-segmented] button { color: #fff; min-inline-size: 88px; } .calls-popover [estilo-call-segmented] [aria-pressed='true'] { background: #f0f0f0; color: #1a1a1a; border-color: transparent; } .calls-window .calls-devices { inline-size: 320px; flex-direction: column; flex-wrap: nowrap; gap: 12px; margin: 0; } .calls-window .calls-devices:not([hidden]) { display: flex; } .calls-window .calls-devices > div { flex: none; } .calls-window .calls-devices .calls-popover-title { margin: 0; } .calls-window .calls-devices .calls-raw-help { color: rgb(255 255 255 / 0.8); } .calls-menu { padding: 6px; min-inline-size: 14rem; } .calls-menu a.calls-menu-danger { color: #ff9c96; font-weight: 600; } /* ── Chat: a sidebar sliding over the picture's right-hand side ── */ .calls-chat-overlay { position: absolute; z-index: 5; inset-block: 12px calc(var(--calls-bar-reserve) + 8px); inset-inline-end: 12px; inline-size: min(320px, calc(100% - 24px)); display: flex; flex-direction: column; border-radius: 16px; background: var(--rst-surface); color: var(--rst-text); box-shadow: 0 8px 28px rgb(0 0 0 / 0.35); transition: translate 0.2s ease-out, opacity 0.2s ease-out, display 0.2s allow-discrete; } .calls-chat-overlay[hidden] { display: none; translate: calc(100% + 12px) 0; opacity: 0; } @starting-style { .calls-chat-overlay:not([hidden]) { translate: calc(100% + 12px) 0; opacity: 0; } } .calls-chat-overlay .chat { flex: 1; min-block-size: 0; margin: 0; border: 0; border-radius: 16px; padding: 14px 14px 12px; } .calls-chat-overlay .chat-title { color: var(--rst-text); font-size: 0.9375rem; font-weight: 650; } .calls-chat-overlay .chat-log { flex: 1; min-block-size: 4rem; max-block-size: none; } .calls-chat-overlay .calls-chat-collapsed { display: none; } .calls-chat-close { position: absolute; inset-block-start: 8px; inset-inline-end: 8px; display: grid; place-items: center; inline-size: 32px; block-size: 32px; border: 0; border-radius: 999px; background: transparent; color: var(--rst-text); cursor: pointer; } .calls-chat-close:hover { background: rgb(255 255 255 / 0.1); } .calls-chat-close .icon { inline-size: 18px; block-size: 18px; } /* A name rising clear of the bar moves with the bar's fade; the warm rim's own two transitions are kept beside it, or this would replace them. */ @media (prefers-reduced-motion: no-preference) { .calls-window .calls-tile-name { transition: --lvl var(--lvl-t, 300ms) ease-out, --on var(--on-t, 0ms) step-end, translate var(--calls-fade-in) ease-out; } } @media (prefers-reduced-motion: reduce) { .calls-fades, .calls-window #tiles[data-layout='speaker'], .calls-window #tiles > .calls-tile-pip { transition: none; } /* Panels fade rather than slide. */ .calls-chat-overlay { transition: opacity 0.1s linear, display 0.1s allow-discrete; } .calls-chat-overlay[hidden] { translate: none; } @starting-style { .calls-chat-overlay:not([hidden]) { translate: none; opacity: 0; } } } /* ── A phone: More controls on the left, Leave on the right, and the rest in a sheet above them, as the iPhone app has it ── */ @media (max-width: 599px), (max-height: 499px) and (pointer: coarse) { .calls-window { --calls-bar-reserve: 84px; } .calls-window #controls { inset-inline: 16px; inset-block-end: 16px; translate: none; inline-size: auto; justify-content: space-between; padding: 0; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: none; pointer-events: none; } .calls-window #controls > button { pointer-events: auto; inline-size: 56px; block-size: 56px; background: var(--calls-glass); -webkit-backdrop-filter: blur(18px) saturate(1.6); backdrop-filter: blur(18px) saturate(1.6); box-shadow: 0 4px 16px rgb(0 0 0 / 0.25); } .calls-window #controls > button .icon { inline-size: 24px; block-size: 24px; } .calls-window #controls > #leave { inline-size: 56px; background: #d62924; } .calls-window #controls .calls-bar-phone { display: inline-grid; } .calls-window #controls [data-tip]::after { display: none; } .calls-bar-spacer { display: none; } .calls-sheet { display: none; pointer-events: auto; } .calls-sheet[data-open] { position: absolute; inset-block-end: calc(100% + 12px); inset-inline: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; padding: 12px; border-radius: 22px; background: color-mix(in srgb, var(--rst-surface) 96%, transparent); box-shadow: 0 8px 28px rgb(0 0 0 / 0.4); } .calls-window #controls .calls-sheet[data-open] > button, .calls-window #controls .calls-sheet[data-open] > form > button { display: flex; flex-direction: column; justify-content: center; gap: 6px; inline-size: 100%; block-size: auto; min-block-size: 76px; padding: 8px 4px; border-radius: 16px; background: rgb(255 255 255 / 0.08); font-size: var(--rst-fs-xs); line-height: 1.25; text-align: center; } .calls-sheet[data-open] > form { display: flex; } .calls-window #controls .calls-sheet[data-open] > button[hidden] { display: none; } .calls-window #controls .calls-sheet[data-open] .icon { inline-size: 22px; block-size: 22px; flex: none; } .calls-sheet[data-open] .calls-bar-label { position: static; inline-size: auto; block-size: auto; overflow: visible; clip-path: none; white-space: normal; } .calls-sheet[data-open] .calls-badge { inset-block-start: 6px; inset-inline-end: 10px; } .calls-popover { inset-inline: 12px; inline-size: auto; max-inline-size: none; } .calls-window .calls-devices { inline-size: auto; } .calls-chat-overlay { inset-inline: 8px; inset-block-start: 8px; inline-size: auto; } .calls-top { padding-inline: 16px; } .calls-window #tiles > .calls-tile-pip { inline-size: clamp(88px, 28cqw, 150px); aspect-ratio: 3 / 4; } .calls-window #tiles[data-layout='speaker'] > figure:not([data-stage]) { aspect-ratio: 3 / 4; } } @media (max-height: 499px) and (pointer: coarse) { .calls-window #tiles > .calls-tile-pip { inline-size: clamp(88px, 20cqw, 150px); aspect-ratio: 4 / 3; } .calls-window #tiles[data-layout='speaker'] > figure:not([data-stage]) { aspect-ratio: 4 / 3; } .calls-sheet[data-open] { grid-template-columns: repeat(5, minmax(0, 1fr)); } .calls-window #controls .calls-sheet[data-open] > button, .calls-window #controls .calls-sheet[data-open] > form > button { min-block-size: 64px; } } /* ── "Report a problem" (spec §15.26) ── report.js's sheet: a modal over the call or the holding screen, in the page's own theme, which inside the call window is the window's dark one (color-scheme: dark is inherited into the top layer). Centred by the browser, and never wider than the screen less a margin, so a phone gets the whole width. */ .calls-report { box-sizing: border-box; inline-size: min(28rem, calc(100vw - 32px)); max-block-size: calc(100dvh - 32px); overflow: auto; padding: var(--rst-sp-4); border: 1px solid var(--rst-line); border-radius: 16px; background: var(--rst-surface); color: var(--rst-text); box-shadow: var(--rst-shadow-pop); } .calls-report::backdrop { background: rgb(0 0 0 / 0.55); } .calls-report-form { margin: 0; } .calls-report-title { margin: 0 0 var(--rst-sp-3); font-size: 1.0625rem; font-weight: 650; } .calls-report textarea { box-sizing: border-box; inline-size: 100%; resize: vertical; } .calls-report-disclosure { margin: var(--rst-sp-3) 0 0; color: var(--rst-text-muted); font-size: var(--rst-fs-sm); } .calls-report-error { margin: var(--rst-sp-3) 0 0; color: var(--rst-tone-negative-fg); font-size: var(--rst-fs-sm); } .calls-report-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--rst-sp-2); margin-block-start: var(--rst-sp-4); } .calls-report-done { border-radius: var(--rst-radius-sm); outline-offset: 4px; } .calls-report-done p { margin: 0 0 var(--rst-sp-2); } .calls-report-thanks { font-weight: 600; } /* One click selects the whole reference, for copying by hand. */ .calls-report-ref { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.06em; -webkit-user-select: all; user-select: all; } .calls-report-copied { margin: 0; min-block-size: 1.2em; color: var(--rst-text-muted); font-size: var(--rst-fs-sm); } /* Beside the way out of a call that is over, read like its links. */ .calls-report-after { padding: 0; border: 0; background: none; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; } .calls-window .calls-status .calls-report-after { margin-inline-start: 8px; color: #fff; font-weight: 600; } .calls-quality-row .calls-raw-help { margin-block-start: var(--rst-sp-1); }