видео-аватар пациента (волны/мимика/субтитры), бренд ПУЛЬС, PRD.md
This commit is contained in:
parent
803cabbde7
commit
38bee301c3
166
PRD.md
Normal file
166
PRD.md
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
# PRD · ПУЛЬС — Неотложка
|
||||||
|
|
||||||
|
**Игровой тренажёр клинических кейсов по неотложной медицинской помощи**
|
||||||
|
|
||||||
|
Версия документа: 1.0
|
||||||
|
Статус: реализовано (MVP), в итеративной доработке
|
||||||
|
Платформа: статический сайт (HTML + CSS + vanilla JS), публикуется через Git Pages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Концепция
|
||||||
|
|
||||||
|
**ПУЛЬС** — образовательный игровой тренажёр для студентов-медиков 6 курса. Студент работает с «говорящими» видео-пациентами: выслушивает жалобы голосом, самостоятельно назначает обследования и лечение, ставит диагноз и оказывает экстренную помощь на время. За точность начисляется опыт (XP), открываются достижения и уровни, результаты попадают в рейтинг. Алгоритмы решения основаны на клинических протоколах диагностики и лечения МЗ РК.
|
||||||
|
|
||||||
|
**Ценность:** превращает сухую зубрёжку протоколов в тренировку клинического мышления в условиях, близких к приёмному отделению, с мгновенной обратной связью и игровой мотивацией.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Целевая аудитория
|
||||||
|
|
||||||
|
- **Основная:** студенты 6 курса медицинских вузов (интерны, ординаторы) — отработка неотложных состояний.
|
||||||
|
- **Дополнительная:** врачи СМП и приёмного покоя для повторения алгоритмов; преподаватели — как материал для семинаров.
|
||||||
|
- **Контекст применения:** Казахстан (протоколы МЗ РК, русскоязычный интерфейс).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Цели и задачи
|
||||||
|
|
||||||
|
### Цели продукта
|
||||||
|
1. Дать практику клинического мышления по неотложным состояниям в безопасной среде.
|
||||||
|
2. Закрепить алгоритмы клинических протоколов МЗ РК через повторение.
|
||||||
|
3. Повысить мотивацию к обучению за счёт геймификации.
|
||||||
|
|
||||||
|
### Метрики успеха (MVP)
|
||||||
|
- Завершение полного экзамена (6 кейсов) за один заход — основная конверсия.
|
||||||
|
- Доля повторных прохождений кейсов (replay) — вовлечённость.
|
||||||
|
- Средний балл по кейсу/экзамену — уровень усвоения.
|
||||||
|
- Количество разблокированных достижений — глубина погружения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Функциональные требования
|
||||||
|
|
||||||
|
### 4.1. Профиль игрока и уровни
|
||||||
|
- Создание профиля: имя + аватар (emoji).
|
||||||
|
- Система опыта (XP): начисляется за результаты кейсов (пропорционально приросту лучшего балла).
|
||||||
|
- 8 уровней: Стажёр → Студент → Интерн → Ординатор → Врач → Специалист → Эксперт → Профессор.
|
||||||
|
- Шапка профиля: аватар, имя, текущий уровень, XP-бар до следующего уровня.
|
||||||
|
- Сохранение профиля и прогресса в браузере (localStorage).
|
||||||
|
|
||||||
|
### 4.2. Карта прохождения тем
|
||||||
|
- 3 темы: «ОКС и инфаркт», «ОДН · астма · ХОБЛ», «Шок и травма».
|
||||||
|
- По каждой теме: прогресс-бар (кейсов пройдено), заработанные звёзды.
|
||||||
|
- Клик по теме фильтрует кейсы и переносит к списку.
|
||||||
|
|
||||||
|
### 4.3. Кейсы
|
||||||
|
- 6 клинических кейсов (по 2 на тему).
|
||||||
|
- Карточка кейса: портрет-аватар (фото), тема, звёзды (1–3), лучший результат, счётчик прохождений, статус «Пройден», кнопка «Играть/Повторить».
|
||||||
|
|
||||||
|
### 4.4. Видео-пациент (говорящий аватар)
|
||||||
|
- В шаге «Пациент» — видеоплеер с крупным портретом пациента.
|
||||||
|
- При нажатии «Прослушать»: синтез речи (Web Speech API), портрет «оживает» — звуковые волны, лёгкая анимация говорения, индикатор LIVE.
|
||||||
|
- **Субтитры** синхронно с речью (по фразам).
|
||||||
|
- Голос подбирается под пациента: пол, возраст, состояние (pitch/rate/громкость), паузы-«дыхание» между фразами (у астматика — рваная речь с длинными паузами, у шокового — тихая и замедленная).
|
||||||
|
|
||||||
|
### 4.5. Пошаговое решение кейса (экзамен)
|
||||||
|
Каждый кейс = 4 шага + результат:
|
||||||
|
1. **Обследования** — множественный выбор из перечня (нужные vs лишние).
|
||||||
|
2. **Лечение** — множественный выбор препаратов (нужные vs противопоказанные).
|
||||||
|
3. **Диагноз** — выбор одного верного варианта.
|
||||||
|
4. **Экстренная помощь ⏱** — за 60 секунд выбрать правильные действия; таймер с обратным отсчётом, автозавершение при истечении.
|
||||||
|
|
||||||
|
Требования к выбору вариантов:
|
||||||
|
- Варианты кликабельны и доступны для нажатия на всех шагах.
|
||||||
|
- После «Подтвердить» варианты блокируются и подсвечиваются: верно / лишнее / пропущено / верный диагноз.
|
||||||
|
|
||||||
|
### 4.6. Оценка и обратная связь
|
||||||
|
- Балл за шаг множественного выбора: `(TP + TN) / всего × 100%`.
|
||||||
|
- Диагноз: 100% / 0%.
|
||||||
|
- Вес шагов в кейсе: обследования 20%, лечение 20%, диагноз 30%, экстренная помощь 30%.
|
||||||
|
- Экран результата кейса: кольцевой индикатор %, грейд (Отлично/Хорошо/Удовл./Неуд), разбивка по шагам, награды (звёзды, XP, новые достижения), разбор: правильный диагноз, **протокол МЗ РК**, ключевые данные, типичные ошибки.
|
||||||
|
|
||||||
|
### 4.7. Геймификация
|
||||||
|
- **Звёзды за кейс:** ≥85% → 3, ≥70% → 2, ≥55% → 1.
|
||||||
|
- **Достижения (11 шт.):** «Первый шаг», «Точный диагноз», «Безупречный кейс», «Чистая работа», «Молниеносная реакция», «Мастер ОКС/ОДН/шока», «Великий диагност», «Полное прохождение», «Профессор».
|
||||||
|
- **Тосты** при разблокировке достижений и повышении уровня.
|
||||||
|
|
||||||
|
### 4.8. Рейтинг игроков
|
||||||
|
- Топ-10 результатов полного экзамена (имя, аватар, %, дата).
|
||||||
|
- Сохраняется локально в браузере; результат текущего игрока подсвечен.
|
||||||
|
|
||||||
|
### 4.9. Полный экзамен
|
||||||
|
- Последовательное прохождение всех 6 кейсов.
|
||||||
|
- Итоговый экран: средний %, грейд, разбивка по темам и по кейсам.
|
||||||
|
- Результат добавляется в рейтинг.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Содержание кейсов (по протоколам МЗ РК)
|
||||||
|
|
||||||
|
| # | Тема | Кейс | Протокол МЗ РК |
|
||||||
|
|---|------|------|----------------|
|
||||||
|
| 1 | ОКС | Острый инфаркт миокарда с подъёмом ST (STEMI) | «Острый коронарный синдром» |
|
||||||
|
| 2 | ОКС | ОКС без подъёма ST (NSTEMI) | «Острый коронарный синдром» |
|
||||||
|
| 3 | ОДН | Острый тяжёлый приступ бронхиальной астмы | «Бронхиальная астма» |
|
||||||
|
| 4 | ОДН | Обострение ХОБЛ с ОДН 2 типа | «Хроническая обструктивная болезнь лёгких» |
|
||||||
|
| 5 | Шок | Травматический геморрагический шок | «Шок», «Острое кровотечение» |
|
||||||
|
| 6 | Шок | Септический шок | «Сепсис и септический шок» |
|
||||||
|
|
||||||
|
Ключевые положения протоколов, отражённые в кейсах:
|
||||||
|
- **STEMI:** первичное ЧКВ (door-to-balloon ≤ 90 мин); при недоступности ≤ 120 мин — фибринолизис, в т.ч. догоспитальный; двойная антитромбоцитарная терапия + антикоагулянт.
|
||||||
|
- **NSTEMI:** тромболизис НЕ показан; стратификация по GRACE; ранняя инвазивная стратегия.
|
||||||
|
- **Астма:** β2-агонисты + ипратропия бромид + системные ГКС + магний; седативные/опиоиды противопоказаны.
|
||||||
|
- **ХОБЛ:** контролируемая О₂-терапия (целевой SpO₂ 88–92%); NIV при гиперкапнии.
|
||||||
|
- **Геморрагический шок:** damage control, остановка кровотечения, гемотрансфузия 1:1:1, транексамовая кислота; борьба с триадой смерти.
|
||||||
|
- **Сепсис:** hour-1 bundle (лактат, гемокультуры, антибиотики в 1-й час, инфузия 30 мл/кг, норадреналин).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Дизайн
|
||||||
|
|
||||||
|
- **Стиль:** выдержанная классическая палитра — графит-navy, кремовый «бумажный» фон, приглушённое золото (акцент), бордо (ОКС), тёмно-бирюзовый (ОДН), кирпичный (шок), хирургический зелёный (верно). Строгий медицинский вид, без неон-яркости.
|
||||||
|
- **Типографика:** Manrope.
|
||||||
|
- **Адаптив:** desktop / планшет / мобильный.
|
||||||
|
- **Иконки:** emoji и inline-элементы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Технологии и ограничения
|
||||||
|
|
||||||
|
- **Стек:** статические `index.html` + `style.css` + `app.js` (vanilla JS). Без бэкенда, БД, сборщиков, аутентификации.
|
||||||
|
- **Озвучка:** Web Speech API (SpeechSynthesis), русские голоса браузера.
|
||||||
|
- **Хранение:** localStorage (профиль, прогресс, рейтинг).
|
||||||
|
- **Публикация:** Git Pages, ветка `pages`.
|
||||||
|
- **Ограничения песочницы:** только статика; реальные фото/видео пациентов не используются (этика) — применяется анимированный видео-аватар.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Пользовательский путь
|
||||||
|
|
||||||
|
1. Открывает сайт → видит шапку профиля и hero с уровнем/XP.
|
||||||
|
2. Создаёт профиль (имя + аватар) через ⚙️.
|
||||||
|
3. Заходит в «Карту прохождения» или сразу в «Кейсы».
|
||||||
|
4. Выбирает кейс → видеопациент рассказывает жалобы голосом.
|
||||||
|
5. Проходит 4 шага (обследования → лечение → диагноз → экстренная помощь на 60 с).
|
||||||
|
6. Получает результат: %, звёзды, XP, достижения, разбор с протоколом МЗ РК.
|
||||||
|
7. Идёт к следующему кейсу или запускает полный экзамен → попадает в рейтинг.
|
||||||
|
8. Растёт в уровне от Стажёра до Профессора.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Roadmap (возможные доработки)
|
||||||
|
|
||||||
|
- Расширение тем: кома и неврология, острый живот, отравления, анафилаксия.
|
||||||
|
- Режим «Экзамен с таймером на весь кейс».
|
||||||
|
- Экспорт результата / сертификат прохождения.
|
||||||
|
- Импорт своих медиа (фото/видео пациентов) для преподавателя.
|
||||||
|
- Поддержка казахского языка интерфейса.
|
||||||
|
- Синхронизация прогресса между устройствами (требует бэкенда — вне песочницы).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Ссылки
|
||||||
|
|
||||||
|
- Сайт: https://pages.git.vibe42.kz/saniya_akpanova_91/emergency-cases/
|
||||||
|
- Репозиторий: https://git.vibe42.kz/saniya_akpanova_91/emergency-cases
|
||||||
44
app.js
44
app.js
@ -469,22 +469,24 @@ function pickVoice(preferMale){
|
|||||||
return ru[0];
|
return ru[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
let speakState={playing:false, queue:[], idx:0, timer:null, btn:null, avatar:null, opts:null};
|
let speakState={playing:false, queue:[], idx:0, timer:null, btn:null, avatar:null, opts:null, player:null, subsEl:null, onPhrase:null, onEnd:null};
|
||||||
|
|
||||||
function stopSpeak(){
|
function stopSpeak(){
|
||||||
if("speechSynthesis" in window) window.speechSynthesis.cancel();
|
if("speechSynthesis" in window) window.speechSynthesis.cancel();
|
||||||
if(speakState.timer){ clearTimeout(speakState.timer); speakState.timer=null; }
|
if(speakState.timer){ clearTimeout(speakState.timer); speakState.timer=null; }
|
||||||
speakState.playing=false;
|
speakState.playing=false;
|
||||||
if(speakState.btn){ speakState.btn.classList.remove("is-playing"); speakState.btn.textContent="▶ Прослушать"; }
|
if(speakState.btn){ speakState.btn.classList.remove("is-playing"); speakState.btn.textContent="▶ Прослушать жалобы"; }
|
||||||
if(speakState.avatar) speakState.avatar.classList.remove("is-speaking");
|
if(speakState.avatar) speakState.avatar.classList.remove("is-speaking");
|
||||||
speakState.queue=[]; speakState.idx=0;
|
if(speakState.player) speakState.player.classList.remove("is-speaking");
|
||||||
|
if(speakState.onEnd){ const cb=speakState.onEnd; speakState.onEnd=null; cb(); }
|
||||||
|
speakState.queue=[]; speakState.idx=0; speakState.player=null; speakState.subsEl=null; speakState.onPhrase=null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitPhrases(text){
|
function splitPhrases(text){
|
||||||
return text.split(/(?<=[.!?…])\s+|(?<=…)\s*/).map(s=>s.trim()).filter(Boolean);
|
return text.split(/(?<=[.!?…])\s+|(?<=…)\s*/).map(s=>s.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function speakPatient(text, opts, btn, avatar){
|
function speakPatient(text, opts, btn, avatar, ext){
|
||||||
const synth=window.speechSynthesis;
|
const synth=window.speechSynthesis;
|
||||||
if(!synth){ return; }
|
if(!synth){ return; }
|
||||||
if(speakState.playing){ stopSpeak(); return; }
|
if(speakState.playing){ stopSpeak(); return; }
|
||||||
@ -492,9 +494,14 @@ function speakPatient(text, opts, btn, avatar){
|
|||||||
const phrases=splitPhrases(text);
|
const phrases=splitPhrases(text);
|
||||||
if(!phrases.length) return;
|
if(!phrases.length) return;
|
||||||
speakState.queue=phrases; speakState.idx=0; speakState.btn=btn; speakState.avatar=avatar; speakState.opts=opts;
|
speakState.queue=phrases; speakState.idx=0; speakState.btn=btn; speakState.avatar=avatar; speakState.opts=opts;
|
||||||
|
speakState.player=ext&&ext.player||null;
|
||||||
|
speakState.subsEl=ext&&ext.subsEl||null;
|
||||||
|
speakState.onPhrase=ext&&ext.onPhrase||null;
|
||||||
|
speakState.onEnd=ext&&ext.onEnd||null;
|
||||||
speakState.playing=true;
|
speakState.playing=true;
|
||||||
btn.classList.add("is-playing"); btn.textContent="⏹ Остановить";
|
btn.classList.add("is-playing"); btn.textContent="⏹ Остановить";
|
||||||
if(avatar) avatar.classList.add("is-speaking");
|
if(avatar) avatar.classList.add("is-speaking");
|
||||||
|
if(speakState.player) speakState.player.classList.add("is-speaking");
|
||||||
playNext();
|
playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,6 +509,8 @@ function playNext(){
|
|||||||
const synth=window.speechSynthesis;
|
const synth=window.speechSynthesis;
|
||||||
if(!speakState.playing || speakState.idx>=speakState.queue.length){ stopSpeak(); return; }
|
if(!speakState.playing || speakState.idx>=speakState.queue.length){ stopSpeak(); return; }
|
||||||
const phrase=speakState.queue[speakState.idx];
|
const phrase=speakState.queue[speakState.idx];
|
||||||
|
if(speakState.subsEl) speakState.subsEl.textContent=phrase;
|
||||||
|
if(speakState.onPhrase) speakState.onPhrase(phrase, speakState.idx);
|
||||||
const opts=speakState.opts||{};
|
const opts=speakState.opts||{};
|
||||||
const u=new SpeechSynthesisUtterance(phrase);
|
const u=new SpeechSynthesisUtterance(phrase);
|
||||||
u.lang="ru-RU";
|
u.lang="ru-RU";
|
||||||
@ -869,10 +878,18 @@ function patientBlock(c){
|
|||||||
<div class="patient-badges"><span class="badge ${c.badge}">${c.themeLabel}</span></div>
|
<div class="patient-badges"><span class="badge ${c.badge}">${c.themeLabel}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="speak-bar">
|
<div class="vplayer" id="vplayer">
|
||||||
<span class="speak-ico">🔊</span>
|
<div class="vp-screen">
|
||||||
<span class="speak-text"><b>Жалобы пациента</b><small>Включите звук и нажмите — пациент говорит голосом</small></span>
|
<div class="vp-waves"><i></i><i></i><i></i></div>
|
||||||
<button class="speak-btn" id="speakBtn">▶ Прослушать</button>
|
<div class="vp-avatar">${c.patient.avatar}</div>
|
||||||
|
<span class="vp-live">● ВИДЕО · LIVE</span>
|
||||||
|
<span class="vp-photo">📷 Фото пациента</span>
|
||||||
|
</div>
|
||||||
|
<div class="vp-subs" id="vpSubs">Нажмите «Прослушать», чтобы пациент рассказал жалобы</div>
|
||||||
|
<div class="vp-ctrl">
|
||||||
|
<button class="speak-btn" id="speakBtn">▶ Прослушать жалобы</button>
|
||||||
|
<span class="vp-hint">🔊 Включите звук на устройстве</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="complaint">«${c.complaint}»</div>
|
<div class="complaint">«${c.complaint}»</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -888,8 +905,15 @@ function vitalsHTML(v){
|
|||||||
${extra}</div>`;
|
${extra}</div>`;
|
||||||
}
|
}
|
||||||
function bindSpeak(c, root){
|
function bindSpeak(c, root){
|
||||||
const btn=root.querySelector("#speakBtn"), av=root.querySelector("#pAvatar");
|
const btn=root.querySelector("#speakBtn"),
|
||||||
if(btn) btn.addEventListener("click",()=>speakPatient(c.complaint, c.patient.voice, btn, av));
|
av=root.querySelector(".vp-avatar"),
|
||||||
|
player=root.querySelector("#vplayer"),
|
||||||
|
subs=root.querySelector("#vpSubs");
|
||||||
|
if(!btn) return;
|
||||||
|
btn.addEventListener("click",()=>speakPatient(c.complaint, c.patient.voice, btn, av, {
|
||||||
|
player, subsEl:subs,
|
||||||
|
onEnd:()=>{ if(subs) subs.textContent="Нажмите «Прослушать», чтобы пациент рассказал жалобы"; }
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPatient(){
|
function renderPatient(){
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>МедИгра · Неотложка — тренажёр для медиков</title>
|
<title>ПУЛЬС · Неотложка — игровой тренажёр для медиков</title>
|
||||||
<meta name="description" content="Игровой тренажёр клинических кейсов по неотложной помощи: говорящие пациенты, уровни, опыт, достижения и рейтинг. По протоколам МЗ РК.">
|
<meta name="description" content="ПУЛЬС — игровой тренажёр клинических кейсов по неотложной помощи: говорящие видео-пациенты, уровни, опыт, достижения и рейтинг. По протоколам МЗ РК.">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&display=swap" rel="stylesheet">
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<!-- ИГРОВАЯ ШАПКА -->
|
<!-- ИГРОВАЯ ШАПКА -->
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div class="container topbar-in">
|
<div class="container topbar-in">
|
||||||
<a class="brand" href="#top">🩺 МедИгра<span>Неотложка</span></a>
|
<a class="brand" href="#top">🫀 ПУЛЬС<span>Неотложка</span></a>
|
||||||
<div class="player" id="playerBar"></div>
|
<div class="player" id="playerBar"></div>
|
||||||
<div class="topbar-actions">
|
<div class="topbar-actions">
|
||||||
<button class="tbtn" id="btnAchv" title="Достижения">🏅</button>
|
<button class="tbtn" id="btnAchv" title="Достижения">🏅</button>
|
||||||
@ -92,7 +92,7 @@
|
|||||||
<!-- FOOTER -->
|
<!-- FOOTER -->
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p><b>МедИгра · Неотложка</b> — игровой тренажёр для студентов-медиков. Алгоритмы основаны на клинических протоколах диагностики и лечения МЗ РК. Материалы носят обучающий характер.</p>
|
<p><b>ПУЛЬС · Неотложка</b> — игровой тренажёр для студентов-медиков. Алгоритмы основаны на клинических протоколах диагностики и лечения МЗ РК. Материалы носят обучающий характер.</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
21
style.css
21
style.css
@ -280,6 +280,26 @@ h1,h2,h3{letter-spacing:-.4px}
|
|||||||
.speak-btn.is-playing{background:var(--red)}
|
.speak-btn.is-playing{background:var(--red)}
|
||||||
.complaint{background:rgba(176,141,87,.12);border-left:4px solid var(--gold);border-radius:0 12px 12px 0;padding:16px 18px;font-size:16.5px;line-height:1.7;margin-top:16px;font-style:italic;color:#dfe5ec}
|
.complaint{background:rgba(176,141,87,.12);border-left:4px solid var(--gold);border-radius:0 12px 12px 0;padding:16px 18px;font-size:16.5px;line-height:1.7;margin-top:16px;font-style:italic;color:#dfe5ec}
|
||||||
|
|
||||||
|
/* видео-аватар пациента */
|
||||||
|
.vplayer{background:#0f1828;border-radius:var(--radius-lg);overflow:hidden;margin-bottom:18px;box-shadow:var(--shadow)}
|
||||||
|
.vp-screen{position:relative;aspect-ratio:16/10;background:radial-gradient(circle at 50% 38%,#2a3a52,#101a2c);display:grid;place-items:center;overflow:hidden}
|
||||||
|
.vp-waves{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none}
|
||||||
|
.vp-waves i{position:absolute;width:130px;height:130px;border-radius:50%;border:2px solid rgba(176,141,87,.5);opacity:0}
|
||||||
|
.vplayer.is-speaking .vp-waves i{animation:vpwave 1.7s ease-out infinite}
|
||||||
|
.vplayer.is-speaking .vp-waves i:nth-child(2){animation-delay:.55s}
|
||||||
|
.vplayer.is-speaking .vp-waves i:nth-child(3){animation-delay:1.1s}
|
||||||
|
@keyframes vpwave{0%{transform:scale(.55);opacity:.7}100%{transform:scale(2.3);opacity:0}}
|
||||||
|
.vp-avatar{font-size:104px;line-height:1;position:relative;z-index:2;filter:drop-shadow(0 10px 18px rgba(0,0,0,.45))}
|
||||||
|
.vplayer.is-speaking .vp-avatar{animation:vptalk .45s ease-in-out infinite alternate}
|
||||||
|
@keyframes vptalk{0%{transform:scale(1) translateY(0)}100%{transform:scale(1.05) translateY(-4px)}}
|
||||||
|
.vp-live{position:absolute;top:12px;left:12px;background:rgba(0,0,0,.45);color:#fff;font-size:11px;font-weight:800;letter-spacing:.5px;padding:4px 10px;border-radius:6px;z-index:3}
|
||||||
|
.vplayer.is-speaking .vp-live{color:#ff6b6b}
|
||||||
|
.vp-photo{position:absolute;top:12px;right:12px;background:rgba(0,0,0,.45);color:#cfd6e0;font-size:11px;font-weight:700;padding:4px 10px;border-radius:6px;z-index:3}
|
||||||
|
.vp-subs{background:rgba(0,0,0,.6);color:#fff;padding:12px 16px;font-size:14.5px;line-height:1.45;min-height:50px;text-align:center;display:flex;align-items:center;justify-content:center}
|
||||||
|
.vp-ctrl{padding:12px 16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;background:#0c1422}
|
||||||
|
.vp-hint{color:#9ba6b4;font-size:13px}
|
||||||
|
.vp-ctrl .speak-btn{margin-left:auto}
|
||||||
|
|
||||||
/* аккордеон */
|
/* аккордеон */
|
||||||
.acc{border:1px solid var(--line);border-radius:var(--radius);margin-bottom:12px;overflow:hidden;background:var(--white)}
|
.acc{border:1px solid var(--line);border-radius:var(--radius);margin-bottom:12px;overflow:hidden;background:var(--white)}
|
||||||
.acc summary{list-style:none;cursor:pointer;padding:15px 18px;font-weight:800;font-size:15.5px;display:flex;align-items:center;gap:10px;background:var(--paper);user-select:none}
|
.acc summary{list-style:none;cursor:pointer;padding:15px 18px;font-weight:800;font-size:15.5px;display:flex;align-items:center;gap:10px;background:var(--paper);user-select:none}
|
||||||
@ -430,4 +450,5 @@ h1,h2,h3{letter-spacing:-.4px}
|
|||||||
.hero-stats{gap:20px}
|
.hero-stats{gap:20px}
|
||||||
.exam-bar,.exam-stage-track,.exam-body{padding-left:16px;padding-right:16px}
|
.exam-bar,.exam-stage-track,.exam-body{padding-left:16px;padding-right:16px}
|
||||||
.toasts{right:12px;left:12px;max-width:none}
|
.toasts{right:12px;left:12px;max-width:none}
|
||||||
|
.vp-avatar{font-size:72px}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user