domma-cms 0.107.1 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,6 +48,13 @@
48
48
  you let them in. <strong>Copy link</strong> gives you a link to pass on yourself. The same
49
49
  button is in online bookings' calls (Booking Pro) and online meetings (Calendar); in a
50
50
  Messages call it rings in another team member.</p>
51
+ <p><strong>Blur your background</strong>: click <strong>Blur</strong> on the join screen or in the
52
+ call's control bar, and everyone sees you sharp against a blurred room. It works out what is
53
+ you inside your own browser - no picture is sent anywhere to do it, and nothing goes to Google
54
+ or anyone else. The first time takes a few seconds while it loads; after that your browser
55
+ remembers the choice for your next call. It needs a reasonably recent computer: if yours
56
+ cannot keep up, blur turns itself off and says so. Everyone in any call has it - your guests
57
+ too.</p>
51
58
  <p>Who may start a call is the <strong>Calls</strong> permission under <strong>System &gt;
52
59
  Roles</strong>; Admin is given it once, the first time a Tool with calls runs. A site without
53
60
  any of those Tools has no instant calls at all.</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.107.1",
3
+ "version": "0.108.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The beat for background blur (blur.js): a Worker, because a background tab slows the page's own timers to once
3
+ * a second and the others in the call would see a frozen picture. Post it a delay in ms; it answers once, then.
4
+ */
5
+ self.onmessage = (e) => {
6
+ setTimeout(() => self.postMessage(0), Math.max(0, Number(e.data) || 0));
7
+ };
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Background blur for the call page - the camera's picture with everything but the person blurred.
3
+ *
4
+ * Runs entirely in this browser: MediaPipe's selfie segmenter (vendored under ./vendor/mediapipe, Apache 2.0,
5
+ * its usage logging patched out) marks which pixels are the person, and a canvas draws the frame blurred with the
6
+ * person sharp on top. The canvas becomes a video track (captureStream) that call.js sends instead of the
7
+ * camera's. No frame leaves the browser except as the call's own video, and nothing is fetched from anyone but
8
+ * this site.
9
+ *
10
+ * Loaded on demand (import()) the first time blur is turned on - about 12 MB, compressed in transit - so a call
11
+ * that never blurs costs nothing.
12
+ *
13
+ * const blur = await startBlur(cameraTrack); // throws when this browser cannot run it (or not within 20 s)
14
+ * blur.track // the blurred track to send and show
15
+ * blur.paused = true // skip the work (a screen is being shared)
16
+ * blur.stop()
17
+ *
18
+ * blur.track ends by itself - an 'ended' event, then call.js puts the camera back - when this machine cannot keep
19
+ * up (a frame costing over SLOW_MS for SLOW_FOR) or the segmenter keeps failing; `blur.reason` says which.
20
+ *
21
+ * The beat comes from a tiny Worker (blur-clock.js), not requestAnimationFrame or the page's own timers: a
22
+ * background tab stops rAF and slows timers to once a second, and the others would see a frozen picture whenever
23
+ * the caller looked at another tab. Each frame leaves at least as long idle as the last one took, so a slow machine
24
+ * drops frames rather than its buttons and chat.
25
+ */
26
+ import {ImageSegmenter} from './vendor/mediapipe/vision_bundle.mjs';
27
+
28
+ const HERE = new URL('./vendor/mediapipe/', import.meta.url);
29
+ const MAX_WIDTH = 1280; // the camera is asked for 720p; anything larger is drawn at this width
30
+ const BG_SCALE = 6; // the background is blurred at 1/6 size, then scaled back up (cheap and smooth)
31
+ const KEEP = 0.35; // how much of the last frame's mask carries over (steadier edges, less flicker)
32
+ const SLOW_MS = 250; // a frame this slow on average (under 4 a second, with the idle gap)...
33
+ const SLOW_FOR = 5000; // ...for this long, and blur gives up
34
+ const START_LIMIT = 20000; // loading and the first frame, or it is not going to work here
35
+
36
+ let segmenterReady = null;
37
+
38
+ /** One segmenter for the page: the GPU where it works, the CPU otherwise. Dropped (and rebuilt next time) if it breaks. */
39
+ function segmenter() {
40
+ segmenterReady ??= (async () => {
41
+ const fileset = {
42
+ wasmLoaderPath: new URL('vision_wasm_internal.js', HERE).href,
43
+ wasmBinaryPath: new URL('vision_wasm_internal.wasm', HERE).href
44
+ };
45
+ const options = (delegate) => ({
46
+ baseOptions: {modelAssetPath: new URL('selfie_segmenter_landscape.tflite', HERE).href, delegate},
47
+ runningMode: 'VIDEO',
48
+ outputCategoryMask: false,
49
+ outputConfidenceMasks: true
50
+ });
51
+ try {
52
+ return await ImageSegmenter.createFromOptions(fileset, options('GPU'));
53
+ } catch {
54
+ return ImageSegmenter.createFromOptions(fileset, options('CPU'));
55
+ }
56
+ })();
57
+ segmenterReady.catch(() => { segmenterReady = null; }); // a failed load may be tried again
58
+ return segmenterReady;
59
+ }
60
+ function dropSegmenter(seg) {
61
+ segmenterReady = null;
62
+ try { seg?.close?.(); } catch { /* already gone */ }
63
+ }
64
+
65
+ const canvas = (w, h) => Object.assign(document.createElement('canvas'), {width: w, height: h});
66
+ const smooth = (x) => (x <= 0.25 ? 0 : x >= 0.75 ? 1 : (x - 0.25) * 2); // a soft threshold: firm edges, no halo of half-person
67
+ const timeout = (ms, what) => new Promise((_, reject) => setTimeout(() => reject(new Error(`${what} took too long`)), ms));
68
+
69
+ /** The beat: a Worker's timer, which a background tab does not slow down. The page's own timer where there is none. */
70
+ function clock(tick) {
71
+ let worker = null;
72
+ try {
73
+ worker = new Worker(new URL(`./blur-clock.js${new URL(import.meta.url).search}`, import.meta.url));
74
+ worker.onmessage = () => tick();
75
+ } catch { worker = null; }
76
+ let timer = null;
77
+ return {
78
+ after(ms) {
79
+ if (worker) worker.postMessage(ms);
80
+ else timer = setTimeout(tick, ms);
81
+ },
82
+ stop() { worker?.terminate(); clearTimeout(timer); }
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Start blurring a camera track.
88
+ *
89
+ * @param {MediaStreamTrack} source - the camera's video track (left running; the caller owns it)
90
+ * @returns {Promise<{track: MediaStreamTrack, stop: () => void, paused: boolean, reason: string}>}
91
+ */
92
+ export async function startBlur(source) {
93
+ const video = document.createElement('video');
94
+ video.muted = true;
95
+ video.playsInline = true;
96
+ try {
97
+ const seg = await Promise.race([(async () => {
98
+ const s = await segmenter();
99
+ video.srcObject = new MediaStream([source]);
100
+ await video.play();
101
+ if (!video.videoWidth) await new Promise(r => video.addEventListener('loadeddata', r, {once: true}));
102
+ return s;
103
+ })(), timeout(START_LIMIT, 'Starting blur')]);
104
+ return run(seg, source, video);
105
+ } catch (err) {
106
+ video.pause();
107
+ video.srcObject = null;
108
+ throw err;
109
+ }
110
+ }
111
+
112
+ function run(seg, source, video) {
113
+ const out = canvas(16, 16);
114
+ const g = out.getContext('2d');
115
+ const bg = canvas(16, 9);
116
+ const bgc = bg.getContext('2d');
117
+ const filters = 'filter' in bgc;
118
+ const person = canvas(16, 16);
119
+ const pc = person.getContext('2d');
120
+ let W = 0; let H = 0;
121
+ let maskCanvas = null; let mc = null; let maskImage = null; let last = null;
122
+
123
+ /** Size everything to the camera's picture - again whenever it changes (a phone turned, the browser adapting it). */
124
+ function fit() {
125
+ if (!video.videoWidth || !video.videoHeight) return false;
126
+ const scale = Math.min(1, MAX_WIDTH / video.videoWidth);
127
+ const w = Math.round(video.videoWidth * scale); const h = Math.round(video.videoHeight * scale);
128
+ if (w === W && h === H) return true;
129
+ W = w; H = h;
130
+ out.width = person.width = W; out.height = person.height = H;
131
+ bg.width = Math.max(16, Math.round(W / BG_SCALE)); bg.height = Math.max(9, Math.round(H / BG_SCALE));
132
+ if (filters) bgc.filter = 'blur(2px)';
133
+ return true;
134
+ }
135
+ fit();
136
+ // The canvas paints once before the first mask, so the track never starts with an empty frame.
137
+ g.drawImage(video, 0, 0, W, H);
138
+ const track = out.captureStream(30).getVideoTracks()[0];
139
+
140
+ function drawMask(mask) {
141
+ const w = mask.width; const h = mask.height;
142
+ if (!maskCanvas || maskCanvas.width !== w || maskCanvas.height !== h) {
143
+ maskCanvas = canvas(w, h);
144
+ mc = maskCanvas.getContext('2d');
145
+ maskImage = mc.createImageData(w, h);
146
+ last = new Float32Array(w * h);
147
+ }
148
+ const data = mask.getAsFloat32Array();
149
+ const px = maskImage.data;
150
+ for (let i = 0; i < data.length; i++) {
151
+ const v = last[i] = data[i] * (1 - KEEP) + last[i] * KEEP;
152
+ px[i * 4 + 3] = smooth(v) * 255;
153
+ }
154
+ mc.putImageData(maskImage, 0, 0);
155
+ }
156
+
157
+ function compose() {
158
+ // The background: shrunk and blurred, stretched back over the whole frame. Drawn a little past each edge,
159
+ // because a blurred picture fades out at its own border and the last frame would show through.
160
+ const pad = 2;
161
+ bgc.drawImage(video, -pad, -pad, bg.width + pad * 2, bg.height + pad * 2);
162
+ g.imageSmoothingEnabled = true;
163
+ g.imageSmoothingQuality = 'high';
164
+ g.drawImage(bg, 0, 0, W, H);
165
+ // The person: the mask (stretched, which feathers it) filled with the sharp frame.
166
+ pc.globalCompositeOperation = 'copy';
167
+ pc.imageSmoothingEnabled = true;
168
+ pc.drawImage(maskCanvas, 0, 0, W, H);
169
+ pc.globalCompositeOperation = 'source-in';
170
+ pc.drawImage(video, 0, 0, W, H);
171
+ g.drawImage(person, 0, 0);
172
+ }
173
+
174
+ const self = {track, paused: false, reason: '', stop};
175
+ let stopped = false;
176
+ let lastTs = 0;
177
+ let cost = 16; // a running average of one frame's work, in ms
178
+ let slowSince = 0;
179
+ let failures = 0;
180
+ const beat = clock(frame);
181
+
182
+ function frame() {
183
+ if (stopped) return;
184
+ const started = performance.now();
185
+ if (source.readyState === 'ended') { stop(); return; }
186
+ if (!self.paused && source.enabled && video.readyState >= 2 && fit()) {
187
+ let result = null;
188
+ try {
189
+ const ts = Math.max(lastTs + 1, started);
190
+ lastTs = ts;
191
+ result = seg.segmentForVideo(video, ts);
192
+ const mask = result?.confidenceMasks?.[0];
193
+ if (mask) { drawMask(mask); compose(); failures = 0; }
194
+ } catch (err) {
195
+ failures += 1;
196
+ if (failures === 1) console.warn('[call] blur:', err?.message || err);
197
+ if (failures > 30) { dropSegmenter(seg); stop('failed'); return; }
198
+ } finally {
199
+ try { result?.close?.(); } catch { /* nothing to free */ }
200
+ }
201
+ cost = cost * 0.9 + (performance.now() - started) * 0.1;
202
+ if (cost > SLOW_MS) {
203
+ slowSince ||= started;
204
+ if (started - slowSince > SLOW_FOR) { stop('slow'); return; }
205
+ } else slowSince = 0;
206
+ }
207
+ const spent = performance.now() - started;
208
+ const every = cost > 28 ? 1000 / 15 : 1000 / 30;
209
+ beat.after(Math.max(cost, every - spent, 4));
210
+ }
211
+
212
+ function stop(reason = '') {
213
+ if (stopped) return;
214
+ stopped = true;
215
+ self.reason = reason;
216
+ beat.stop();
217
+ video.pause();
218
+ video.srcObject = null;
219
+ track.stop();
220
+ track.dispatchEvent(new Event('ended'));
221
+ }
222
+
223
+ frame();
224
+ return self;
225
+ }
@@ -1 +1 @@
1
- .dmc{--dmc-accent: #4f7cff;--dmc-accent-solid: color-mix(in srgb, var(--dmc-accent) 84%, #000);--dmc-accent-soft: color-mix(in srgb, var(--dmc-accent) 18%, transparent);--dmc-accent-text: color-mix(in srgb, var(--dmc-accent) 55%, #fff);--dmc-gap: 8px;--dmc-base: #1b1d21;--dmc-panel: #25282d;--dmc-raised: #2f3338;--dmc-line: #363a40;--dmc-on-dark: #e8eaed;--dmc-muted-dark: #9aa0a8;--dmc-red: #d9453f;--dmc-red-hover: #c23a35;--dmc-page: #f3f4f6;--dmc-card: #fff;--dmc-ink: #1f2328;--dmc-ink-muted: #5b6470;--dmc-border: #dde0e4;--dmc-offset: 0px;--dmc-left: 0px;--dmc-right: 0px;display:flex;flex-direction:column;gap:1rem;color:var(--dmc-ink)}.dmc [hidden]{display:none!important}html.dmc-page,html.dmc-page body{overflow:hidden!important}.dmc.dmc--app{position:fixed;z-index:40;top:var(--dmc-offset);left:var(--dmc-left);right:var(--dmc-right);bottom:0;margin:0;background:var(--dmc-page);overflow-y:auto;overscroll-behavior:contain;padding:2.5rem 1rem;align-items:center;justify-content:safe center;gap:1rem}.dmc.dmc--app[data-stage=in]{padding:0;background:var(--dmc-base);overflow:hidden}.dmc.dmc--app>*{width:min(60rem,100%);flex:none}.dmc.dmc--app>.dmc-call{width:100%;flex:1 1 auto}.dmc-head{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}.dmc.dmc--app:not([data-stage=check])>.dmc-head{width:min(28rem,100%);justify-content:center;text-align:center}.dmc-title{font-size:1.35rem;font-weight:600;margin:0;flex:1 1 auto;letter-spacing:-.01em;color:var(--dmc-ink)}.dmc-btn{font:inherit;font-size:.9rem;cursor:pointer;padding:.5rem .95rem;min-height:2.5rem;border-radius:8px;border:1px solid var(--dmc-border);background:var(--dmc-card);color:inherit;display:inline-flex;align-items:center;justify-content:center;gap:.45rem;position:relative;font-weight:500;transition:background-color .15s,border-color .15s,color .15s}.dmc-btn:hover{border-color:#b9bec5;background:#f7f8f9}.dmc-btn:disabled{opacity:.55;cursor:default}.dmc-btn:focus-visible,.dmc-compose:focus-visible,.dmc-input:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:2px}.dmc-i{display:inline-flex}.dmc-btn svg,.dmc-count svg,.dmc-note svg,.dmc-chip svg{width:18px;height:18px;flex:none}.dmc-btn.is-off{background:#fdecec;border-color:#f3c4c2;color:#b3261e}.dmc-btn--go{background:var(--dmc-accent-solid);border-color:transparent;color:#fff;font-weight:600}.dmc-btn--go:hover{background:color-mix(in srgb,var(--dmc-accent) 74%,#000);border-color:transparent}.dmc-btn--small{padding:.3rem .65rem;min-height:2rem;font-size:.82rem}.dmc-panel{display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:1.75rem;align-items:center;background:var(--dmc-card);border:1px solid var(--dmc-border);border-radius:12px;padding:1.5rem;box-shadow:0 1px 2px #1018280d,0 8px 24px #1018280f}.dmc-panel--msg{display:flex;flex-direction:column;align-items:center;text-align:center;padding:2.5rem 2rem;gap:.25rem;width:min(28rem,100%)!important;align-self:center}.dmc-panel--msg h2{margin:.75rem 0 .25rem;font-size:1.2rem;font-weight:600;color:var(--dmc-ink)}.dmc-panel--msg p{color:var(--dmc-ink-muted);margin:0 0 1.25rem;font-size:.92rem;line-height:1.5}.dmc-pulse{width:64px;height:64px;border-radius:50%;display:grid;place-items:center;color:var(--dmc-accent-solid);background:var(--dmc-accent-soft);box-shadow:0 0 color-mix(in srgb,var(--dmc-accent) 35%,transparent);animation:dmc-pulse 2.2s infinite}.dmc-pulse svg{width:26px;height:26px}@keyframes dmc-pulse{70%{box-shadow:0 0 0 16px transparent}to{box-shadow:0 0 0 0 transparent}}.dmc-preview{position:relative;aspect-ratio:16 / 9;background:var(--dmc-base);border-radius:10px;overflow:hidden}.dmc-preview video,.dmc-tile video{width:100%;height:100%;object-fit:cover;display:block}.dmc-self,.dmc-self-preview{transform:scaleX(-1)}.dmc-nocam{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;color:var(--dmc-muted-dark);margin:0;font-size:.9rem}.dmc-avatar-dot{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;background:var(--dmc-raised);color:var(--dmc-on-dark);font-size:1.8rem;font-weight:600}.dmc-preview-tools{position:absolute;left:0;right:0;bottom:.75rem;display:flex;justify-content:center;gap:.5rem}.dmc-round{width:44px;height:44px;min-height:0;padding:0;border-radius:10px;border:1px solid rgba(255,255,255,.14);background:#1b1d21b8;color:#fff;backdrop-filter:blur(6px)}.dmc-round:hover{background:#2f3338e6;border-color:#ffffff4d}.dmc-round.is-off{background:var(--dmc-red);border-color:transparent;color:#fff}.dmc-check h2{margin:0 0 .35rem;font-size:1.25rem;font-weight:600;color:var(--dmc-ink)}.dmc-label{display:grid;gap:.35rem;margin:1rem 0 .25rem;font-weight:500;font-size:.88rem;color:var(--dmc-ink)}.dmc-input{font:inherit;font-size:.95rem;padding:.55rem .75rem;min-height:2.5rem;border:1px solid var(--dmc-border);border-radius:8px;background:#fff;color:var(--dmc-ink);min-width:0}.dmc-label .dmc-input:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-row{display:flex;gap:.5rem;flex-wrap:wrap;margin:.9rem 0}.dmc-chipbtn{font-size:.86rem}.dmc-note{color:var(--dmc-ink-muted);font-size:.9rem;line-height:1.45;margin:0}.dmc-note--rec{display:flex;gap:.5rem;align-items:flex-start;font-size:.85rem;margin:.25rem 0 0}.dmc-note--rec svg{color:var(--dmc-red);fill:currentColor;width:10px;height:10px;margin-top:.35rem}.dmc-error{color:#b3261e;font-weight:500;font-size:.9rem;margin:.5rem 0 0}.dmc-check .dmc-btn--go{width:100%;min-height:2.75rem;margin-top:1rem;font-size:.95rem}.dmc-call{display:flex;color:var(--dmc-on-dark);background:var(--dmc-base);overflow:hidden;color-scheme:dark;min-height:0;font-size:14px}.dmc-stage{position:relative;flex:1 1 auto;min-width:0;display:flex;flex-direction:column}.dmc-top{order:-1;flex:none;display:flex;align-items:center;justify-content:space-between;gap:1rem;height:44px;padding:0 1rem;background:var(--dmc-panel);border-bottom:1px solid var(--dmc-line)}.dmc-top-l{display:flex;align-items:center;gap:.75rem;min-width:0}.dmc-top .dmc-top-title{margin:0;font-size:.92rem;line-height:1.3;font-weight:600;letter-spacing:0;color:var(--dmc-on-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-timer{flex:none;font-size:.82rem;color:var(--dmc-muted-dark);font-variant-numeric:tabular-nums;padding-left:.75rem;border-left:1px solid var(--dmc-line)}.dmc-top-r{display:flex;align-items:center;gap:.6rem;flex:none}.dmc-rec{display:inline-flex;align-items:center;gap:.4rem;color:#ff8a85;background:#d9453f29;border:1px solid rgba(217,69,63,.45);border-radius:6px;padding:.12rem .5rem;font-size:.74rem;font-weight:600;letter-spacing:.02em}.dmc-rec-dot{width:7px;height:7px;border-radius:50%;background:#ff5a52;animation:dmc-blink 1.4s infinite}@keyframes dmc-blink{50%{opacity:.3}}.dmc-count{display:inline-flex;align-items:center;gap:.35rem;font-size:.82rem;color:var(--dmc-muted-dark);font-variant-numeric:tabular-nums}.dmc-count svg{width:16px;height:16px}.dmc-main{position:relative;flex:1 1 auto;min-height:0}.dmc-remote{position:absolute;inset:var(--dmc-gap);display:flex;flex-wrap:wrap;align-content:center;justify-content:center;gap:var(--dmc-gap);--cols: 1;--rows: 1}.dmc-cell{min-width:0;min-height:0;display:flex;width:var(--tile-w, calc((100% - (var(--cols) - 1) * var(--dmc-gap)) / var(--cols)));height:var(--tile-h, calc((100% - (var(--rows) - 1) * var(--dmc-gap)) / var(--rows)))}.dmc-main[data-tiles="2"] .dmc-remote{--cols: 2}.dmc-main[data-tiles="3"] .dmc-remote,.dmc-main[data-tiles="4"] .dmc-remote{--cols: 2;--rows: 2}.dmc-main[data-tiles="5"] .dmc-remote,.dmc-main[data-tiles="6"] .dmc-remote{--cols: 3;--rows: 2}.dmc-tile{position:relative;margin:0;background:var(--dmc-panel);border-radius:8px;overflow:hidden}.dmc-cell .dmc-tile{flex:1 1 auto;width:100%;height:100%}.dmc-tile video{position:relative;z-index:1}.dmc-avatar{position:absolute;inset:0;display:grid;place-items:center;z-index:0}.dmc-avatar span{width:clamp(3rem,18%,6rem);aspect-ratio:1;border-radius:50%;display:grid;place-items:center;background:var(--dmc-raised);color:var(--dmc-on-dark);font-size:clamp(1.1rem,2.4vw,2.2rem);font-weight:600}.dmc-tile--me .dmc-avatar{z-index:2;background:var(--dmc-panel)}.dmc-chip{position:absolute;left:.5rem;bottom:.5rem;z-index:3;display:inline-flex;align-items:center;gap:.3rem;max-width:calc(100% - 1rem);padding:.18rem .5rem;border-radius:6px;background:#1b1d21c7;color:var(--dmc-on-dark);font-size:.78rem;font-weight:500;line-height:1.35}.dmc-chip>span:last-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-chip-mute{display:inline-flex;color:#ff8a85}.dmc-chip-mute svg{width:13px;height:13px}.dmc-tile--me{position:absolute;z-index:2;right:calc(var(--dmc-gap) * 2);bottom:calc(var(--dmc-gap) * 2);width:clamp(10rem,17%,15rem);aspect-ratio:16 / 9;box-shadow:0 6px 20px #00000073;outline:1px solid rgba(255,255,255,.14)}.dmc-tile--me .dmc-chip{left:.35rem;bottom:.35rem;font-size:.7rem;padding:.1rem .4rem}.dmc-main[data-tiles="0"] .dmc-tile--me{inset:0;width:auto;aspect-ratio:auto;border-radius:0;outline:0;box-shadow:none}.dmc-main[data-tiles="0"] .dmc-tile--me .dmc-chip{left:.75rem;bottom:.75rem;font-size:.78rem;padding:.18rem .5rem}.dmc-main[data-tiles="0"] .dmc-tile--me:after{content:"";position:absolute;inset:0;z-index:2;background:#1b1d218c;pointer-events:none}.dmc-alone{position:absolute;inset:0;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:1rem;pointer-events:none}.dmc-alone-title{margin:0;font-size:1.1rem;font-weight:600;color:#fff}.dmc-alone-sub{margin:.35rem 0 0;color:#c9cdd2;font-size:.88rem}.dmc-waiting{position:absolute;z-index:4;top:calc(44px + .75rem);right:.75rem;width:min(20rem,calc(100% - 1.5rem));padding:.85rem;border-radius:10px;background:var(--dmc-raised);box-shadow:0 12px 32px #00000073;border:1px solid #40454c;animation:dmc-drop .2s ease}@keyframes dmc-drop{0%{opacity:0;transform:translateY(-4px)}}.dmc-waiting h3{display:flex;align-items:center;gap:.5rem;margin:0 0 .7rem;font-size:.85rem;font-weight:600;color:var(--dmc-on-dark)}.dmc-waiting-dot{width:8px;height:8px;border-radius:50%;background:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-waiting ul{list-style:none;margin:0;padding:0;display:grid;gap:.7rem;max-height:14rem;overflow-y:auto}.dmc-waiting li{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:.4rem .6rem}.dmc-winitial{width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:#454a52;color:#fff;font-weight:600;font-size:.85rem}.dmc-wname{font-weight:500;color:var(--dmc-on-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-wbtns{grid-column:2;display:flex;gap:.4rem}.dmc-waiting .dmc-btn svg{width:15px;height:15px}.dmc-waiting .dmc-btn--go{background:var(--dmc-accent-solid);color:#fff}.dmc-btn--ghost{background:transparent;color:var(--dmc-on-dark);border-color:#4a4f57}.dmc-btn--ghost:hover{background:#ffffff0f;border-color:#626872}.dmc-bar{flex:none;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:.5rem;padding:.5rem 1rem;background:var(--dmc-panel);border-top:1px solid var(--dmc-line)}.dmc-bar-mid{display:flex;align-items:center;gap:.35rem}.dmc-bar-end{display:flex;justify-content:flex-end}.dmc-tool{flex-direction:column;gap:3px;min-width:4.25rem;height:3.5rem;min-height:0;padding:.3rem .6rem;border-radius:10px;border:1px solid transparent;background:transparent;color:var(--dmc-on-dark);flex:none;font-weight:400}.dmc-tool:hover{background:var(--dmc-raised);border-color:transparent}.dmc-tool svg{width:20px;height:20px}.dmc-lbl{font-size:.72rem;line-height:1;color:var(--dmc-muted-dark);white-space:nowrap}.dmc-tool:hover .dmc-lbl{color:var(--dmc-on-dark)}.dmc-tool.is-off{background:transparent;color:#ff8a85}.dmc-tool.is-off:hover{background:#d9453f24}.dmc-tool.is-off .dmc-lbl{color:#ff8a85}.dmc-tool.is-on{background:var(--dmc-accent-soft);color:var(--dmc-accent-text)}.dmc-tool.is-on .dmc-lbl{color:var(--dmc-accent-text)}.dmc-btn--rec svg{color:var(--dmc-red);fill:currentColor;width:16px;height:16px;margin:2px 0}.dmc-btn--rec.is-on{background:#d9453f2e;color:#ff8a85}.dmc-btn--rec.is-on .dmc-lbl{color:#ff8a85}.dmc-btn--rec.is-on svg{color:#ff5a52;animation:dmc-blink 1.4s infinite}.dmc-btn--leave{background:var(--dmc-red);color:#fff;flex-direction:row;gap:.45rem;height:2.75rem;min-width:0;padding:0 1.1rem;border-radius:10px;font-weight:600}.dmc-btn--leave:hover{background:var(--dmc-red-hover)}.dmc-btn--leave .dmc-lbl,.dmc-btn--leave:hover .dmc-lbl{color:#fff;font-size:.85rem}.dmc-btn--leave svg{width:18px;height:18px}.dmc-badge{position:absolute;top:2px;right:10px;min-width:1.1rem;height:1.1rem;padding:0 .28rem;border-radius:999px;background:var(--dmc-accent-solid);color:#fff;font-size:.66rem;font-weight:600;line-height:1.1rem;text-align:center;box-shadow:0 0 0 2px var(--dmc-panel)}.dmc-chat{flex:none;width:0;display:flex;flex-direction:column;background:var(--dmc-panel);color:var(--dmc-on-dark);overflow:hidden;transition:width .2s ease;border-left:1px solid var(--dmc-line);font-size:14px}.dmc-call:not(.is-chat) .dmc-chat{border-left-width:0}.dmc-call.is-chat .dmc-chat{width:20rem}.dmc-chat>*{min-width:20rem}.dmc-chat-head{flex:none;display:flex;align-items:center;gap:.5rem;height:44px;padding:0 .5rem 0 1rem;border-bottom:1px solid var(--dmc-line)}.dmc-chat-titles{flex:1;min-width:0;display:flex;align-items:baseline;gap:.6rem}.dmc-chat-titles h3{margin:0;font-size:.92rem;font-weight:600;color:var(--dmc-on-dark)}.dmc-chat-titles span{font-size:.75rem;color:var(--dmc-muted-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-chat-back,.dmc-chat-close{width:2rem;height:2rem;border:0;border-radius:8px;background:transparent;color:var(--dmc-muted-dark);cursor:pointer;display:grid;place-items:center}.dmc-chat-back:hover,.dmc-chat-close:hover{background:var(--dmc-raised);color:var(--dmc-on-dark)}.dmc-chat-back:focus-visible,.dmc-chat-close:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:1px}.dmc-chat-back svg,.dmc-chat-close svg{width:18px;height:18px}.dmc-chat-back{display:none}.dmc-chat-body{position:relative;flex:1;min-height:0;display:flex}.dmc-messages{list-style:none;margin:0;padding:.5rem 1rem 1rem;flex:1;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#43484f transparent}.dmc-chat-empty{position:absolute;inset:auto 1.5rem 45%;margin:0;text-align:center;color:var(--dmc-muted-dark);font-size:.85rem;line-height:1.5}.dmc-msg{display:flex;flex-direction:column;margin-top:3px}.dmc-msg.is-first{margin-top:.85rem}.dmc-msg.is-mine{align-items:flex-end}.dmc-msg.is-other{align-items:flex-start}.dmc-msg.is-sys{align-items:center;margin-top:.6rem}.dmc-msg.is-sys+.dmc-msg.is-sys:not(.has-day){margin-top:.2rem}.dmc-msg.has-day.is-first{margin-top:.35rem}.dmc-day{align-self:stretch;display:flex;align-items:center;gap:.75rem;margin:.5rem 0;color:var(--dmc-muted-dark);font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.dmc-day:before,.dmc-day:after{content:"";flex:1;height:1px;background:var(--dmc-line)}.dmc-sys{color:#80868e;font-size:.76rem;line-height:1.4;text-align:center}.dmc-meta{display:flex;align-items:baseline;gap:.5rem;margin:0 .15rem .2rem;max-width:88%}.dmc-from{font-size:.8rem;font-weight:500;color:#8fb0ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-time{font-size:.7rem;color:#80868e;font-variant-numeric:tabular-nums;flex:none}.dmc-bubble{max-width:88%;padding:.45rem .7rem;border-radius:8px;background:var(--dmc-raised);color:var(--dmc-on-dark);line-height:1.42;overflow-wrap:anywhere}.is-mine .dmc-bubble{background:color-mix(in srgb,var(--dmc-accent) 20%,var(--dmc-panel));color:#f1f4ff}.dmc-text{white-space:pre-wrap}.dmc-msg[data-hue="1"] .dmc-from{color:#6fc2ad}.dmc-msg[data-hue="2"] .dmc-from{color:#d6ad72}.dmc-msg[data-hue="3"] .dmc-from{color:#bf9fdc}.dmc-msg[data-hue="4"] .dmc-from{color:#e0948e}.dmc-msg[data-hue="5"] .dmc-from{color:#9fcb8d}.dmc-msg[data-hue="6"] .dmc-from{color:#7fbad6}.dmc-msg[data-hue="7"] .dmc-from{color:#d49fba}.dmc-newchip{position:absolute;left:50%;bottom:.75rem;transform:translate(-50%);display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;border:1px solid #40454c;border-radius:8px;padding:.35rem .7rem;background:var(--dmc-raised);color:var(--dmc-on-dark);font:inherit;font-size:.78rem;box-shadow:0 4px 14px #0006}.dmc-newchip svg{width:14px;height:14px;color:var(--dmc-accent-text)}.dmc-send{flex:none;display:flex;align-items:flex-end;gap:.5rem;padding:.75rem 1rem;border-top:1px solid var(--dmc-line)}.dmc-compose{flex:1;resize:none;border:1px solid #40454c;border-radius:8px;padding:.5rem .7rem;min-height:0;background:var(--dmc-base);color:var(--dmc-on-dark);line-height:1.35rem;font-size:14px;overflow-y:hidden;max-height:calc(6.4rem + 2px);display:block}.dmc-compose::placeholder{color:#80868e}.dmc-compose:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-sendbtn{width:2.4rem;height:2.4rem;min-height:0;padding:0;border:0;border-radius:8px;background:var(--dmc-accent-solid);color:#fff;flex:none}.dmc-sendbtn:hover{background:color-mix(in srgb,var(--dmc-accent) 74%,#000)}.dmc-sendbtn:disabled{opacity:1;background:var(--dmc-raised);color:#80868e}.dmc-sendbtn svg{width:17px;height:17px;margin-left:-2px}.dmc-invite{flex:none;width:0;display:flex;flex-direction:column;background:var(--dmc-panel);color:var(--dmc-on-dark);overflow:hidden;transition:width .2s ease;border-left:0 solid var(--dmc-line);font-size:14px}.dmc-invite.is-open{width:22rem;border-left-width:1px}.dmc-invite>*{min-width:22rem}.dmc .dmc-invite ul{margin:0;padding:0;list-style:none}.dmc .dmc-invite li{margin:0}.dmc-inv-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:.85rem 1rem 1rem;scrollbar-width:thin;scrollbar-color:#43484f transparent}.dmc-inv-input{font:inherit;font-size:14px;width:100%;min-width:0;min-height:2.4rem;padding:.45rem .7rem;border:1px solid #40454c;border-radius:8px;background:var(--dmc-base);color:var(--dmc-on-dark)}.dmc-inv-input::placeholder{color:#80868e}.dmc-inv-input:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-inv-search{position:relative;display:block}.dmc-inv-search .dmc-inv-input{padding-left:2.2rem}.dmc-inv-sico{position:absolute;left:.7rem;top:50%;transform:translateY(-50%);display:inline-flex;color:#80868e;pointer-events:none}.dmc-inv-sico svg{width:16px;height:16px}.dmc-inv-label{margin:1rem 0 .4rem;font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dmc-muted-dark)}.dmc-inv-chips{margin-top:.75rem}.dmc-inv-chips ul{display:flex;flex-wrap:wrap;gap:.35rem}.dmc-inv-chip{display:inline-flex;align-items:center;gap:.35rem;max-width:100%;padding:.2rem .25rem .2rem .6rem;border-radius:6px;background:var(--dmc-accent-soft);color:var(--dmc-accent-text);font-size:.8rem}.dmc-inv-chipname{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#f1f4ff;font-weight:500}.dmc-inv-chipkind{font-size:.68rem;opacity:.85}.dmc-inv-chipx{display:grid;place-items:center;width:1.4rem;height:1.4rem;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer;padding:0}.dmc-inv-chipx:hover{background:#ffffff1a}.dmc-inv-chipx:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:1px}.dmc-inv-chipx svg{width:13px;height:13px}.dmc-inv-list{list-style:none;margin:0;padding:0;display:grid;gap:2px}.dmc-inv-row{width:100%;display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:.6rem;padding:.45rem .5rem;border:1px solid transparent;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dmc-inv-row:hover{background:var(--dmc-raised)}.dmc-inv-row:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:-2px}.dmc-inv-row.is-picked{background:var(--dmc-accent-soft)}.dmc-inv-av{width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:#454a52;color:#fff;font-weight:600;font-size:.82rem}.dmc-inv-who{min-width:0;display:flex;flex-direction:column}.dmc-inv-name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-inv-mail{font-size:.75rem;color:var(--dmc-muted-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-inv-kind{font-size:.68rem;font-weight:600;padding:.1rem .4rem;border-radius:4px;border:1px solid #4a4f57;color:var(--dmc-muted-dark)}.dmc-inv-kind[data-kind=user]{border-color:transparent;background:#3a3f46;color:#c9cdd2}.dmc-inv-tick{width:1.25rem;height:1.25rem;border-radius:50%;display:grid;place-items:center;border:1.5px solid #5a6069;color:transparent}.dmc-inv-tick svg{width:12px;height:12px}.dmc-inv-row.is-picked .dmc-inv-tick{background:var(--dmc-accent-solid);border-color:transparent;color:#fff}.dmc-inv-only{margin:.25rem 0 0;color:var(--dmc-on-dark);font-size:.88rem;line-height:1.5}.dmc-inv-empty{margin:.5rem .25rem;color:var(--dmc-muted-dark);font-size:.85rem;line-height:1.45}.dmc-inv-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) auto;gap:.4rem}.dmc-inv-add{width:2.4rem;min-height:2.4rem;padding:0;background:var(--dmc-raised);border-color:#40454c;color:var(--dmc-on-dark)}.dmc-inv-add:hover{background:#3a3f46;border-color:#5a6069}.dmc-inv-add svg{width:17px;height:17px}.dmc-inv-foot{flex:none;border-top:1px solid var(--dmc-line);padding:.75rem 1rem calc(.85rem + env(safe-area-inset-bottom,0px));display:grid;gap:.55rem}.dmc-inv-via{border:0;margin:0;padding:0;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:.35rem 1rem}.dmc-inv-via legend{float:left;width:100%;margin:0 0 .1rem;padding:0;font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dmc-muted-dark)}.dmc-inv-via label{display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.86rem}.dmc-inv-via input{width:16px;height:16px;margin:0;accent-color:var(--dmc-accent)}.dmc-inv-hint{margin:0;font-size:.75rem;line-height:1.45;color:#80868e}.dmc-inv-status{margin:0;font-size:.82rem;line-height:1.4;color:#9fcb8d}.dmc-inv-status.is-err{color:#ff8a85}.dmc-inv-results{list-style:none;margin:0;padding:0;display:grid;gap:.25rem;max-height:7.5rem;overflow-y:auto;font-size:.8rem}.dmc-inv-res{display:flex;gap:.4rem;line-height:1.35;color:var(--dmc-muted-dark)}.dmc-inv-res:before{content:"";flex:none;width:6px;height:6px;margin-top:.4em;border-radius:50%;background:#6fb56a}.dmc-inv-res.is-err:before{background:#e0605a}.dmc-inv-rname{color:var(--dmc-on-dark);font-weight:500;flex:none;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dmc-inv-link{font-size:.78rem;color:var(--dmc-muted-dark)}.dmc-inv-btns{display:flex;gap:.5rem}.dmc-inv-btns .dmc-btn{flex:1;min-height:2.5rem}.dmc-inv-copy svg,.dmc-inv-send svg{width:16px;height:16px}.dmc-inv-send:disabled{opacity:1;background:var(--dmc-raised);color:#80868e}.dmc-tool--invite svg{width:20px;height:20px}@media(max-width:760px){.dmc.dmc--app{padding:1rem .75rem}.dmc-panel{grid-template-columns:1fr;gap:1.1rem;padding:1rem}.dmc-panel--msg{padding:2rem 1.25rem}.dmc-title{font-size:1.15rem}.dmc-top{padding:0 .75rem}.dmc-waiting{left:.5rem;right:.5rem;top:calc(44px + .5rem);width:auto}.dmc-bar{display:flex;justify-content:space-around;gap:.25rem;padding:.45rem .4rem calc(.45rem + env(safe-area-inset-bottom,0px))}.dmc-bar-mid{display:contents}.dmc-tool{width:44px;height:44px;min-width:0;padding:0;background:var(--dmc-raised)}.dmc-tool.is-off{background:#d9453f33}.dmc-lbl{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%)}.dmc-bar-side:empty{display:none}.dmc-bar-end{display:contents}.dmc-btn--leave{width:52px;padding:0;background:var(--dmc-red)}.dmc-badge{top:-4px;right:-4px}.dmc-tile--me{right:.5rem;bottom:.5rem}.dmc.dmc--app:has(.dmc-call.is-chat){z-index:1000}.dmc-chat{position:fixed;inset:0;z-index:1000;width:auto;border:0;transform:translateY(100%);transition:transform .22s ease,visibility 0s .22s;visibility:hidden}.dmc-call.is-chat .dmc-chat{width:auto;transform:none;visibility:visible;transition:transform .22s ease}.dmc-chat>*{min-width:0}.dmc-chat-back{display:grid}.dmc-chat-close{display:none}.dmc-chat-head{padding:0 .75rem 0 .35rem}.dmc-send{padding-bottom:calc(.75rem + env(safe-area-inset-bottom,0px))}.dmc.dmc--app:has(.dmc-invite.is-open){z-index:1000}.dmc-invite{position:fixed;inset:0;z-index:1000;width:auto;border:0;transform:translateY(100%);transition:transform .22s ease,visibility 0s .22s;visibility:hidden}.dmc-invite.is-open{width:auto;border:0;transform:none;visibility:visible;transition:transform .22s ease}.dmc-invite>*{min-width:0}}@media(max-width:760px)and (orientation:portrait){.dmc-tile--me{width:clamp(6rem,26%,8.5rem);aspect-ratio:3 / 4}}@media(prefers-reduced-motion:reduce){.dmc *,.dmc *:after{animation:none!important;transition:none!important}}
1
+ .dmc{--dmc-accent: #4f7cff;--dmc-accent-solid: color-mix(in srgb, var(--dmc-accent) 84%, #000);--dmc-accent-soft: color-mix(in srgb, var(--dmc-accent) 18%, transparent);--dmc-accent-text: color-mix(in srgb, var(--dmc-accent) 55%, #fff);--dmc-gap: 8px;--dmc-base: #1b1d21;--dmc-panel: #25282d;--dmc-raised: #2f3338;--dmc-line: #363a40;--dmc-on-dark: #e8eaed;--dmc-muted-dark: #9aa0a8;--dmc-red: #d9453f;--dmc-red-hover: #c23a35;--dmc-page: #f3f4f6;--dmc-card: #fff;--dmc-ink: #1f2328;--dmc-ink-muted: #5b6470;--dmc-border: #dde0e4;--dmc-offset: 0px;--dmc-left: 0px;--dmc-right: 0px;display:flex;flex-direction:column;gap:1rem;color:var(--dmc-ink)}.dmc [hidden]{display:none!important}html.dmc-page,html.dmc-page body{overflow:hidden!important}.dmc.dmc--app{position:fixed;z-index:40;top:var(--dmc-offset);left:var(--dmc-left);right:var(--dmc-right);bottom:0;margin:0;background:var(--dmc-page);overflow-y:auto;overscroll-behavior:contain;padding:2.5rem 1rem;align-items:center;justify-content:safe center;gap:1rem}.dmc.dmc--app[data-stage=in]{padding:0;background:var(--dmc-base);overflow:hidden}.dmc.dmc--app>*{width:min(60rem,100%);flex:none}.dmc.dmc--app>.dmc-call{width:100%;flex:1 1 auto}.dmc-head{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}.dmc.dmc--app:not([data-stage=check])>.dmc-head{width:min(28rem,100%);justify-content:center;text-align:center}.dmc-title{font-size:1.35rem;font-weight:600;margin:0;flex:1 1 auto;letter-spacing:-.01em;color:var(--dmc-ink)}.dmc-btn{font:inherit;font-size:.9rem;cursor:pointer;padding:.5rem .95rem;min-height:2.5rem;border-radius:8px;border:1px solid var(--dmc-border);background:var(--dmc-card);color:inherit;display:inline-flex;align-items:center;justify-content:center;gap:.45rem;position:relative;font-weight:500;transition:background-color .15s,border-color .15s,color .15s}.dmc-btn:hover{border-color:#b9bec5;background:#f7f8f9}.dmc-btn:disabled{opacity:.55;cursor:default}.dmc-btn:focus-visible,.dmc-compose:focus-visible,.dmc-input:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:2px}.dmc-i{display:inline-flex}.dmc-btn svg,.dmc-count svg,.dmc-note svg,.dmc-chip svg{width:18px;height:18px;flex:none}.dmc-btn.is-off{background:#fdecec;border-color:#f3c4c2;color:#b3261e}.dmc-btn--go{background:var(--dmc-accent-solid);border-color:transparent;color:#fff;font-weight:600}.dmc-btn--go:hover{background:color-mix(in srgb,var(--dmc-accent) 74%,#000);border-color:transparent}.dmc-btn--small{padding:.3rem .65rem;min-height:2rem;font-size:.82rem}.dmc-panel{display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:1.75rem;align-items:center;background:var(--dmc-card);border:1px solid var(--dmc-border);border-radius:12px;padding:1.5rem;box-shadow:0 1px 2px #1018280d,0 8px 24px #1018280f}.dmc-panel--msg{display:flex;flex-direction:column;align-items:center;text-align:center;padding:2.5rem 2rem;gap:.25rem;width:min(28rem,100%)!important;align-self:center}.dmc-panel--msg h2{margin:.75rem 0 .25rem;font-size:1.2rem;font-weight:600;color:var(--dmc-ink)}.dmc-panel--msg p{color:var(--dmc-ink-muted);margin:0 0 1.25rem;font-size:.92rem;line-height:1.5}.dmc-pulse{width:64px;height:64px;border-radius:50%;display:grid;place-items:center;color:var(--dmc-accent-solid);background:var(--dmc-accent-soft);box-shadow:0 0 color-mix(in srgb,var(--dmc-accent) 35%,transparent);animation:dmc-pulse 2.2s infinite}.dmc-pulse svg{width:26px;height:26px}@keyframes dmc-pulse{70%{box-shadow:0 0 0 16px transparent}to{box-shadow:0 0 0 0 transparent}}.dmc-preview{position:relative;aspect-ratio:16 / 9;background:var(--dmc-base);border-radius:10px;overflow:hidden}.dmc-preview video,.dmc-tile video{width:100%;height:100%;object-fit:cover;display:block}.dmc-self,.dmc-self-preview{transform:scaleX(-1)}.dmc-nocam{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;color:var(--dmc-muted-dark);margin:0;font-size:.9rem}.dmc-avatar-dot{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;background:var(--dmc-raised);color:var(--dmc-on-dark);font-size:1.8rem;font-weight:600}.dmc-preview-tools{position:absolute;left:0;right:0;bottom:.75rem;display:flex;justify-content:center;gap:.5rem}.dmc-round{width:44px;height:44px;min-height:0;padding:0;border-radius:10px;border:1px solid rgba(255,255,255,.14);background:#1b1d21b8;color:#fff;backdrop-filter:blur(6px)}.dmc-round:hover{background:#2f3338e6;border-color:#ffffff4d}.dmc-round.is-off{background:var(--dmc-red);border-color:transparent;color:#fff}.dmc-check h2{margin:0 0 .35rem;font-size:1.25rem;font-weight:600;color:var(--dmc-ink)}.dmc-label{display:grid;gap:.35rem;margin:1rem 0 .25rem;font-weight:500;font-size:.88rem;color:var(--dmc-ink)}.dmc-input{font:inherit;font-size:.95rem;padding:.55rem .75rem;min-height:2.5rem;border:1px solid var(--dmc-border);border-radius:8px;background:#fff;color:var(--dmc-ink);min-width:0}.dmc-label .dmc-input:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-row{display:flex;gap:.5rem;flex-wrap:wrap;margin:.9rem 0}.dmc-chipbtn{font-size:.86rem}.dmc-round.is-on{background:var(--dmc-accent-solid);border-color:transparent;color:#fff}.dmc-chipbtn.is-on{background:var(--dmc-accent-soft);border-color:var(--dmc-accent);color:inherit}.dmc-note{color:var(--dmc-ink-muted);font-size:.9rem;line-height:1.45;margin:0}.dmc-note--rec{display:flex;gap:.5rem;align-items:flex-start;font-size:.85rem;margin:.25rem 0 0}.dmc-note--rec svg{color:var(--dmc-red);fill:currentColor;width:10px;height:10px;margin-top:.35rem}.dmc-error{color:#b3261e;font-weight:500;font-size:.9rem;margin:.5rem 0 0}.dmc-check .dmc-btn--go{width:100%;min-height:2.75rem;margin-top:1rem;font-size:.95rem}.dmc-call{display:flex;color:var(--dmc-on-dark);background:var(--dmc-base);overflow:hidden;color-scheme:dark;min-height:0;font-size:14px}.dmc-stage{position:relative;flex:1 1 auto;min-width:0;display:flex;flex-direction:column}.dmc-top{order:-1;flex:none;display:flex;align-items:center;justify-content:space-between;gap:1rem;height:44px;padding:0 1rem;background:var(--dmc-panel);border-bottom:1px solid var(--dmc-line)}.dmc-top-l{display:flex;align-items:center;gap:.75rem;min-width:0}.dmc-top .dmc-top-title{margin:0;font-size:.92rem;line-height:1.3;font-weight:600;letter-spacing:0;color:var(--dmc-on-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-timer{flex:none;font-size:.82rem;color:var(--dmc-muted-dark);font-variant-numeric:tabular-nums;padding-left:.75rem;border-left:1px solid var(--dmc-line)}.dmc-top-r{display:flex;align-items:center;gap:.6rem;flex:none}.dmc-rec{display:inline-flex;align-items:center;gap:.4rem;color:#ff8a85;background:#d9453f29;border:1px solid rgba(217,69,63,.45);border-radius:6px;padding:.12rem .5rem;font-size:.74rem;font-weight:600;letter-spacing:.02em}.dmc-rec-dot{width:7px;height:7px;border-radius:50%;background:#ff5a52;animation:dmc-blink 1.4s infinite}@keyframes dmc-blink{50%{opacity:.3}}.dmc-count{display:inline-flex;align-items:center;gap:.35rem;font-size:.82rem;color:var(--dmc-muted-dark);font-variant-numeric:tabular-nums}.dmc-count svg{width:16px;height:16px}.dmc-main{position:relative;flex:1 1 auto;min-height:0}.dmc-remote{position:absolute;inset:var(--dmc-gap);display:flex;flex-wrap:wrap;align-content:center;justify-content:center;gap:var(--dmc-gap);--cols: 1;--rows: 1}.dmc-cell{min-width:0;min-height:0;display:flex;width:var(--tile-w, calc((100% - (var(--cols) - 1) * var(--dmc-gap)) / var(--cols)));height:var(--tile-h, calc((100% - (var(--rows) - 1) * var(--dmc-gap)) / var(--rows)))}.dmc-main[data-tiles="2"] .dmc-remote{--cols: 2}.dmc-main[data-tiles="3"] .dmc-remote,.dmc-main[data-tiles="4"] .dmc-remote{--cols: 2;--rows: 2}.dmc-main[data-tiles="5"] .dmc-remote,.dmc-main[data-tiles="6"] .dmc-remote{--cols: 3;--rows: 2}.dmc-tile{position:relative;margin:0;background:var(--dmc-panel);border-radius:8px;overflow:hidden}.dmc-cell .dmc-tile{flex:1 1 auto;width:100%;height:100%}.dmc-tile video{position:relative;z-index:1}.dmc-avatar{position:absolute;inset:0;display:grid;place-items:center;z-index:0}.dmc-avatar span{width:clamp(3rem,18%,6rem);aspect-ratio:1;border-radius:50%;display:grid;place-items:center;background:var(--dmc-raised);color:var(--dmc-on-dark);font-size:clamp(1.1rem,2.4vw,2.2rem);font-weight:600}.dmc-tile--me .dmc-avatar{z-index:2;background:var(--dmc-panel)}.dmc-chip{position:absolute;left:.5rem;bottom:.5rem;z-index:3;display:inline-flex;align-items:center;gap:.3rem;max-width:calc(100% - 1rem);padding:.18rem .5rem;border-radius:6px;background:#1b1d21c7;color:var(--dmc-on-dark);font-size:.78rem;font-weight:500;line-height:1.35}.dmc-chip>span:last-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-chip-mute{display:inline-flex;color:#ff8a85}.dmc-chip-mute svg{width:13px;height:13px}.dmc-tile--me{position:absolute;z-index:2;right:calc(var(--dmc-gap) * 2);bottom:calc(var(--dmc-gap) * 2);width:clamp(10rem,17%,15rem);aspect-ratio:16 / 9;box-shadow:0 6px 20px #00000073;outline:1px solid rgba(255,255,255,.14)}.dmc-tile--me .dmc-chip{left:.35rem;bottom:.35rem;font-size:.7rem;padding:.1rem .4rem}.dmc-main[data-tiles="0"] .dmc-tile--me{inset:0;width:auto;aspect-ratio:auto;border-radius:0;outline:0;box-shadow:none}.dmc-main[data-tiles="0"] .dmc-tile--me .dmc-chip{left:.75rem;bottom:.75rem;font-size:.78rem;padding:.18rem .5rem}.dmc-main[data-tiles="0"] .dmc-tile--me:after{content:"";position:absolute;inset:0;z-index:2;background:#1b1d218c;pointer-events:none}.dmc-alone{position:absolute;inset:0;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:1rem;pointer-events:none}.dmc-alone-title{margin:0;font-size:1.1rem;font-weight:600;color:#fff}.dmc-alone-sub{margin:.35rem 0 0;color:#c9cdd2;font-size:.88rem}.dmc-waiting{position:absolute;z-index:4;top:calc(44px + .75rem);right:.75rem;width:min(20rem,calc(100% - 1.5rem));padding:.85rem;border-radius:10px;background:var(--dmc-raised);box-shadow:0 12px 32px #00000073;border:1px solid #40454c;animation:dmc-drop .2s ease}@keyframes dmc-drop{0%{opacity:0;transform:translateY(-4px)}}.dmc-waiting h3{display:flex;align-items:center;gap:.5rem;margin:0 0 .7rem;font-size:.85rem;font-weight:600;color:var(--dmc-on-dark)}.dmc-waiting-dot{width:8px;height:8px;border-radius:50%;background:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-waiting ul{list-style:none;margin:0;padding:0;display:grid;gap:.7rem;max-height:14rem;overflow-y:auto}.dmc-waiting li{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:.4rem .6rem}.dmc-winitial{width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:#454a52;color:#fff;font-weight:600;font-size:.85rem}.dmc-wname{font-weight:500;color:var(--dmc-on-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-wbtns{grid-column:2;display:flex;gap:.4rem}.dmc-waiting .dmc-btn svg{width:15px;height:15px}.dmc-waiting .dmc-btn--go{background:var(--dmc-accent-solid);color:#fff}.dmc-btn--ghost{background:transparent;color:var(--dmc-on-dark);border-color:#4a4f57}.dmc-btn--ghost:hover{background:#ffffff0f;border-color:#626872}.dmc-bar{flex:none;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:.5rem;padding:.5rem 1rem;background:var(--dmc-panel);border-top:1px solid var(--dmc-line)}.dmc-bar-mid{display:flex;align-items:center;gap:.35rem}.dmc-bar-end{display:flex;justify-content:flex-end}.dmc-tool{flex-direction:column;gap:3px;min-width:4.25rem;height:3.5rem;min-height:0;padding:.3rem .6rem;border-radius:10px;border:1px solid transparent;background:transparent;color:var(--dmc-on-dark);flex:none;font-weight:400}.dmc-tool:hover{background:var(--dmc-raised);border-color:transparent}.dmc-tool svg{width:20px;height:20px}.dmc-lbl{font-size:.72rem;line-height:1;color:var(--dmc-muted-dark);white-space:nowrap}.dmc-tool:hover .dmc-lbl{color:var(--dmc-on-dark)}.dmc-tool.is-off{background:transparent;color:#ff8a85}.dmc-tool.is-off:hover{background:#d9453f24}.dmc-tool.is-off .dmc-lbl{color:#ff8a85}.dmc-tool.is-on{background:var(--dmc-accent-soft);color:var(--dmc-accent-text)}.dmc-tool.is-on .dmc-lbl{color:var(--dmc-accent-text)}.dmc-btn--rec svg{color:var(--dmc-red);fill:currentColor;width:16px;height:16px;margin:2px 0}.dmc-btn--rec.is-on{background:#d9453f2e;color:#ff8a85}.dmc-btn--rec.is-on .dmc-lbl{color:#ff8a85}.dmc-btn--rec.is-on svg{color:#ff5a52;animation:dmc-blink 1.4s infinite}.dmc-btn--leave{background:var(--dmc-red);color:#fff;flex-direction:row;gap:.45rem;height:2.75rem;min-width:0;padding:0 1.1rem;border-radius:10px;font-weight:600}.dmc-btn--leave:hover{background:var(--dmc-red-hover)}.dmc-btn--leave .dmc-lbl,.dmc-btn--leave:hover .dmc-lbl{color:#fff;font-size:.85rem}.dmc-btn--leave svg{width:18px;height:18px}.dmc-badge{position:absolute;top:2px;right:10px;min-width:1.1rem;height:1.1rem;padding:0 .28rem;border-radius:999px;background:var(--dmc-accent-solid);color:#fff;font-size:.66rem;font-weight:600;line-height:1.1rem;text-align:center;box-shadow:0 0 0 2px var(--dmc-panel)}.dmc-chat{flex:none;width:0;display:flex;flex-direction:column;background:var(--dmc-panel);color:var(--dmc-on-dark);overflow:hidden;transition:width .2s ease;border-left:1px solid var(--dmc-line);font-size:14px}.dmc-call:not(.is-chat) .dmc-chat{border-left-width:0}.dmc-call.is-chat .dmc-chat{width:20rem}.dmc-chat>*{min-width:20rem}.dmc-chat-head{flex:none;display:flex;align-items:center;gap:.5rem;height:44px;padding:0 .5rem 0 1rem;border-bottom:1px solid var(--dmc-line)}.dmc-chat-titles{flex:1;min-width:0;display:flex;align-items:baseline;gap:.6rem}.dmc-chat-titles h3{margin:0;font-size:.92rem;font-weight:600;color:var(--dmc-on-dark)}.dmc-chat-titles span{font-size:.75rem;color:var(--dmc-muted-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-chat-back,.dmc-chat-close{width:2rem;height:2rem;border:0;border-radius:8px;background:transparent;color:var(--dmc-muted-dark);cursor:pointer;display:grid;place-items:center}.dmc-chat-back:hover,.dmc-chat-close:hover{background:var(--dmc-raised);color:var(--dmc-on-dark)}.dmc-chat-back:focus-visible,.dmc-chat-close:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:1px}.dmc-chat-back svg,.dmc-chat-close svg{width:18px;height:18px}.dmc-chat-back{display:none}.dmc-chat-body{position:relative;flex:1;min-height:0;display:flex}.dmc-messages{list-style:none;margin:0;padding:.5rem 1rem 1rem;flex:1;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#43484f transparent}.dmc-chat-empty{position:absolute;inset:auto 1.5rem 45%;margin:0;text-align:center;color:var(--dmc-muted-dark);font-size:.85rem;line-height:1.5}.dmc-msg{display:flex;flex-direction:column;margin-top:3px}.dmc-msg.is-first{margin-top:.85rem}.dmc-msg.is-mine{align-items:flex-end}.dmc-msg.is-other{align-items:flex-start}.dmc-msg.is-sys{align-items:center;margin-top:.6rem}.dmc-msg.is-sys+.dmc-msg.is-sys:not(.has-day){margin-top:.2rem}.dmc-msg.has-day.is-first{margin-top:.35rem}.dmc-day{align-self:stretch;display:flex;align-items:center;gap:.75rem;margin:.5rem 0;color:var(--dmc-muted-dark);font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.dmc-day:before,.dmc-day:after{content:"";flex:1;height:1px;background:var(--dmc-line)}.dmc-sys{color:#80868e;font-size:.76rem;line-height:1.4;text-align:center}.dmc-meta{display:flex;align-items:baseline;gap:.5rem;margin:0 .15rem .2rem;max-width:88%}.dmc-from{font-size:.8rem;font-weight:500;color:#8fb0ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-time{font-size:.7rem;color:#80868e;font-variant-numeric:tabular-nums;flex:none}.dmc-bubble{max-width:88%;padding:.45rem .7rem;border-radius:8px;background:var(--dmc-raised);color:var(--dmc-on-dark);line-height:1.42;overflow-wrap:anywhere}.is-mine .dmc-bubble{background:color-mix(in srgb,var(--dmc-accent) 20%,var(--dmc-panel));color:#f1f4ff}.dmc-text{white-space:pre-wrap}.dmc-msg[data-hue="1"] .dmc-from{color:#6fc2ad}.dmc-msg[data-hue="2"] .dmc-from{color:#d6ad72}.dmc-msg[data-hue="3"] .dmc-from{color:#bf9fdc}.dmc-msg[data-hue="4"] .dmc-from{color:#e0948e}.dmc-msg[data-hue="5"] .dmc-from{color:#9fcb8d}.dmc-msg[data-hue="6"] .dmc-from{color:#7fbad6}.dmc-msg[data-hue="7"] .dmc-from{color:#d49fba}.dmc-newchip{position:absolute;left:50%;bottom:.75rem;transform:translate(-50%);display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;border:1px solid #40454c;border-radius:8px;padding:.35rem .7rem;background:var(--dmc-raised);color:var(--dmc-on-dark);font:inherit;font-size:.78rem;box-shadow:0 4px 14px #0006}.dmc-newchip svg{width:14px;height:14px;color:var(--dmc-accent-text)}.dmc-send{flex:none;display:flex;align-items:flex-end;gap:.5rem;padding:.75rem 1rem;border-top:1px solid var(--dmc-line)}.dmc-compose{flex:1;resize:none;border:1px solid #40454c;border-radius:8px;padding:.5rem .7rem;min-height:0;background:var(--dmc-base);color:var(--dmc-on-dark);line-height:1.35rem;font-size:14px;overflow-y:hidden;max-height:calc(6.4rem + 2px);display:block}.dmc-compose::placeholder{color:#80868e}.dmc-compose:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-sendbtn{width:2.4rem;height:2.4rem;min-height:0;padding:0;border:0;border-radius:8px;background:var(--dmc-accent-solid);color:#fff;flex:none}.dmc-sendbtn:hover{background:color-mix(in srgb,var(--dmc-accent) 74%,#000)}.dmc-sendbtn:disabled{opacity:1;background:var(--dmc-raised);color:#80868e}.dmc-sendbtn svg{width:17px;height:17px;margin-left:-2px}.dmc-invite{flex:none;width:0;display:flex;flex-direction:column;background:var(--dmc-panel);color:var(--dmc-on-dark);overflow:hidden;transition:width .2s ease;border-left:0 solid var(--dmc-line);font-size:14px}.dmc-invite.is-open{width:22rem;border-left-width:1px}.dmc-invite>*{min-width:22rem}.dmc .dmc-invite ul{margin:0;padding:0;list-style:none}.dmc .dmc-invite li{margin:0}.dmc-inv-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:.85rem 1rem 1rem;scrollbar-width:thin;scrollbar-color:#43484f transparent}.dmc-inv-input{font:inherit;font-size:14px;width:100%;min-width:0;min-height:2.4rem;padding:.45rem .7rem;border:1px solid #40454c;border-radius:8px;background:var(--dmc-base);color:var(--dmc-on-dark)}.dmc-inv-input::placeholder{color:#80868e}.dmc-inv-input:focus{outline:0;border-color:var(--dmc-accent);box-shadow:0 0 0 3px var(--dmc-accent-soft)}.dmc-inv-search{position:relative;display:block}.dmc-inv-search .dmc-inv-input{padding-left:2.2rem}.dmc-inv-sico{position:absolute;left:.7rem;top:50%;transform:translateY(-50%);display:inline-flex;color:#80868e;pointer-events:none}.dmc-inv-sico svg{width:16px;height:16px}.dmc-inv-label{margin:1rem 0 .4rem;font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dmc-muted-dark)}.dmc-inv-chips{margin-top:.75rem}.dmc-inv-chips ul{display:flex;flex-wrap:wrap;gap:.35rem}.dmc-inv-chip{display:inline-flex;align-items:center;gap:.35rem;max-width:100%;padding:.2rem .25rem .2rem .6rem;border-radius:6px;background:var(--dmc-accent-soft);color:var(--dmc-accent-text);font-size:.8rem}.dmc-inv-chipname{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#f1f4ff;font-weight:500}.dmc-inv-chipkind{font-size:.68rem;opacity:.85}.dmc-inv-chipx{display:grid;place-items:center;width:1.4rem;height:1.4rem;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer;padding:0}.dmc-inv-chipx:hover{background:#ffffff1a}.dmc-inv-chipx:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:1px}.dmc-inv-chipx svg{width:13px;height:13px}.dmc-inv-list{list-style:none;margin:0;padding:0;display:grid;gap:2px}.dmc-inv-row{width:100%;display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:.6rem;padding:.45rem .5rem;border:1px solid transparent;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dmc-inv-row:hover{background:var(--dmc-raised)}.dmc-inv-row:focus-visible{outline:2px solid var(--dmc-accent);outline-offset:-2px}.dmc-inv-row.is-picked{background:var(--dmc-accent-soft)}.dmc-inv-av{width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:#454a52;color:#fff;font-weight:600;font-size:.82rem}.dmc-inv-who{min-width:0;display:flex;flex-direction:column}.dmc-inv-name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-inv-mail{font-size:.75rem;color:var(--dmc-muted-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dmc-inv-kind{font-size:.68rem;font-weight:600;padding:.1rem .4rem;border-radius:4px;border:1px solid #4a4f57;color:var(--dmc-muted-dark)}.dmc-inv-kind[data-kind=user]{border-color:transparent;background:#3a3f46;color:#c9cdd2}.dmc-inv-tick{width:1.25rem;height:1.25rem;border-radius:50%;display:grid;place-items:center;border:1.5px solid #5a6069;color:transparent}.dmc-inv-tick svg{width:12px;height:12px}.dmc-inv-row.is-picked .dmc-inv-tick{background:var(--dmc-accent-solid);border-color:transparent;color:#fff}.dmc-inv-only{margin:.25rem 0 0;color:var(--dmc-on-dark);font-size:.88rem;line-height:1.5}.dmc-inv-empty{margin:.5rem .25rem;color:var(--dmc-muted-dark);font-size:.85rem;line-height:1.45}.dmc-inv-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) auto;gap:.4rem}.dmc-inv-add{width:2.4rem;min-height:2.4rem;padding:0;background:var(--dmc-raised);border-color:#40454c;color:var(--dmc-on-dark)}.dmc-inv-add:hover{background:#3a3f46;border-color:#5a6069}.dmc-inv-add svg{width:17px;height:17px}.dmc-inv-foot{flex:none;border-top:1px solid var(--dmc-line);padding:.75rem 1rem calc(.85rem + env(safe-area-inset-bottom,0px));display:grid;gap:.55rem}.dmc-inv-via{border:0;margin:0;padding:0;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:.35rem 1rem}.dmc-inv-via legend{float:left;width:100%;margin:0 0 .1rem;padding:0;font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dmc-muted-dark)}.dmc-inv-via label{display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.86rem}.dmc-inv-via input{width:16px;height:16px;margin:0;accent-color:var(--dmc-accent)}.dmc-inv-hint{margin:0;font-size:.75rem;line-height:1.45;color:#80868e}.dmc-inv-status{margin:0;font-size:.82rem;line-height:1.4;color:#9fcb8d}.dmc-inv-status.is-err{color:#ff8a85}.dmc-inv-results{list-style:none;margin:0;padding:0;display:grid;gap:.25rem;max-height:7.5rem;overflow-y:auto;font-size:.8rem}.dmc-inv-res{display:flex;gap:.4rem;line-height:1.35;color:var(--dmc-muted-dark)}.dmc-inv-res:before{content:"";flex:none;width:6px;height:6px;margin-top:.4em;border-radius:50%;background:#6fb56a}.dmc-inv-res.is-err:before{background:#e0605a}.dmc-inv-rname{color:var(--dmc-on-dark);font-weight:500;flex:none;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dmc-inv-link{font-size:.78rem;color:var(--dmc-muted-dark)}.dmc-inv-btns{display:flex;gap:.5rem}.dmc-inv-btns .dmc-btn{flex:1;min-height:2.5rem}.dmc-inv-copy svg,.dmc-inv-send svg{width:16px;height:16px}.dmc-inv-send:disabled{opacity:1;background:var(--dmc-raised);color:#80868e}.dmc-tool--invite svg{width:20px;height:20px}@media(max-width:760px){.dmc.dmc--app{padding:1rem .75rem}.dmc-panel{grid-template-columns:1fr;gap:1.1rem;padding:1rem}.dmc-panel--msg{padding:2rem 1.25rem}.dmc-title{font-size:1.15rem}.dmc-top{padding:0 .75rem}.dmc-waiting{left:.5rem;right:.5rem;top:calc(44px + .5rem);width:auto}.dmc-bar{display:flex;justify-content:space-around;gap:.25rem;padding:.45rem .4rem calc(.45rem + env(safe-area-inset-bottom,0px))}.dmc-bar-mid{display:contents}.dmc-tool{width:44px;height:44px;min-width:0;padding:0;background:var(--dmc-raised)}.dmc-tool.is-off{background:#d9453f33}.dmc-lbl{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%)}.dmc-bar-side:empty{display:none}.dmc-bar-end{display:contents}.dmc-btn--leave{width:52px;padding:0;background:var(--dmc-red)}.dmc-badge{top:-4px;right:-4px}.dmc-tile--me{right:.5rem;bottom:.5rem}.dmc.dmc--app:has(.dmc-call.is-chat){z-index:1000}.dmc-chat{position:fixed;inset:0;z-index:1000;width:auto;border:0;transform:translateY(100%);transition:transform .22s ease,visibility 0s .22s;visibility:hidden}.dmc-call.is-chat .dmc-chat{width:auto;transform:none;visibility:visible;transition:transform .22s ease}.dmc-chat>*{min-width:0}.dmc-chat-back{display:grid}.dmc-chat-close{display:none}.dmc-chat-head{padding:0 .75rem 0 .35rem}.dmc-send{padding-bottom:calc(.75rem + env(safe-area-inset-bottom,0px))}.dmc.dmc--app:has(.dmc-invite.is-open){z-index:1000}.dmc-invite{position:fixed;inset:0;z-index:1000;width:auto;border:0;transform:translateY(100%);transition:transform .22s ease,visibility 0s .22s;visibility:hidden}.dmc-invite.is-open{width:auto;border:0;transform:none;visibility:visible;transition:transform .22s ease}.dmc-invite>*{min-width:0}}@media(max-width:760px)and (orientation:portrait){.dmc-tile--me{width:clamp(6rem,26%,8.5rem);aspect-ratio:3 / 4}}@media(prefers-reduced-motion:reduce){.dmc *,.dmc *:after{animation:none!important;transition:none!important}}
@@ -12,6 +12,8 @@
12
12
  * The screen is domma-reactive (M) - stages, controls, people waiting, chat -
13
13
  * and plain code only where bindings cannot reach: the <video> elements'
14
14
  * streams, the recording canvas, and the RTCPeerConnections themselves.
15
+ * Background blur is blur.js, imported the first time it is turned on; the choice
16
+ * is remembered in this browser (localStorage 'dm-call:blur').
15
17
  * Browsers connect to each other directly (a mesh - up to 6 people); the
16
18
  * site only passes the messages that set that up (see _lib/call/routes.js).
17
19
  * Every piece of text goes in through bindings (textContent), never as HTML.
@@ -44,6 +46,7 @@ const TEMPLATE = `
44
46
  <div class="dmc-preview-tools">
45
47
  <button type="button" class="dmc-btn dmc-round" data-on-click="toggleMic" data-bind-class="!micOn.value && 'is-off'" data-bind-aria-pressed="!micOn.value" data-bind-aria-label="micOn.value ? 'Turn the microphone off' : 'Turn the microphone on'">${pair('micOn.value', 'mic', 'mic-off')}</button>
46
48
  <button type="button" class="dmc-btn dmc-round" data-on-click="toggleCam" data-bind-class="!camOn.value && 'is-off'" data-bind-aria-pressed="!camOn.value" data-bind-aria-label="camOn.value ? 'Turn the camera off' : 'Turn the camera on'">${pair('camOn.value', 'video', 'video-off')}</button>
49
+ <button type="button" class="dmc-btn dmc-round dmc-round--blur" data-on-click="toggleBlur" data-bind-hidden="!blurOffered.value" data-bind-disabled="blurBusy.value" data-bind-class="blurOn.value && 'is-on'" data-bind-aria-pressed="blurOn.value" data-bind-aria-label="blurOn.value ? 'Stop blurring your background' : 'Blur your background'" data-bind-title="blurLabel.value">${ico('sparkles')}</button>
47
50
  </div>
48
51
  </div>
49
52
  <div class="dmc-check">
@@ -54,7 +57,9 @@ const TEMPLATE = `
54
57
  <div class="dmc-row">
55
58
  <button type="button" class="dmc-btn dmc-chipbtn" data-on-click="toggleMic" data-bind-class="!micOn.value && 'is-off'" data-bind-aria-pressed="!micOn.value">${pair('micOn.value', 'mic', 'mic-off')}<span data-bind-text="micOn.value ? 'Mic on' : 'Mic off'"></span></button>
56
59
  <button type="button" class="dmc-btn dmc-chipbtn" data-on-click="toggleCam" data-bind-class="!camOn.value && 'is-off'" data-bind-aria-pressed="!camOn.value">${pair('camOn.value', 'video', 'video-off')}<span data-bind-text="camOn.value ? 'Camera on' : 'Camera off'"></span></button>
60
+ <button type="button" class="dmc-btn dmc-chipbtn" data-on-click="toggleBlur" data-bind-hidden="!blurOffered.value" data-bind-disabled="blurBusy.value" data-bind-class="blurOn.value && 'is-on'" data-bind-aria-pressed="blurOn.value">${ico('sparkles')}<span data-bind-text="blurLabel.value"></span></button>
57
61
  </div>
62
+ <p class="dmc-note" data-bind-hidden="!blurNote.value" data-bind-text="blurNote.value" role="status"></p>
58
63
  <p class="dmc-note dmc-note--rec" data-bind-hidden="!recordingAllowed.value">${ico('circle')}<span>The host may record this call. You will see a red Recording notice while they do.</span></p>
59
64
  <p class="dmc-error" data-bind-hidden="!error.value" data-bind-text="error.value" role="alert"></p>
60
65
  <button type="button" class="dmc-btn dmc-btn--go" data-on-click="join" data-bind-disabled="busy.value">${ico('phone')}<span>Join the call</span></button>
@@ -118,6 +123,7 @@ const TEMPLATE = `
118
123
  <div class="dmc-bar-mid">
119
124
  <button type="button" class="dmc-btn dmc-tool" data-on-click="toggleMic" data-bind-class="!micOn.value && 'is-off'" data-bind-aria-pressed="!micOn.value" data-bind-aria-label="micOn.value ? 'Mute' : 'Unmute'">${pair('micOn.value', 'mic', 'mic-off')}<span class="dmc-lbl" data-bind-text="micOn.value ? 'Mute' : 'Unmute'"></span></button>
120
125
  <button type="button" class="dmc-btn dmc-tool" data-on-click="toggleCam" data-bind-class="!camOn.value && 'is-off'" data-bind-aria-pressed="!camOn.value" data-bind-aria-label="camOn.value ? 'Stop video' : 'Start video'">${pair('camOn.value', 'video', 'video-off')}<span class="dmc-lbl" data-bind-text="camOn.value ? 'Stop video' : 'Start video'"></span></button>
126
+ <button type="button" class="dmc-btn dmc-tool dmc-tool--blur" data-on-click="toggleBlur" data-bind-hidden="!blurOffered.value" data-bind-disabled="blurBusy.value" data-bind-class="blurOn.value && 'is-on'" data-bind-aria-pressed="blurOn.value" data-bind-aria-label="blurOn.value ? 'Stop blurring your background' : 'Blur your background'" data-bind-title="blurOn.value ? 'Stop blurring your background' : 'Blur your background'">${ico('sparkles')}<span class="dmc-lbl" data-bind-text="blurLabel.value"></span></button>
121
127
  <button type="button" class="dmc-btn dmc-tool dmc-tool--share" data-on-click="toggleShare" data-bind-hidden="!canShare" data-bind-class="sharing.value && 'is-on'" data-bind-aria-pressed="sharing.value" data-bind-aria-label="sharing.value ? 'Stop sharing' : 'Share your screen'">${ico('monitor')}<span class="dmc-lbl" data-bind-text="sharing.value ? 'Stop share' : 'Share'"></span></button>
122
128
  <button type="button" class="dmc-btn dmc-tool dmc-tool--chat" data-on-click="toggleSide" data-bind-class="sideOpen.value && 'is-on'" data-bind-aria-pressed="sideOpen.value" data-bind-aria-label="chatLabel.value">${ico('message-circle')}<span class="dmc-lbl">Chat</span><span class="dmc-badge" data-bind-hidden="!unread.value || sideOpen.value" data-bind-text="unreadText.value"></span></button>
123
129
  <button type="button" class="dmc-btn dmc-tool dmc-tool--invite" data-slot="invite" data-on-click="toggleInvite" data-bind-hidden="!canInvite.value" data-bind-class="inviteOpen.value && 'is-on'" data-bind-aria-pressed="inviteOpen.value" aria-label="Add people">${ico('user-plus')}<span class="dmc-lbl">Add people</span></button>
@@ -235,6 +241,17 @@ function mount(el) {
235
241
  }
236
242
 
237
243
  // ---- State the screen shows -------------------------------------------------------
244
+ // Blur needs WebAssembly, a canvas that can become a video track, and WebGL2 (MediaPipe reads frames through it
245
+ // even when it segments on the CPU) - offered only where all three are there.
246
+ const canBlur = typeof WebAssembly === 'object' && typeof HTMLCanvasElement.prototype.captureStream === 'function'
247
+ && (() => {
248
+ try {
249
+ const gl = document.createElement('canvas').getContext('webgl2');
250
+ gl?.getExtension('WEBGL_lose_context')?.loseContext(); // only asked; give the context straight back
251
+ return Boolean(gl);
252
+ } catch { return false; }
253
+ })();
254
+ let blurWanted = false; // what the caller last asked for (a slow start may finish after they changed their mind)
238
255
  const stage = M.observable('check'); // check | waiting | in | ended | away | error
239
256
  const name = M.observable(cfg.name || '');
240
257
  const micOn = M.observable(true);
@@ -274,6 +291,14 @@ function mount(el) {
274
291
  const canSend = M.computed(() => /\S/.test(draft.value || ''));
275
292
  const title = M.observable(cfg.title || 'Call');
276
293
 
294
+ // ---- Background blur (blur.js, loaded the first time it is turned on) ------------------
295
+ const hasCam = M.observable(false);
296
+ const blurOn = M.observable(false);
297
+ const blurBusy = M.observable(false);
298
+ const blurNote = M.observable('');
299
+ const blurOffered = M.computed(() => canBlur && hasCam.value);
300
+ const blurLabel = M.computed(() => (blurBusy.value ? 'Starting blur…' : blurOn.value ? 'Blur on' : 'Blur'));
301
+
277
302
  // ---- Add people (the host, when the Tool allows it: join answers canInvite) ----------------
278
303
  const canInvite = M.observable(false);
279
304
  // What the Tool offers this host (join's inviteOptions): search the team (+ contacts when typed), contacts and
@@ -325,13 +350,69 @@ function mount(el) {
325
350
  // peers is kept in step with tiles (a plain observable, not a computed over a computed).
326
351
  const stopTiles = M.effect(() => { tiles.value = String(Math.min(6, peers.value.length)); });
327
352
 
353
+ let blur = null; // {track, stop, stream} while the background is blurred
354
+ const BLUR_KEY = 'dm-call:blur';
355
+ /** The picture this browser sends while not sharing a screen: the blurred one, or the camera's. */
356
+ const camVideo = () => blur?.track || local?.getVideoTracks()[0] || null;
357
+
328
358
  const selfVideos = () => el.querySelectorAll('.dmc-self, .dmc-self-preview');
329
- const showSelf = () => selfVideos().forEach(v => { v.srcObject = screen || local; });
359
+ const selfStream = () => screen || blur?.stream || local;
360
+ const showSelf = () => { const s = selfStream(); selfVideos().forEach(v => { v.srcObject = s; }); };
361
+ /** Each connection's video sender: the camera's, or one added for a screen when there was no camera. */
362
+ const videoSender = (entry) => entry.pc.getSenders().find(s => s.track?.kind === 'video') || entry.videoSender || null;
363
+ /** Put the current camera picture (blurred or not) on every connection - not while a screen is shared. */
364
+ const sendCam = () => { if (!screen) pcs.forEach(entry => videoSender(entry)?.replaceTrack(camVideo())); };
365
+
366
+ async function startBlurring() {
367
+ const cam = local?.getVideoTracks()[0];
368
+ if (!cam || blur || blurBusy.value) return;
369
+ blurBusy.value = true;
370
+ blurNote.value = '';
371
+ try {
372
+ const mod = await import(new URL(`./blur.js${new URL(import.meta.url).search}`, import.meta.url).href);
373
+ const started = await mod.startBlur(cam);
374
+ if (!local || local.getVideoTracks()[0] !== cam || !blurWanted) { started.stop(); return; } // left, switched off or lost the camera meanwhile
375
+ blur = Object.assign(started, {stream: new MediaStream([started.track])});
376
+ blur.track.enabled = camOn.value;
377
+ blur.paused = Boolean(screen);
378
+ // It stops itself if this machine cannot keep it going: back to the plain camera, and say so.
379
+ blur.track.addEventListener('ended', () => {
380
+ if (blur !== started) return;
381
+ blur = null; blurOn.value = false; sendCam(); showSelf();
382
+ blurSay(started.reason === 'slow' ? 'Background blur turned off - this device could not keep up.'
383
+ : 'Background blur stopped working, so it is off. You can try turning it on again.');
384
+ });
385
+ blurOn.value = true;
386
+ sendCam();
387
+ showSelf();
388
+ } catch (err) {
389
+ console.warn('[call] blur:', err?.message || err);
390
+ blurOn.value = false;
391
+ blurSay('Background blur could not start in this browser.');
392
+ try { localStorage.removeItem(BLUR_KEY); } catch { /* storage blocked */ }
393
+ } finally { blurBusy.value = false; }
394
+ }
395
+ /** A blur notice: under the buttons before the call, and as a line in the chat (this browser only) once in it. */
396
+ let blurSaid = 0;
397
+ function blurSay(text) {
398
+ blurNote.value = text;
399
+ if (stage.value === 'in') addItem({id: `blur-${Date.now()}-${++blurSaid}`, kind: 'system', at: new Date().toISOString(), text});
400
+ }
401
+ function stopBlurring() {
402
+ const was = blur;
403
+ blur = null;
404
+ blurOn.value = false;
405
+ if (!was) return;
406
+ sendCam();
407
+ showSelf();
408
+ was.stop();
409
+ }
330
410
 
331
411
  async function getDevices() {
332
412
  try {
333
413
  local = await navigator.mediaDevices.getUserMedia({video: {width: {ideal: 1280}, height: {ideal: 720}}, audio: true});
334
414
  deviceNote.value = 'Your camera and microphone are working.';
415
+ hasCam.value = local.getVideoTracks().length > 0;
335
416
  } catch {
336
417
  try {
337
418
  local = await navigator.mediaDevices.getUserMedia({audio: true});
@@ -345,10 +426,13 @@ function mount(el) {
345
426
  }
346
427
  }
347
428
  showSelf();
429
+ try { blurWanted = localStorage.getItem(BLUR_KEY) === '1'; } catch { /* storage blocked */ }
430
+ if (blurWanted && canBlur && hasCam.value) startBlurring();
348
431
  }
349
432
  const setTracks = () => {
350
433
  local?.getAudioTracks().forEach(t => { t.enabled = micOn.value; });
351
434
  local?.getVideoTracks().forEach(t => { t.enabled = camOn.value; });
435
+ if (blur) blur.track.enabled = camOn.value;
352
436
  };
353
437
 
354
438
  // ---- Peer connections (perfect negotiation) ----------------------------------------
@@ -357,7 +441,8 @@ function mount(el) {
357
441
  const pc = new RTCPeerConnection({iceServers});
358
442
  const entry = {pc, polite: me.id > peer.id, makingOffer: false, ignore: false, stream: new MediaStream(), chain: Promise.resolve()};
359
443
  pcs.set(peer.id, entry);
360
- const out = screen ? [...(local?.getAudioTracks() || []), ...screen.getVideoTracks()] : (local?.getTracks() || []);
444
+ const cam = camVideo();
445
+ const out = [...(local?.getAudioTracks() || []), ...(screen ? screen.getVideoTracks() : cam ? [cam] : [])];
361
446
  out.forEach(track => pc.addTrack(track, local));
362
447
  pc.ontrack = (e) => { entry.stream.addTrack(e.track); attach(); if (e.track.kind === 'audio') addToMix(e.track); };
363
448
  pc.onicecandidate = (e) => { if (e.candidate) post('signal', {to: peer.id, data: {candidate: e.candidate}}); };
@@ -606,6 +691,8 @@ function mount(el) {
606
691
  clearInterval(clockTimer); clockTimer = null;
607
692
  stage.value = kind;
608
693
  stopRecording();
694
+ blurWanted = false;
695
+ stopBlurring();
609
696
  for (const id of [...pcs.keys()]) dropPeer(id);
610
697
  [local, screen].forEach(s => s?.getTracks().forEach(t => t.stop()));
611
698
  me = null;
@@ -672,7 +759,7 @@ function mount(el) {
672
759
  title, people, tiles, chatLabel, askName: cfg.askName !== false, canShare: Boolean(navigator.mediaDevices?.getDisplayMedia), canRecord,
673
760
  stage, name, initial, micOn, camOn, sharing, recording, iRecord, recordingAllowed, host, busy, error, deviceNote, sideOpen, unread,
674
761
  unreadText, draft, canSend, peers, waiting, waitingTitle, messages, newBelow, endTitle, endText, meCaption, countText, countTitle,
675
- clock, chatSub, aloneText,
762
+ clock, chatSub, aloneText, blurOn, blurBusy, blurNote, blurOffered, blurLabel,
676
763
  canInvite, inviteSearch, inviteTyped, inviteCopy, inviteOpen, query, found, picked, typedName, typedEmail, viaNotify, viaMessage, viaEmail, canMessage,
677
764
  inviteBusy, inviteNote, inviteBad, sent, copiedLink, inviteSub, searchHint, foundTitle, emptyText, viaHint, canSendInvites, sendLabel,
678
765
 
@@ -749,6 +836,13 @@ function mount(el) {
749
836
  },
750
837
 
751
838
  toggleMic() { micOn.value = !micOn.value; setTracks(); },
839
+ toggleBlur() {
840
+ if (blurBusy.value) return;
841
+ blurWanted = !blurOn.value;
842
+ try { if (blurWanted) localStorage.setItem(BLUR_KEY, '1'); else localStorage.removeItem(BLUR_KEY); } catch { /* storage blocked */ }
843
+ blurNote.value = '';
844
+ if (blurWanted) startBlurring(); else stopBlurring();
845
+ },
752
846
  toggleCam() {
753
847
  if (!local?.getVideoTracks().length) { error.value = 'No camera is available.'; return; }
754
848
  camOn.value = !camOn.value; setTracks();
@@ -826,15 +920,14 @@ function mount(el) {
826
920
  },
827
921
 
828
922
  async toggleShare() {
829
- // Each connection's video sender: the camera's, or one added for a screen when there was no camera
830
- // (remembered, so sharing again reuses it rather than adding another).
831
- const videoSender = (entry) => entry.pc.getSenders().find(s => s.track?.kind === 'video') || entry.videoSender || null;
923
+ // A screen goes out on the camera's sender, or one added for it when there was no camera
924
+ // (remembered as entry.videoSender, so sharing again reuses it rather than adding another).
832
925
  if (sharing.value) {
833
926
  screen?.getTracks().forEach(t => t.stop());
834
927
  screen = null;
835
928
  sharing.value = false;
836
- const cam = local?.getVideoTracks()[0] || null;
837
- pcs.forEach(entry => videoSender(entry)?.replaceTrack(cam));
929
+ if (blur) blur.paused = false;
930
+ sendCam();
838
931
  showSelf();
839
932
  return;
840
933
  }
@@ -843,6 +936,7 @@ function mount(el) {
843
936
  const track = screen.getVideoTracks()[0];
844
937
  track.onended = () => { if (sharing.value) model.toggleShare(); };
845
938
  sharing.value = true;
939
+ if (blur) blur.paused = true; // nobody sees it while the screen is shown
846
940
  pcs.forEach(entry => {
847
941
  const sender = videoSender(entry);
848
942
  if (sender) sender.replaceTrack(track);
@@ -909,7 +1003,7 @@ function mount(el) {
909
1003
  el.querySelector('.dmc-messages')?.addEventListener('scroll', () => { if (newBelow.value && nearBottom()) newBelow.value = false; }, {passive: true});
910
1004
  window.addEventListener('pagehide', () => { if (me) post('leave', {}, {keepalive: true}); });
911
1005
  getDevices().then(setTracks);
912
- return () => { stopLayout?.(); stopAttach?.(); stopScan?.(); stopTiles?.(); clearInterval(clockTimer); handleBindings?.dispose?.(); };
1006
+ return () => { stopLayout?.(); stopAttach?.(); stopScan?.(); stopTiles?.(); clearInterval(clockTimer); blurWanted = false; stopBlurring(); handleBindings?.dispose?.(); };
913
1007
  }
914
1008
 
915
1009
  document.querySelectorAll('[data-dm-call]').forEach(mount);