@vanillaskyai/video 0.10.14 → 0.10.16
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.
- package/CHANGELOG.md +11 -0
- package/dist/react.js +16 -26
- package/dist/server.js +24 -1
- package/package.json +1 -1
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/stock.ts +9 -4
- package/styles/video-chat.css +42 -20
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.10.16
|
|
8
|
+
|
|
9
|
+
- Start the first fully prepared scene without waiting for an eight-second startup buffer, while preserving narration and visual readiness.
|
|
10
|
+
- Keep generated narration alive when a superseded playback attempt rejects after pause and resume.
|
|
11
|
+
- Give ending suggestions larger responsive cards with complete labels, including narrow embedded players.
|
|
12
|
+
- Match simple singular and plural stock subjects in the starter without discarding required subjects or exclusions.
|
|
13
|
+
|
|
14
|
+
## 0.10.15
|
|
15
|
+
|
|
16
|
+
- Recover a mislabeled first chat brief only when its complete authored content validates, preserving its shots and ending. Keep malformed JSON and incomplete or later records rejected.
|
|
17
|
+
|
|
7
18
|
## 0.10.14
|
|
8
19
|
|
|
9
20
|
- Attach content-free record-shape diagnostics to rejected chat plans without accepting alternate formats or exposing model output.
|
package/dist/react.js
CHANGED
|
@@ -1385,18 +1385,6 @@ function createSceneTimeline(options) {
|
|
|
1385
1385
|
}
|
|
1386
1386
|
|
|
1387
1387
|
// src/player/scene-readiness.ts
|
|
1388
|
-
var READY_AHEAD_SECONDS = 8;
|
|
1389
|
-
function canStartPreparedSequence(scenes, complete) {
|
|
1390
|
-
let seconds = 0;
|
|
1391
|
-
let count = 0;
|
|
1392
|
-
for (const scene of scenes) {
|
|
1393
|
-
if (!scene) break;
|
|
1394
|
-
count += 1;
|
|
1395
|
-
seconds += scene.timing.fixedDuration ?? 0;
|
|
1396
|
-
if (seconds >= READY_AHEAD_SECONDS) return true;
|
|
1397
|
-
}
|
|
1398
|
-
return complete && count > 0 && count === scenes.length;
|
|
1399
|
-
}
|
|
1400
1388
|
function preparedSceneDuration(scene, spokenSeconds, metadata) {
|
|
1401
1389
|
const timing = metadata?.timing;
|
|
1402
1390
|
const authored = (timing?.revealSeconds ?? 0) + (timing?.holdSeconds ?? 0) + (timing?.exitSeconds ?? 0);
|
|
@@ -1586,6 +1574,18 @@ function createVideoChatVoice(options = {}) {
|
|
|
1586
1574
|
let disposed = false;
|
|
1587
1575
|
let generatedSpeechUnavailable = false;
|
|
1588
1576
|
let playbackFailure;
|
|
1577
|
+
let playbackAttempt = 0;
|
|
1578
|
+
const playGenerated = (element, fail) => {
|
|
1579
|
+
const attempt = ++playbackAttempt;
|
|
1580
|
+
const reject = () => {
|
|
1581
|
+
if (attempt === playbackAttempt && !held && sounding === element) fail?.();
|
|
1582
|
+
};
|
|
1583
|
+
try {
|
|
1584
|
+
void element.play().catch(reject);
|
|
1585
|
+
} catch {
|
|
1586
|
+
reject();
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
1589
|
const notifyFallback = () => {
|
|
1590
1590
|
try {
|
|
1591
1591
|
void Promise.resolve(options.onFallback?.()).catch(() => void 0);
|
|
@@ -1698,6 +1698,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
1698
1698
|
return { seconds: line.seconds, ...line.source === "generated" && line.measured === true ? { supportsOffsets: true } : {} };
|
|
1699
1699
|
},
|
|
1700
1700
|
pause() {
|
|
1701
|
+
playbackAttempt++;
|
|
1701
1702
|
held = true;
|
|
1702
1703
|
sounding?.pause();
|
|
1703
1704
|
globalThis.speechSynthesis?.pause();
|
|
@@ -1712,12 +1713,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
1712
1713
|
} catch {
|
|
1713
1714
|
}
|
|
1714
1715
|
}
|
|
1715
|
-
if (sounding)
|
|
1716
|
-
const fail = playbackFailure;
|
|
1717
|
-
void sounding.play().catch(() => {
|
|
1718
|
-
if (!held) fail?.();
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1716
|
+
if (sounding) playGenerated(sounding, playbackFailure);
|
|
1721
1717
|
if (!silent) globalThis.speechSynthesis?.resume();
|
|
1722
1718
|
},
|
|
1723
1719
|
setMuted(muted) {
|
|
@@ -1848,13 +1844,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
1848
1844
|
element.onended = finish;
|
|
1849
1845
|
element.onerror = fail;
|
|
1850
1846
|
signal.addEventListener("abort", stop, { once: true });
|
|
1851
|
-
|
|
1852
|
-
if (!held) void element.play().catch(() => {
|
|
1853
|
-
if (!held) fail();
|
|
1854
|
-
});
|
|
1855
|
-
} catch {
|
|
1856
|
-
fail();
|
|
1857
|
-
}
|
|
1847
|
+
if (!held) playGenerated(element, fail);
|
|
1858
1848
|
});
|
|
1859
1849
|
} catch {
|
|
1860
1850
|
playbackFailed = true;
|
|
@@ -2372,7 +2362,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2372
2362
|
return;
|
|
2373
2363
|
}
|
|
2374
2364
|
if (!timeline) {
|
|
2375
|
-
if (!style || openingActive || heldRef.current || available === appended
|
|
2365
|
+
if (!style || openingActive || heldRef.current || available === appended) return;
|
|
2376
2366
|
timeline = createSceneTimeline({ style, orientation });
|
|
2377
2367
|
timelineRef.current = timeline;
|
|
2378
2368
|
openingController.abort(new DOMException("Opening replaced by response", "AbortError"));
|
package/dist/server.js
CHANGED
|
@@ -952,6 +952,21 @@ function readShot(value, clipDurationSec, answerSubject = "") {
|
|
|
952
952
|
continuity: item?.continuity === "continue" ? "continue" : "cut"
|
|
953
953
|
};
|
|
954
954
|
}
|
|
955
|
+
function recoverFirstBrief(part, clipDurationSec) {
|
|
956
|
+
if (!part || typeof part.type !== "string" || !part.type.trim() || part.type === "answer" || part.type === "shot") return;
|
|
957
|
+
const bounded = (value, maximum, allowEmpty = false) => typeof value === "string" && value.trim().length <= maximum && (allowEmpty || Boolean(value.trim()));
|
|
958
|
+
if (!["opening", "subject", "development", "visualDirection", "ending"].every((key) => Object.hasOwn(part, key)) || !bounded(part.opening, 300) || !bounded(part.subject, 80) || !bounded(part.development, 2e3, true) || !bounded(part.visualDirection, 600)) return;
|
|
959
|
+
const ending = object(part.ending);
|
|
960
|
+
if (!ending || !bounded(ending.narration, 2e3) || !bounded(ending.subject, 80, true) || !bounded(ending.action, 600, true) || Object.hasOwn(ending, "title") && !bounded(ending.title, 65) || typeof ending.durationSec !== "number" || !Number.isFinite(ending.durationSec) || ending.continuity !== "cut" && ending.continuity !== "continue") return;
|
|
961
|
+
const subject = text(part.subject, 80);
|
|
962
|
+
return {
|
|
963
|
+
opening: text(part.opening, 300),
|
|
964
|
+
subject,
|
|
965
|
+
development: text(part.development, 2e3),
|
|
966
|
+
visualDirection: text(part.visualDirection, 600),
|
|
967
|
+
ending: readShot(ending, clipDurationSec, subject)
|
|
968
|
+
};
|
|
969
|
+
}
|
|
955
970
|
function replaceStream(source, textStream) {
|
|
956
971
|
if (!(typeof source === "object" && source != null && "textStream" in source)) return textStream;
|
|
957
972
|
return new Proxy({ textStream }, {
|
|
@@ -984,7 +999,7 @@ function createChatShotPlanner(options) {
|
|
|
984
999
|
const upstream = typeof source === "object" && source != null && "textStream" in source ? source.textStream : source;
|
|
985
1000
|
const translated = (async function* () {
|
|
986
1001
|
let brief, buffer = "", index = 0, bodyDuration = 0, lastNarration = "";
|
|
987
|
-
let firstBody = true;
|
|
1002
|
+
let firstBody = true, recordsSeen = 0;
|
|
988
1003
|
const reject = (cause) => {
|
|
989
1004
|
incomplete.add(context);
|
|
990
1005
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
@@ -1011,8 +1026,15 @@ function createChatShotPlanner(options) {
|
|
|
1011
1026
|
const line = (raw) => {
|
|
1012
1027
|
const trimmed = raw.trim();
|
|
1013
1028
|
if (!trimmed || /^```(?:json|ndjson)?$/i.test(trimmed)) return;
|
|
1029
|
+
const firstRecord = recordsSeen++ === 0;
|
|
1014
1030
|
const value = JSON.parse(trimmed);
|
|
1015
1031
|
const part = object(value);
|
|
1032
|
+
const recovered = firstRecord && !brief && index === 0 ? recoverFirstBrief(part, clipDurationSec) : void 0;
|
|
1033
|
+
if (recovered) {
|
|
1034
|
+
brief = recovered;
|
|
1035
|
+
options.publishOpening({ line: brief.opening, keyword: brief.subject });
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1016
1038
|
if (part?.type === "answer") {
|
|
1017
1039
|
if (brief) throw new Error("Chat answer brief was emitted more than once");
|
|
1018
1040
|
brief = { opening: text(part.opening, 300), subject: text(part.subject, 80), visualDirection: text(part.visualDirection, 600), development: text(part.development, 2e3) };
|
|
@@ -1193,6 +1215,7 @@ async function* resolveShots(parts, context, options) {
|
|
|
1193
1215
|
// src/server/video-chat-prompts.ts
|
|
1194
1216
|
function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlreadyProvided = false, maxGeneratedVideos = 5, clipDurationSec = 5, mode = "cinematic") {
|
|
1195
1217
|
return [
|
|
1218
|
+
'Use the exact record type "answer" for the first brief and "shot" for developing beats. Output JSON records only, with no prose outside them, including when explaining a limitation.',
|
|
1196
1219
|
"Write a complete, intentful video answer as newline-delimited JSON. Match the user's form and tone; mixed intents can combine directions.",
|
|
1197
1220
|
`First write one brief: {"type":"answer","intent":"explanation|story|comedy|imagination|practical","opening":"a short inviting spoken introduction of 6\u20139 words","subject":"literal visual subject","development":"the essential development of this answer","visualDirection":"consistent subjects, appearance and visual approach","ending":{"title":"short meaningful chapter title, at most 65 characters","narration":"the authored payoff","subject":"literal subject","action":"visible action or change","durationSec":${clipDurationSec},"continuity":"cut|continue"}}.`,
|
|
1198
1221
|
`Then stream each developing shot on its own line: {"type":"shot","title":"short meaningful chapter title, at most 65 characters","narration":"the exact spoken beat","subject":"2\u20138 literal filmable words, at most 80 characters","action":"concrete subject, action or visible change and useful framing","durationSec":${clipDurationSec},"continuity":"cut|continue"}.`,
|
package/package.json
CHANGED
|
@@ -17,6 +17,11 @@ const cache = new Map<string, { expires: number; media: StockVideo | null }>();
|
|
|
17
17
|
const words = (value: string): string[] => value.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
18
18
|
const ignored = new Set(['a','an','the','in','on','at','of','with','and','to']);
|
|
19
19
|
const terms = (value: string) => words(value).filter(word => !ignored.has(word));
|
|
20
|
+
// Only simple English -s forms; avoid aggressive stemming and ambiguous endings.
|
|
21
|
+
function wordForm(word: string) {
|
|
22
|
+
return /^[a-z]{3,}s$/.test(word) && !/(?:ss|us|is|ies)$/.test(word) && word !== 'news'
|
|
23
|
+
? word.slice(0, -1) : word;
|
|
24
|
+
}
|
|
20
25
|
function selectionHint(value: unknown) {
|
|
21
26
|
const phrase = (input: unknown) => {
|
|
22
27
|
if (typeof input !== 'string') return undefined;
|
|
@@ -48,7 +53,7 @@ export async function findStockFootage(query: string, orientation: VideoOrientat
|
|
|
48
53
|
const normalized = query.trim().toLowerCase().replace(/\s+/g, " ");
|
|
49
54
|
const tokens = words(normalized);
|
|
50
55
|
const selection = selectionHint(rawSelection);
|
|
51
|
-
const key = JSON.stringify({version:
|
|
56
|
+
const key = JSON.stringify({version: 3, orientation, query: normalized, selection});
|
|
52
57
|
const apiKey = process.env.PEXELS_API_KEY;
|
|
53
58
|
if (!apiKey || !tokens.length || normalized.length > 80 || tokens.length > 8) return null;
|
|
54
59
|
const existing = cache.get(key);
|
|
@@ -66,10 +71,10 @@ export async function findStockFootage(query: string, orientation: VideoOrientat
|
|
|
66
71
|
const slug = new URL(video.url).pathname.replace(/^\/video\//, "");
|
|
67
72
|
const title = typeof video.title === "string" ? video.title : "";
|
|
68
73
|
const tags = Array.isArray(video.tags) ? video.tags.filter((tag): tag is string => typeof tag === "string").join(" ") : "";
|
|
69
|
-
const subject = words(`${slug} ${title} ${typeof video.description === "string" ? video.description : ""} ${tags}`).filter(token => !/^\d+$/.test(token));
|
|
70
|
-
let matches = tokens.filter(token => subject.includes(token)).length;
|
|
74
|
+
const subject = words(`${slug} ${title} ${typeof video.description === "string" ? video.description : ""} ${tags}`).filter(token => !/^\d+$/.test(token)).map(wordForm);
|
|
75
|
+
let matches = tokens.filter(token => subject.includes(wordForm(token))).length;
|
|
71
76
|
if (selection && subject.length) {
|
|
72
|
-
const covers = (phrase: string) => terms(phrase).every(word => subject.includes(word));
|
|
77
|
+
const covers = (phrase: string) => terms(phrase).every(word => subject.includes(wordForm(word)));
|
|
73
78
|
if (!covers(selection.subject) || selection.exclude?.some(covers)) continue;
|
|
74
79
|
// Query context breaks equal hint matches without outweighing a hint.
|
|
75
80
|
const contextScore = matches / (tokens.length + 1);
|
package/styles/video-chat.css
CHANGED
|
@@ -348,12 +348,15 @@
|
|
|
348
348
|
|
|
349
349
|
.vanillasky-video-chat .ending-body {
|
|
350
350
|
position: absolute;
|
|
351
|
-
inset:
|
|
351
|
+
inset: 96px 0 180px;
|
|
352
352
|
display: flex;
|
|
353
353
|
flex-direction: column;
|
|
354
354
|
align-items: flex-start;
|
|
355
355
|
gap: clamp(0.5rem, 1.4cqh, 0.9rem);
|
|
356
|
-
padding:
|
|
356
|
+
padding: 12px clamp(20px, 3.5cqw, 56px);
|
|
357
|
+
overflow-y: auto;
|
|
358
|
+
overscroll-behavior: contain;
|
|
359
|
+
justify-content: safe flex-end;
|
|
357
360
|
color: oklch(1 0 0);
|
|
358
361
|
}
|
|
359
362
|
|
|
@@ -367,9 +370,13 @@
|
|
|
367
370
|
text-shadow: 0 1px 3px oklch(0.14 0.05 265 / 70%);
|
|
368
371
|
}
|
|
369
372
|
|
|
370
|
-
.vanillasky-video-chat .ending .cards {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
+
.vanillasky-video-chat .ending .cards {
|
|
374
|
+
margin-top: 0;
|
|
375
|
+
display: grid;
|
|
376
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
377
|
+
flex-shrink: 0;
|
|
378
|
+
overflow: visible;
|
|
379
|
+
}
|
|
373
380
|
|
|
374
381
|
.vanillasky-video-chat .ending .card-dots { align-self: flex-start; margin-left: -0.35rem; }
|
|
375
382
|
|
|
@@ -410,7 +417,6 @@
|
|
|
410
417
|
animation: vanillasky-video-chat-fade-in 320ms ease both;
|
|
411
418
|
}
|
|
412
419
|
|
|
413
|
-
|
|
414
420
|
@keyframes vanillasky-video-chat-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
415
421
|
|
|
416
422
|
@keyframes vanillasky-video-chat-rise { from { opacity: 0; transform: translate(-50%, 6px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
|
@@ -600,7 +606,6 @@
|
|
|
600
606
|
|
|
601
607
|
.vanillasky-video-chat .panel .error { pointer-events: auto; width: min(560px, 100%); background: var(--vs-bg); }
|
|
602
608
|
|
|
603
|
-
|
|
604
609
|
.vanillasky-video-chat .welcome { background: var(--vs-media-ground); }
|
|
605
610
|
|
|
606
611
|
.vanillasky-video-chat .welcome .frame-media { max-width: none; max-height: none; object-fit: cover; }
|
|
@@ -625,10 +630,6 @@
|
|
|
625
630
|
|
|
626
631
|
.vanillasky-video-chat .card-dots { display: none; }
|
|
627
632
|
|
|
628
|
-
.vanillasky-video-chat .ending-body { padding: 0 var(--vs-media-gutter) 190px; }
|
|
629
|
-
|
|
630
|
-
.vanillasky-video-chat .ending .cards { margin-top: 0; }
|
|
631
|
-
|
|
632
633
|
.vanillasky-video-chat .ending-label { font-size: 15px; margin-bottom: 6px; }
|
|
633
634
|
|
|
634
635
|
@media (max-width: 700px) {
|
|
@@ -643,15 +644,13 @@
|
|
|
643
644
|
.vanillasky-video-chat .cards { gap: 12px; width: calc(100% + 20px); max-width: calc(100% + 20px); padding: 12px 20px 12px 12px; }
|
|
644
645
|
.vanillasky-video-chat .cards button { width: 172px; aspect-ratio: .95; }
|
|
645
646
|
.vanillasky-video-chat .card-prompt { font-size: 14px; padding: 14px; }
|
|
646
|
-
|
|
647
|
-
.vanillasky-video-chat .ending .cards button { width: 165px; aspect-ratio: 1.15; }
|
|
647
|
+
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
@media (max-height: 650px) and (min-width: 701px) {
|
|
651
651
|
.vanillasky-video-chat .welcome-body { padding-top: 110px; padding-bottom: 118px; }
|
|
652
652
|
.vanillasky-video-chat .welcome-title { font-size: clamp(32px, 4vw, 54px); }
|
|
653
653
|
.vanillasky-video-chat .cards button { width: 160px; }
|
|
654
|
-
.vanillasky-video-chat .ending-body { padding-bottom: 160px; }
|
|
655
654
|
}
|
|
656
655
|
|
|
657
656
|
@media (max-height: 650px) and (max-width: 700px) {
|
|
@@ -709,8 +708,6 @@
|
|
|
709
708
|
|
|
710
709
|
.vanillasky-video-chat .expanded-captions .eyebrow { color: var(--vs-media-muted); }
|
|
711
710
|
|
|
712
|
-
|
|
713
|
-
|
|
714
711
|
.vanillasky-video-chat .dock-hint { position: absolute; inset: auto 0 7px; text-align: center; margin: 0; color: var(--vs-media-muted); font-size: 11px; line-height: 16px; }
|
|
715
712
|
|
|
716
713
|
.vanillasky-video-chat .panel-inner:has(.dock-hint) { padding-bottom: max(38px, calc(env(safe-area-inset-bottom) + 20px)); }
|
|
@@ -784,7 +781,6 @@
|
|
|
784
781
|
|
|
785
782
|
.vanillasky-video-chat .composer-meta:empty { display: none; }
|
|
786
783
|
|
|
787
|
-
|
|
788
784
|
.vanillasky-video-chat .composer-meta .error { width: auto; }
|
|
789
785
|
|
|
790
786
|
@media (max-width: 700px) {
|
|
@@ -922,7 +918,6 @@
|
|
|
922
918
|
.vanillasky-video-chat .developer-links button { width: 100%; border: 0; background: transparent; text-align: left; cursor: pointer; font-family: inherit; }
|
|
923
919
|
.vanillasky-video-chat .developer-about p { margin: 0; padding: 2px 12px 12px; color: var(--vs-fg-muted); font-size: 12px; line-height: 1.6; }
|
|
924
920
|
|
|
925
|
-
|
|
926
921
|
.vanillasky-video-chat .history { display: grid; gap: 4px; }
|
|
927
922
|
|
|
928
923
|
.vanillasky-video-chat .history .section-label { padding-top: 8px; padding-bottom: 5px; }
|
|
@@ -943,7 +938,6 @@
|
|
|
943
938
|
|
|
944
939
|
.vanillasky-video-chat .history-row small { display: block; margin-top: 4px; font-size: 11px; font-weight: 400; color: #aeb2c7; }
|
|
945
940
|
|
|
946
|
-
|
|
947
941
|
@media (max-width: 700px) {
|
|
948
942
|
.vanillasky-video-chat .sheet-popover { right: 12px; width: min(360px, calc(100% - 24px)); max-height: calc(100dvh - 102px); padding: 16px; }
|
|
949
943
|
.vanillasky-video-chat .popover-heading .round { width: 44px; height: 44px; }
|
|
@@ -1028,7 +1022,6 @@
|
|
|
1028
1022
|
}
|
|
1029
1023
|
}
|
|
1030
1024
|
|
|
1031
|
-
|
|
1032
1025
|
/* Caption controls float outside the reading measure, so the complete cue
|
|
1033
1026
|
keeps the same width when controls appear or disappear. */
|
|
1034
1027
|
.vanillasky-video-chat .caption-actions {
|
|
@@ -1085,3 +1078,32 @@
|
|
|
1085
1078
|
.vanillasky-video-chat .media-credit { color: var(--vs-fg); font-size: 11px; margin-left: 12px; text-underline-offset: 3px; }
|
|
1086
1079
|
@keyframes vanillasky-chapter-enter { from { opacity: 0; } to { opacity: 1; } }
|
|
1087
1080
|
@media (prefers-reduced-motion: reduce) { .vanillasky-video-chat .opening-chapter { animation: none; } }
|
|
1081
|
+
|
|
1082
|
+
.vanillasky-video-chat .ending .cards li { min-width: 0; }
|
|
1083
|
+
|
|
1084
|
+
.vanillasky-video-chat .ending .cards button {
|
|
1085
|
+
display: flex;
|
|
1086
|
+
flex-direction: column;
|
|
1087
|
+
justify-content: flex-end;
|
|
1088
|
+
width: 100%;
|
|
1089
|
+
height: 100%;
|
|
1090
|
+
min-height: 180px;
|
|
1091
|
+
aspect-ratio: auto;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.vanillasky-video-chat .ending .card-prompt {
|
|
1095
|
+
position: relative;
|
|
1096
|
+
inset: auto;
|
|
1097
|
+
overflow-wrap: anywhere;
|
|
1098
|
+
font-size: clamp(14px, 1.2vw, 18px);
|
|
1099
|
+
line-height: 1.4;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
@container (min-width: 1200px) {
|
|
1103
|
+
.vanillasky-video-chat .ending .cards { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
1104
|
+
.vanillasky-video-chat .ending .cards button { min-height: 210px; }
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
@media (max-height: 650px) {
|
|
1108
|
+
.vanillasky-video-chat .ending-body { inset-block: 80px 180px; }
|
|
1109
|
+
}
|