1
0
Fork 0

fix colors, add new case

This commit is contained in:
koplenov 2026-04-05 06:23:32 +03:00
parent bbd8620e94
commit 042f282348
8 changed files with 1880 additions and 163 deletions

View file

@ -4,13 +4,14 @@ import Case1Desktop from './cases/Case1Desktop';
import Case2Desktop from './cases/Case2Desktop';
import Case3Desktop from './cases/Case3Desktop';
import Case4Desktop from './cases/Case4Desktop';
import Case5Desktop from './cases/Case5Desktop';
import ChatterboxTTS from './components/deepfake/ChatterboxTTS';
import MyQuize from './components/MyQuize';
import CyberSecurityArticle from './components/CyberSecurityArticle';
export type WallpaperType = 'xp' | 'win7' | 'win10';
type OverlayType = 'case1' | 'case2' | 'case3' | 'case4' | 'deepfake' | 'quiz' | 'wiki' | null;
type OverlayType = 'case1' | 'case2' | 'case3' | 'case4' | 'case5' | 'deepfake' | 'quiz' | 'wiki' | null;
const App: React.FC = () => {
const [overlay, setOverlay] = useState<OverlayType>(null);
@ -23,6 +24,7 @@ const App: React.FC = () => {
2: 'case2',
3: 'case3',
4: 'case4',
5: 'case5',
'deepfake': 'deepfake',
};
setTimeout(() => setOverlay(map[level] ?? null), 1200);
@ -56,6 +58,7 @@ const App: React.FC = () => {
{overlay === 'case2' && <Case2Desktop onComplete={close} />}
{overlay === 'case3' && <Case3Desktop onComplete={close} />}
{overlay === 'case4' && <Case4Desktop onComplete={close} />}
{overlay === 'case5' && <Case5Desktop onComplete={close} />}
{overlay === 'deepfake' && <ChatterboxTTS />}
{overlay === 'quiz' && <MyQuize />}
{overlay === 'wiki' && <CyberSecurityArticle />}

View file

@ -1,19 +1,456 @@
import React, { useState } from 'react';
import type { WallpaperType } from '../App';
import MainApp from '../MainApp';
// import './Case3Desktop.css';
interface Case3DesktopProps {
onComplete: (type: WallpaperType) => void;
}
type Page =
| 'home'
| 'mail-inbox'
| 'mail-real'
| 'mail-phishing'
| 'fake-gosuslugi'
| 'gosuslugi';
const PAGE_URL: Record<Page, { url: string; secure: boolean }> = {
'home': { url: 'yandex.ru', secure: true },
'mail-inbox': { url: 'mail.yandex.ru', secure: true },
'mail-real': { url: 'mail.yandex.ru', secure: true },
'mail-phishing': { url: 'mail.yandex.ru', secure: true },
'fake-gosuslugi': { url: 'g0suslugi-confirm.ru/login', secure: false },
'gosuslugi': { url: 'gosuslugi.ru', secure: true },
};
const Case3Desktop: React.FC<Case3DesktopProps> = ({ onComplete }) => {
const [page, setPage] = useState<Page>('home');
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
const [modal, setModal] = useState<'stolen' | 'success' | null>(null);
const { url, secure } = PAGE_URL[page];
const goTo = (p: Page) => { setPage(p); setLogin(''); setPassword(''); };
return (
<div>
<MainApp showGosuslugi={true} showAmnezia={false}/>
<div style={s.root}>
{/* Браузер */}
<div style={s.browser}>
{/* Хром */}
<div style={s.chrome}>
<button style={s.navBtn} onClick={() => goTo('home')}></button>
<button style={s.navBtn} onClick={() => {
if (page === 'fake-gosuslugi') goTo('mail-phishing');
else if (page === 'mail-real' || page === 'mail-phishing') goTo('mail-inbox');
else if (page === 'mail-inbox' || page === 'gosuslugi') goTo('home');
}}></button>
<div style={s.urlBar}>
<span style={secure ? s.httpsIcon : s.httpIcon}>{secure ? '🔒' : '⚠'}</span>
<span style={{ ...s.urlText, color: secure ? '#aaa' : '#f5a623' }}>{url}</span>
</div>
</div>
{/* Закладки */}
<div style={s.bookmarks}>
<button style={s.bookmark} onClick={() => goTo('mail-inbox')}>📧 Почта</button>
<button style={s.bookmark} onClick={() => goTo('gosuslugi')}>🏛 Госуслуги</button>
</div>
{/* Контент */}
<div style={s.content}>
{page === 'home' && <HomePage onGo={goTo} />}
{page === 'mail-inbox' && <MailInbox onOpen={goTo} />}
{page === 'mail-real' && <EmailReal onBack={() => goTo('mail-inbox')} onGosuslugi={() => goTo('gosuslugi')} />}
{page === 'mail-phishing' && <EmailPhishing onBack={() => goTo('mail-inbox')} onLink={() => goTo('fake-gosuslugi')} />}
{page === 'fake-gosuslugi' && (
<FakeGosuslugi
login={login} password={password}
setLogin={setLogin} setPassword={setPassword}
onSubmit={() => setModal('stolen')}
/>
)}
{page === 'gosuslugi' && (
<RealGosuslugi
login={login} password={password}
setLogin={setLogin} setPassword={setPassword}
onSubmit={() => setModal('success')}
/>
)}
</div>
</div>
{/* Модалки */}
{modal === 'stolen' && (
<div style={s.modalOverlay}>
<div style={{ ...s.modal, borderColor: '#FF0040' }}>
<div style={s.modalIcon}>💀</div>
<div style={{ ...s.modalTitle, color: '#FF0040' }}>Аккаунт украден!</div>
<div style={s.modalText}>
Вы ввели логин и пароль на <b style={{ color: '#FF0040' }}>фишинговом сайте</b>.<br/>
Домен <b>g0suslugi-confirm.ru</b> подделка под госуслуги.<br/>
Буква <b>o</b> заменена на цифру <b>0</b>.<br/><br/>
Злоумышленник получил ваши данные и вошёл в ваш аккаунт.
</div>
<div style={s.modalHint}>
💡 Правило: никогда не переходите по ссылкам из писем. Открывайте госуслуги только из закладок.
</div>
<button style={{ ...s.modalBtn, background: 'rgba(255,0,64,0.15)', border: '1px solid #FF0040', color: '#FF0040' }}
onClick={() => { setModal(null); goTo('mail-phishing'); }}>
Попробовать ещё раз
</button>
</div>
</div>
)}
{modal === 'success' && (
<div style={s.modalOverlay}>
<div style={{ ...s.modal, borderColor: '#00FF41' }}>
<div style={s.modalIcon}>🛡</div>
<div style={{ ...s.modalTitle, color: '#00FF41' }}>Всё верно!</div>
<div style={s.modalText}>
Вы открыли <b style={{ color: '#00FF41' }}>официальный сайт</b> госуслуг напрямую из закладок.<br/>
Домен <b>gosuslugi.ru</b> настоящий, соединение защищено HTTPS.<br/><br/>
Ваши данные в безопасности.
</div>
<button style={{ ...s.modalBtn, background: '#00FF41', border: 'none', color: '#000' }}
onClick={() => { setModal(null); onComplete('win10'); }}>
Завершить кейс
</button>
</div>
</div>
)}
</div>
);
};
// ── Страницы ─────────────────────────────────────────
const HomePage: React.FC<{ onGo: (p: Page) => void }> = ({ onGo }) => (
<div style={s.homePage}>
<div style={s.yandexLogo}>Яндекс</div>
<div style={s.homeHint}>Используйте закладки выше или откройте почту для проверки письма</div>
<div style={s.homeIcons}>
<button style={s.homeIcon} onClick={() => onGo('mail-inbox')}>
<span style={{ fontSize: 32 }}>📧</span>
<span>Почта</span>
</button>
<button style={s.homeIcon} onClick={() => onGo('gosuslugi')}>
<span style={{ fontSize: 32 }}>🏛</span>
<span>Госуслуги</span>
</button>
</div>
</div>
);
const MailInbox: React.FC<{ onOpen: (p: Page) => void }> = ({ onOpen }) => (
<div style={s.mailPage}>
<div style={s.mailHeader}>Входящие <span style={{ color: '#00FFFF', fontSize: 12 }}>2 новых</span></div>
{/* Фишинговое письмо — выглядит срочно, идёт первым */}
<div style={{ ...s.emailRow, borderLeft: '3px solid #f5a623' }} onClick={() => onOpen('mail-phishing')}>
<div style={s.emailFrom}>
<span style={s.emailSender}>security@g0suslugi-confirm.ru</span>
<span style={s.emailTime}>10:47</span>
</div>
<div style={s.emailSubject}> Требуется подтверждение аккаунта</div>
<div style={s.emailPreview}>Ваш аккаунт будет заблокирован. Подтвердите данные в течение 24 часов...</div>
</div>
{/* Настоящее письмо */}
<div style={{ ...s.emailRow, borderLeft: '3px solid rgba(0,255,255,0.3)' }} onClick={() => onOpen('mail-real')}>
<div style={s.emailFrom}>
<span style={s.emailSender}>noreply@gosuslugi.ru</span>
<span style={s.emailTime}>09:12</span>
</div>
<div style={s.emailSubject}>Ваша заявка 2847 отклонена</div>
<div style={s.emailPreview}>Заявка на получение справки была отклонена в связи с неполным пакетом...</div>
</div>
</div>
);
const EmailReal: React.FC<{ onBack: () => void; onGosuslugi: () => void }> = ({ onBack, onGosuslugi }) => (
<div style={s.emailPage}>
<button style={s.backLink} onClick={onBack}> Входящие</button>
<div style={s.emailFullFrom}>
<b>От:</b> <span style={{ color: '#00FF41' }}>noreply@gosuslugi.ru</span>
</div>
<div style={s.emailFullSubject}>Ваша заявка 2847 отклонена</div>
<div style={s.emailBody}>
<p>Уважаемый пользователь,</p>
<p>Ваша заявка на получение справки об отсутствии задолженностей (<b>2847</b>) была отклонена.</p>
<p><b>Причина:</b> неполный пакет документов. Отсутствует копия паспорта.</p>
<p>Для повторной подачи заявки войдите на портал Госуслуги.</p>
<p style={{ color: '#888', fontSize: 12 }}>Это автоматическое сообщение, не отвечайте на него.</p>
</div>
<div style={s.emailActions}>
<span style={{ color: '#888', fontSize: 13 }}>Нужно войти на Госуслуги для повторной подачи</span>
<button style={s.actionBtn} onClick={onGosuslugi}>Открыть Госуслуги из закладок </button>
</div>
</div>
);
const EmailPhishing: React.FC<{ onBack: () => void; onLink: () => void }> = ({ onBack, onLink }) => (
<div style={s.emailPage}>
<button style={s.backLink} onClick={onBack}> Входящие</button>
<div style={{ ...s.emailFullFrom, color: '#f5a623' }}>
<b>От:</b> <span style={{ color: '#f5a623' }}>security@g0suslugi-confirm.ru</span>
<span style={s.suspiciousBadge}> Подозрительный отправитель</span>
</div>
<div style={s.emailFullSubject}> Требуется срочное подтверждение аккаунта</div>
<div style={s.emailBody}>
<p>Уважаемый пользователь,</p>
<p>Мы зафиксировали <b style={{ color: '#FF0040' }}>подозрительную активность</b> в вашем аккаунте.</p>
<p>В целях безопасности ваш аккаунт будет <b style={{ color: '#FF0040' }}>заблокирован через 24 часа</b>, если вы не подтвердите личность.</p>
<p>Нажмите кнопку ниже для подтверждения:</p>
<button style={s.phishingLink} onClick={onLink}>
Подтвердить аккаунт
</button>
<p style={{ color: '#555', fontSize: 11, marginTop: 20 }}>
© Портал государственных услуг. Если вы не запрашивали это письмо проигнорируйте его.
</p>
</div>
</div>
);
const FakeGosuslugi: React.FC<{
login: string; password: string;
setLogin: (v: string) => void; setPassword: (v: string) => void;
onSubmit: () => void;
}> = ({ login, password, setLogin, setPassword, onSubmit }) => (
<div style={s.gosPage}>
<div style={s.fakeWarning}> HTTP незащищённое соединение</div>
<div style={s.gosHeader}>
<span style={{ fontSize: 28 }}>🏛</span>
<span style={s.gosTitle}>Госуслуги</span>
</div>
<div style={s.gosSubtitle}>Подтверждение личности</div>
<div style={s.loginForm}>
<input style={s.input} placeholder="Логин или телефон" value={login} onChange={e => setLogin(e.target.value)} />
<input style={s.input} type="password" placeholder="Пароль" value={password} onChange={e => setPassword(e.target.value)} />
<button style={{ ...s.loginBtn, background: '#f5a623', color: '#000' }}
onClick={() => { if (login && password) onSubmit(); }}
disabled={!login || !password}>
Подтвердить
</button>
</div>
<div style={{ color: '#555', fontSize: 11, textAlign: 'center', marginTop: 12 }}>
g0suslugi-confirm.ru
</div>
</div>
);
const RealGosuslugi: React.FC<{
login: string; password: string;
setLogin: (v: string) => void; setPassword: (v: string) => void;
onSubmit: () => void;
}> = ({ login, password, setLogin, setPassword, onSubmit }) => (
<div style={s.gosPage}>
<div style={s.realSafe}>🔒 Безопасное соединение · gosuslugi.ru</div>
<div style={s.gosHeader}>
<span style={{ fontSize: 28 }}>🏛</span>
<span style={s.gosTitle}>Госуслуги</span>
</div>
<div style={s.gosSubtitle}>Войдите в личный кабинет</div>
<div style={s.loginForm}>
<input style={s.input} placeholder="Логин или телефон" value={login} onChange={e => setLogin(e.target.value)} />
<input style={s.input} type="password" placeholder="Пароль" value={password} onChange={e => setPassword(e.target.value)} />
<button style={{ ...s.loginBtn, background: '#00FFFF', color: '#000' }}
onClick={() => { if (login && password) onSubmit(); }}
disabled={!login || !password}>
Войти
</button>
</div>
<div style={{ color: '#555', fontSize: 11, textAlign: 'center', marginTop: 12 }}>
gosuslugi.ru · Официальный портал
</div>
</div>
);
// ── Стили ─────────────────────────────────────────────
const s: Record<string, React.CSSProperties> = {
root: {
minHeight: '100vh',
background: '#0a0a0a',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '24px 16px',
fontFamily: "'Share Tech Mono', monospace",
},
browser: {
width: '100%',
maxWidth: 800,
background: '#0d0d22',
border: '1px solid rgba(0,255,255,0.2)',
borderRadius: 10,
overflow: 'hidden',
boxShadow: '0 0 40px rgba(0,255,255,0.08)',
},
chrome: {
background: '#080818',
borderBottom: '1px solid rgba(0,255,255,0.15)',
padding: '10px 12px',
display: 'flex',
alignItems: 'center',
gap: 8,
},
navBtn: {
background: 'transparent',
border: '1px solid rgba(255,255,255,0.1)',
color: '#888',
borderRadius: 4,
padding: '4px 8px',
cursor: 'pointer',
fontSize: 14,
},
urlBar: {
flex: 1,
background: '#0a0a1a',
border: '1px solid rgba(0,255,255,0.15)',
borderRadius: 4,
padding: '6px 12px',
display: 'flex',
alignItems: 'center',
gap: 8,
fontSize: 13,
},
httpsIcon: { color: '#00FF41', fontSize: 14 },
httpIcon: { color: '#f5a623', fontSize: 14 },
urlText: { flex: 1 },
bookmarks: {
background: '#060612',
borderBottom: '1px solid rgba(0,255,255,0.08)',
padding: '6px 12px',
display: 'flex',
gap: 8,
},
bookmark: {
background: 'transparent',
border: '1px solid rgba(0,255,255,0.15)',
color: '#888',
borderRadius: 4,
padding: '3px 10px',
cursor: 'pointer',
fontSize: 12,
fontFamily: "'Share Tech Mono', monospace",
},
content: { minHeight: 420 },
// Home
homePage: {
display: 'flex', flexDirection: 'column', alignItems: 'center',
justifyContent: 'center', padding: 40, gap: 20,
},
yandexLogo: {
fontSize: 48, fontWeight: 900, color: '#FF0040',
fontFamily: "'Orbitron', monospace", letterSpacing: 2,
},
homeHint: { color: '#555', fontSize: 13, textAlign: 'center' },
homeIcons: { display: 'flex', gap: 32, marginTop: 16 },
homeIcon: {
background: 'rgba(0,255,255,0.04)', border: '1px solid rgba(0,255,255,0.15)',
borderRadius: 10, padding: '16px 24px', cursor: 'pointer',
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: 8, color: '#aaa', fontSize: 13, fontFamily: "'Share Tech Mono', monospace",
},
// Mail
mailPage: { padding: 0 },
mailHeader: {
padding: '14px 20px', borderBottom: '1px solid rgba(0,255,255,0.1)',
color: '#ccc', fontFamily: "'Orbitron', monospace", fontSize: 14,
display: 'flex', alignItems: 'center', gap: 10,
},
emailRow: {
padding: '14px 20px', borderBottom: '1px solid rgba(255,255,255,0.05)',
cursor: 'pointer', transition: 'background 0.15s',
},
emailFrom: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 },
emailSender: { color: '#aaa', fontSize: 13, fontWeight: 'bold' },
emailTime: { color: '#555', fontSize: 12 },
emailSubject: { color: '#ddd', fontSize: 14, marginBottom: 3 },
emailPreview: { color: '#555', fontSize: 12 },
// Email view
emailPage: { padding: 20 },
backLink: {
background: 'transparent', border: 'none', color: '#00FFFF',
cursor: 'pointer', fontSize: 13, padding: 0, marginBottom: 16,
fontFamily: "'Share Tech Mono', monospace",
},
emailFullFrom: { fontSize: 13, color: '#888', marginBottom: 6, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
emailFullSubject: { color: '#ddd', fontSize: 16, fontWeight: 'bold', marginBottom: 16, fontFamily: "'Orbitron', monospace" },
emailBody: { color: '#aaa', fontSize: 14, lineHeight: 1.8, marginBottom: 20 },
suspiciousBadge: {
background: 'rgba(245,166,35,0.12)', border: '1px solid #f5a623',
color: '#f5a623', fontSize: 11, borderRadius: 4, padding: '2px 8px',
},
phishingLink: {
background: 'rgba(0,120,255,0.15)', border: '1px solid #0078ff',
color: '#4da6ff', borderRadius: 6, padding: '10px 20px',
cursor: 'pointer', fontSize: 14, fontFamily: "'Share Tech Mono', monospace",
display: 'block', marginTop: 8,
},
emailActions: {
borderTop: '1px solid rgba(0,255,255,0.1)',
paddingTop: 16, display: 'flex', flexDirection: 'column', gap: 10,
},
actionBtn: {
background: '#00FFFF', border: 'none', borderRadius: 6,
padding: '10px 20px', color: '#000', cursor: 'pointer',
fontSize: 13, fontFamily: "'Orbitron', monospace", fontWeight: 700,
alignSelf: 'flex-start',
},
// Gosuslugi pages
gosPage: { padding: 32, display: 'flex', flexDirection: 'column', alignItems: 'center' },
fakeWarning: {
background: 'rgba(245,166,35,0.12)', border: '1px solid #f5a623',
color: '#f5a623', borderRadius: 6, padding: '8px 16px',
fontSize: 12, marginBottom: 24, width: '100%', textAlign: 'center',
},
realSafe: {
background: 'rgba(0,255,65,0.08)', border: '1px solid #00FF41',
color: '#00FF41', borderRadius: 6, padding: '8px 16px',
fontSize: 12, marginBottom: 24, width: '100%', textAlign: 'center',
},
gosHeader: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 6 },
gosTitle: { color: '#00FFFF', fontFamily: "'Orbitron', monospace", fontSize: 22, fontWeight: 700 },
gosSubtitle: { color: '#888', fontSize: 13, marginBottom: 24 },
loginForm: { display: 'flex', flexDirection: 'column', gap: 12, width: '100%', maxWidth: 320 },
input: {
background: '#0a0a1a', border: '1px solid rgba(0,255,255,0.2)',
borderRadius: 6, padding: '10px 14px', color: '#ccc',
fontSize: 14, fontFamily: "'Share Tech Mono', monospace",
outline: 'none',
},
loginBtn: {
padding: '12px', borderRadius: 6, border: 'none',
fontFamily: "'Orbitron', monospace", fontSize: 14, fontWeight: 700,
cursor: 'pointer', transition: 'opacity 0.2s',
},
// Modals
modalOverlay: {
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.8)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 200,
},
modal: {
background: '#0d0d22', border: '2px solid',
borderRadius: 12, padding: '32px 28px', maxWidth: 440, width: '90%',
textAlign: 'center', boxShadow: '0 0 40px rgba(0,0,0,0.5)',
},
modalIcon: { fontSize: 48, marginBottom: 12 },
modalTitle: { fontFamily: "'Orbitron', monospace", fontSize: 18, fontWeight: 700, marginBottom: 12 },
modalText: { color: '#aaa', fontSize: 14, lineHeight: 1.7, marginBottom: 16 },
modalHint: {
background: 'rgba(0,255,255,0.06)', border: '1px solid rgba(0,255,255,0.2)',
borderRadius: 6, padding: '10px 14px', color: '#888', fontSize: 13,
marginBottom: 20, textAlign: 'left',
},
modalBtn: {
padding: '12px 28px', borderRadius: 6, cursor: 'pointer',
fontFamily: "'Orbitron', monospace", fontSize: 13, fontWeight: 700,
},
};
export default Case3Desktop;

1138
src/cases/Case5Desktop.tsx Normal file
View file

@ -0,0 +1,1138 @@
import React, { useState } from 'react';
import type { WallpaperType } from '../App';
interface Case5DesktopProps {
onComplete: (type: WallpaperType) => void;
}
interface Site {
id: number;
name: string;
domain: string;
https: boolean;
isLegit: boolean;
description: string;
price: number;
productName: string;
paymentProcessor: string | null; // null = прямая форма (скиммер)
skimmerHint: string | null;
adLabel?: boolean;
}
const SITES: Site[] = [
{
id: 1,
name: 'KleyMaster — магазин клея',
domain: 'kley-master.ru',
https: true,
isLegit: true,
description: 'Широкий ассортимент клея: ПВА, момент, эпоксидный. Доставка по России.',
price: 149,
productName: 'Клей ПВА "Универсал" 250мл',
paymentProcessor: 'Тинькофф',
skimmerHint: null,
},
{
id: 2,
name: 'Kley-Online — купить клей дёшево',
domain: 'kley-online.shop',
https: false,
isLegit: false,
adLabel: true,
description: 'Клей ПВА, суперклей, момент. Скидки до 70%! Доставка 1 день.',
price: 99,
productName: 'Клей ПВА 250мл',
paymentProcessor: null,
skimmerHint: 'HTTP-соединение: данные карты передаются в открытом виде',
},
{
id: 3,
name: 'SuperKley Discount — акции и скидки',
domain: 'superkley-discount.ru',
https: false,
isLegit: false,
adLabel: true,
description: 'Суперклей, ПВА, эпоксидный. Оптовые цены. Акция: -50% на всё!',
price: 75,
productName: 'ПВА клей универсальный',
paymentProcessor: null,
skimmerHint: 'Домен зарегистрирован 3 дня назад, нет контактов и юридических данных',
},
{
id: 4,
name: 'HobbyTrade — товары для творчества',
domain: 'hobbytrade-shop.ru',
https: true,
isLegit: true,
description: 'Материалы для рукоделия, клей, краски. Более 15 лет на рынке.',
price: 162,
productName: 'Клей ПВА "Универсал" 250мл',
paymentProcessor: 'СберПей',
skimmerHint: null,
},
{
id: 5,
name: 'KleyCheap — дешевле не бывает',
domain: 'kleycheap.xyz',
https: false,
isLegit: false,
adLabel: true,
description: 'Клей по ценам производителя. Без наценки! Только сегодня -60%.',
price: 59,
productName: 'Клей ПВА 250мл АКЦИЯ',
paymentProcessor: null,
skimmerHint: 'Домен в зоне .xyz — часто используется мошенниками',
},
{
id: 6,
name: 'Kley-Market Online — каталог клея',
domain: 'kley-market-online.ru',
https: true,
isLegit: false,
description: 'Большой каталог клея. Быстрая доставка. Безопасная оплата.',
price: 139,
productName: 'Клей ПВА 250мл',
paymentProcessor: null,
skimmerHint: 'Форма оплаты не перенаправляет на банк — данные карты попадают прямо на сайт',
},
{
id: 7,
name: 'BestKley — выгодные покупки',
domain: 'best-kley.site',
https: false,
isLegit: false,
adLabel: true,
description: 'Лучший клей по лучшим ценам. Гарантия качества. Отзывы покупателей.',
price: 89,
productName: 'Клей ПВА BEST 250мл',
paymentProcessor: null,
skimmerHint: 'Домен .site и отсутствие HTTPS — типичные признаки фишингового магазина',
},
{
id: 8,
name: 'Kley-Opt — оптовые цены',
domain: 'kley-opt24.ru',
https: true,
isLegit: false,
description: 'Клей оптом и в розницу. Цены от производителя. Доставка 2-3 дня.',
price: 119,
productName: 'Клей ПВА универсальный',
paymentProcessor: null,
skimmerHint: 'Скопированный дизайн чужого магазина, платёжная форма ведёт на сторонний сервер',
},
{
id: 9,
name: 'MegaKley24 — акционные предложения',
domain: 'megakley24.ru',
https: true,
isLegit: false,
adLabel: true,
description: 'Клей ПВА, момент, суперклей. 24/7. Карты Visa/MasterCard. Быстро.',
price: 109,
productName: 'Клей ПВА 250мл выгодно',
paymentProcessor: null,
skimmerHint: 'Страница оплаты загружает сторонний скрипт, перехватывающий данные карты',
},
{
id: 10,
name: 'Kley-Sale — распродажа клея',
domain: 'kley-sale.shop',
https: false,
isLegit: false,
description: 'Распродажа! Клей ПВА, эпоксидный, момент. Только сегодня!',
price: 49,
productName: 'Клей ПВА SALE',
paymentProcessor: null,
skimmerHint: 'Сайт создан вчера, нет ИНН/ОГРН, телефон не существует',
},
];
type PageType = 'search' | 'product' | 'payment' | null;
const Case5Desktop: React.FC<Case5DesktopProps> = ({ onComplete }) => {
const [searchQuery, setSearchQuery] = useState('купить клей пва 250мл');
const [searched, setSearched] = useState(false);
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
const [page, setPage] = useState<PageType>(null);
const [cardNum, setCardNum] = useState('');
const [cardExp, setCardExp] = useState('');
const [cardCvv, setCardCvv] = useState('');
const [modal, setModal] = useState<'skimmer' | 'success' | null>(null);
const currentUrl = (() => {
if (!selectedSite) return 'ya.ru';
if (page === 'product') return `${selectedSite.domain}/catalog/kley-pva`;
if (page === 'payment') return `${selectedSite.domain}/checkout/payment`;
return 'ya.ru';
})();
const isSecure = (() => {
if (!selectedSite) return true;
return selectedSite.https;
})();
const goSearch = () => setSearched(true);
const openSite = (site: Site) => { setSelectedSite(site); setPage('product'); };
const goToPayment = () => setPage('payment');
const handlePay = () => {
if (!selectedSite) return;
if (selectedSite.isLegit) {
setModal('success');
} else {
setModal('skimmer');
}
};
const resetToSearch = () => {
setModal(null);
setSelectedSite(null);
setPage(null);
setCardNum('');
setCardExp('');
setCardCvv('');
};
return (
<div style={s.root}>
{/* Легенда */}
<div style={s.story}>
<p>
Вам нужно купить клей ПВА для ремонта. Вы открываете Яндекс и ищете товар в интернете.
<br />
<b style={{ color: '#f5a623' }}>Задача:</b> найти надёжный магазин и безопасно оплатить покупку.
</p>
</div>
{/* Браузер */}
<div style={s.browser}>
{/* Хром браузера */}
<div style={s.chrome}>
{selectedSite && (
<button style={s.navBtn} onClick={resetToSearch}></button>
)}
<div style={s.urlBar}>
<span style={isSecure ? s.httpsIcon : s.httpIcon}>{isSecure ? '🔒' : '⚠'}</span>
<span style={{ ...s.urlText, color: isSecure ? '#aaa' : '#f5a623' }}>{currentUrl}</span>
{!isSecure && <span style={s.httpWarnText}>Небезопасно</span>}
</div>
</div>
{/* Контент */}
<div style={s.content}>
{/* === ЯНДЕКС ПОИСК === */}
{!searched && !selectedSite && (
<div style={s.yandexHome}>
<div style={s.yandexLogo}>
<span style={s.yandexY}>Я</span>
<span style={s.yandexNdex}>ндекс</span>
</div>
<div style={s.searchBox}>
<input
style={s.searchInput}
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && goSearch()}
placeholder="Найти в интернете"
/>
<button style={s.searchBtn} onClick={goSearch}>Найти</button>
</div>
</div>
)}
{/* === РЕЗУЛЬТАТЫ ПОИСКА === */}
{searched && !selectedSite && (
<div style={s.searchResults}>
<div style={s.searchResultsHeader}>
<div style={s.searchBoxSmall}>
<span style={s.yandexSmall}>Я</span>
<input
style={s.searchInputSmall}
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && goSearch()}
/>
<button style={s.searchBtnSmall} onClick={goSearch}>🔍</button>
</div>
<div style={s.resultCount}>Нашлось 10 результатов</div>
</div>
<div style={s.resultsList}>
{SITES.map((site) => (
<div key={site.id} style={s.resultItem} onClick={() => openSite(site)}>
{site.adLabel && <span style={s.adBadge}>Реклама</span>}
<div style={s.resultName}>{site.name}</div>
<div style={{ ...s.resultDomain, color: site.https ? '#4a9e6e' : '#f5a623' }}>
{site.https ? '🔒' : '⚠'} {site.domain}
</div>
<div style={s.resultDesc}>{site.description}</div>
<div style={s.resultPrice}>от {site.price} </div>
</div>
))}
</div>
</div>
)}
{/* === СТРАНИЦА ТОВАРА === */}
{selectedSite && page === 'product' && (
<div style={s.productPage}>
<div style={s.shopHeader}>
<span style={s.shopName}>{selectedSite.name}</span>
{!selectedSite.https && (
<span style={s.unsafeBadge}> HTTP небезопасный сайт</span>
)}
</div>
<div style={s.productCard}>
<div style={s.productImage}>
<span style={{ fontSize: 64 }}>🧴</span>
</div>
<div style={s.productInfo}>
<div style={s.productTitle}>{selectedSite.productName}</div>
<div style={s.productPriceTag}>{selectedSite.price} </div>
<div style={s.productDesc}>
Объём: 250 мл · В наличии · Доставка 25 дней
</div>
<button style={s.buyBtn} onClick={goToPayment}>
🛒 Купить
</button>
</div>
</div>
{/* Подозрительные признаки */}
{!selectedSite.isLegit && (
<div style={s.suspiciousHints}>
<div style={s.hintTitle}>💡 Обратите внимание на признаки сайта:</div>
<ul style={s.hintList}>
{!selectedSite.https && <li> Сайт не использует HTTPS</li>}
{selectedSite.domain.includes('.xyz') && <li> Нестандартный домен (.xyz)</li>}
{selectedSite.domain.includes('.shop') && <li> Нестандартный домен (.shop)</li>}
{selectedSite.domain.includes('.site') && <li> Нестандартный домен (.site)</li>}
{selectedSite.adLabel && <li> Рекламная ссылка в поиске</li>}
<li> Нет раздела «О компании», нет ИНН / ОГРН</li>
</ul>
</div>
)}
{selectedSite.isLegit && (
<div style={s.legitHints}>
<div style={s.hintTitleGood}> Признаки надёжного магазина:</div>
<ul style={s.hintList}>
<li> HTTPS соединение зашифровано</li>
<li> Известный домен .ru</li>
<li> Есть раздел «О нас» с ИНН и ОГРН</li>
<li> Оплата через проверенную платёжную систему</li>
</ul>
</div>
)}
</div>
)}
{/* === СТРАНИЦА ОПЛАТЫ === */}
{selectedSite && page === 'payment' && (
<div style={s.paymentPage}>
<div style={s.paymentTitle}>Оформление заказа</div>
{/* Сводка заказа */}
<div style={s.orderSummary}>
<div style={s.orderRow}>
<span style={s.orderLabel}>Товар:</span>
<span style={s.orderValue}>{selectedSite.productName}</span>
</div>
<div style={s.orderRow}>
<span style={s.orderLabel}>Сумма:</span>
<span style={{ ...s.orderValue, color: '#00FF41', fontWeight: 700 }}>
{selectedSite.price}
</span>
</div>
</div>
{/* Форма оплаты — легитимная */}
{selectedSite.isLegit ? (
<div style={s.legitPayment}>
<div style={s.processorBadge}>
<span style={s.processorIcon}>🏦</span>
<span style={s.processorName}>Оплата через {selectedSite.paymentProcessor}</span>
<span style={s.processorSecure}>🔒 Защищено</span>
</div>
<p style={s.processorNote}>
Вы будете перенаправлены на защищённую страницу банка.
Магазин не получает данные вашей карты.
</p>
<div style={s.cardForm}>
<label style={s.cardLabel}>Номер карты</label>
<input
style={s.cardInput}
placeholder="0000 0000 0000 0000"
value={cardNum}
onChange={e => setCardNum(e.target.value)}
maxLength={19}
/>
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ flex: 1 }}>
<label style={s.cardLabel}>Срок действия</label>
<input
style={s.cardInput}
placeholder="MM/YY"
value={cardExp}
onChange={e => setCardExp(e.target.value)}
maxLength={5}
/>
</div>
<div style={{ flex: 1 }}>
<label style={s.cardLabel}>CVV</label>
<input
style={s.cardInput}
placeholder="•••"
value={cardCvv}
onChange={e => setCardCvv(e.target.value)}
maxLength={3}
type="password"
/>
</div>
</div>
</div>
<button style={s.payBtn} onClick={handlePay}>
Оплатить {selectedSite.price}
</button>
</div>
) : (
/* Форма оплаты — скиммер */
<div style={s.fakePayment}>
<div style={s.fakeSecureBadge}>
<span>🔒</span>
<span>Безопасная оплата картой</span>
<span style={s.visaBadge}>VISA</span>
<span style={s.mcBadge}>MC</span>
</div>
<div style={s.cardForm}>
<label style={s.cardLabel}>Номер карты</label>
<input
style={s.cardInput}
placeholder="0000 0000 0000 0000"
value={cardNum}
onChange={e => setCardNum(e.target.value)}
maxLength={19}
/>
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ flex: 1 }}>
<label style={s.cardLabel}>Срок действия</label>
<input
style={s.cardInput}
placeholder="MM/YY"
value={cardExp}
onChange={e => setCardExp(e.target.value)}
maxLength={5}
/>
</div>
<div style={{ flex: 1 }}>
<label style={s.cardLabel}>CVV</label>
<input
style={s.cardInput}
placeholder="•••"
value={cardCvv}
onChange={e => setCardCvv(e.target.value)}
maxLength={3}
type="password"
/>
</div>
</div>
</div>
<button style={s.payBtnFake} onClick={handlePay}>
Оплатить {selectedSite.price}
</button>
</div>
)}
</div>
)}
</div>
</div>
{/* === МОДАЛ: СКИММЕР === */}
{modal === 'skimmer' && selectedSite && (
<div style={s.modalOverlay}>
<div style={s.modalDanger}>
<div style={s.modalIcon}>💀</div>
<div style={s.modalTitle}>Скиммер! Данные карты похищены</div>
<div style={s.modalText}>
Вы ввели данные карты на сайте <b style={{ color: '#FF0040' }}>{selectedSite.domain}</b>,
который использует <b>веб-скиммер</b> вредоносный скрипт, перехватывающий
платёжные данные прямо на странице оплаты.
</div>
<div style={s.skimmerExplain}>
<div style={s.skimmerExplainTitle}>Что такое скимминг?</div>
<div style={s.skimmerExplainText}>
Скиммер код, встроенный в страницу оплаты. Пока вы вводите номер карты,
он тайно копирует данные и отправляет злоумышленнику. Внешне страница
выглядит как обычная форма оплаты.
</div>
</div>
<div style={s.skimmerReason}>
<div style={s.skimmerReasonTitle}>Почему этот сайт подозрителен:</div>
<div style={s.skimmerReasonText}>{selectedSite.skimmerHint}</div>
</div>
<div style={s.safeRules}>
<div style={s.safeRulesTitle}>Как защититься:</div>
<ul style={s.safeRulesList}>
<li>Платите только на известных площадках (Ozon, Wildberries, DNS и т.п.)</li>
<li>Ищите перенаправление на страницу банка (СберПей, Тинькофф и др.)</li>
<li>Проверяйте HTTPS и домен перед вводом карты</li>
<li>Используйте виртуальную карту с лимитом для онлайн-покупок</li>
</ul>
</div>
<button style={s.retryBtn} onClick={resetToSearch}>
Вернуться к поиску и выбрать другой магазин
</button>
</div>
</div>
)}
{/* === МОДАЛ: УСПЕХ === */}
{modal === 'success' && selectedSite && (
<div style={s.modalOverlay}>
<div style={s.modalSuccess}>
<div style={s.modalIcon}></div>
<div style={{ ...s.modalTitle, color: '#00FF41' }}>Покупка совершена безопасно!</div>
<div style={s.modalText}>
Оплата прошла через <b style={{ color: '#00FF41' }}>{selectedSite.paymentProcessor}</b>.
Магазин <b>{selectedSite.domain}</b> не получил данные вашей карты
они были переданы напрямую банку по зашифрованному соединению.
</div>
<div style={s.successDetails}>
<div style={s.successRow}>
<span>Товар:</span>
<span>{selectedSite.productName}</span>
</div>
<div style={s.successRow}>
<span>Сумма:</span>
<span style={{ color: '#00FF41' }}>{selectedSite.price} </span>
</div>
<div style={s.successRow}>
<span>Статус:</span>
<span style={{ color: '#00FF41' }}>Оплачено</span>
</div>
</div>
<button style={s.completeBtn} onClick={() => { setModal(null); onComplete('win10'); }}>
Завершить кейс
</button>
</div>
</div>
)}
</div>
);
};
const s: Record<string, React.CSSProperties> = {
root: {
minHeight: '100vh',
background: 'linear-gradient(135deg, #0a0a14 0%, #0d1117 60%, #0a1400 100%)',
color: '#ccc',
fontFamily: "'Share Tech Mono', monospace",
padding: '0 0 40px',
},
story: {
maxWidth: 760,
margin: '24px auto 0',
padding: '14px 24px',
background: 'rgba(0,255,255,0.04)',
border: '1px solid rgba(0,255,255,0.15)',
borderRadius: 8,
fontSize: 14,
lineHeight: 1.7,
color: '#aaa',
textAlign: 'center',
},
browser: {
maxWidth: 860,
margin: '24px auto 0',
border: '1px solid rgba(0,255,255,0.2)',
borderRadius: 10,
overflow: 'hidden',
boxShadow: '0 0 30px rgba(0,255,255,0.06)',
},
chrome: {
background: '#080818',
borderBottom: '1px solid rgba(0,255,255,0.15)',
padding: '8px 12px',
display: 'flex',
alignItems: 'center',
gap: 8,
},
navBtn: {
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.1)',
color: '#888',
borderRadius: 4,
padding: '4px 10px',
cursor: 'pointer',
fontSize: 14,
fontFamily: "'Share Tech Mono', monospace",
},
urlBar: {
flex: 1,
background: '#0a0a1a',
border: '1px solid rgba(0,255,255,0.2)',
borderRadius: 4,
padding: '5px 12px',
fontSize: 13,
display: 'flex',
alignItems: 'center',
gap: 8,
},
httpsIcon: { color: '#00FF41', fontSize: 13 },
httpIcon: { color: '#f5a623', fontSize: 13 },
urlText: { flex: 1 },
httpWarnText: { color: '#f5a623', fontSize: 11, marginLeft: 4 },
content: {
background: '#0d0d0d',
minHeight: 480,
},
/* Яндекс главная */
yandexHome: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
paddingTop: 80,
paddingBottom: 60,
gap: 28,
},
yandexLogo: {
fontSize: 52,
fontWeight: 700,
lineHeight: 1,
},
yandexY: {
color: '#FF0040',
fontFamily: 'Georgia, serif',
},
yandexNdex: {
color: '#eee',
fontFamily: 'Georgia, serif',
},
searchBox: {
display: 'flex',
width: '100%',
maxWidth: 560,
gap: 0,
},
searchInput: {
flex: 1,
padding: '12px 16px',
background: '#1a1a2e',
border: '1px solid rgba(0,255,255,0.25)',
borderRight: 'none',
borderRadius: '6px 0 0 6px',
color: '#eee',
fontSize: 15,
fontFamily: "'Share Tech Mono', monospace",
outline: 'none',
},
searchBtn: {
padding: '12px 24px',
background: '#FF0040',
border: 'none',
borderRadius: '0 6px 6px 0',
color: '#fff',
fontFamily: "'Orbitron', monospace",
fontSize: 13,
fontWeight: 700,
cursor: 'pointer',
},
/* Результаты поиска */
searchResults: {
padding: '0 0 24px',
},
searchResultsHeader: {
background: '#080818',
borderBottom: '1px solid rgba(0,255,255,0.1)',
padding: '10px 16px',
display: 'flex',
alignItems: 'center',
gap: 16,
flexWrap: 'wrap',
},
searchBoxSmall: {
display: 'flex',
alignItems: 'center',
flex: 1,
maxWidth: 400,
background: '#0d0d22',
border: '1px solid rgba(0,255,255,0.2)',
borderRadius: 6,
overflow: 'hidden',
},
yandexSmall: {
color: '#FF0040',
fontFamily: 'Georgia, serif',
fontSize: 18,
fontWeight: 700,
padding: '0 8px',
},
searchInputSmall: {
flex: 1,
background: 'transparent',
border: 'none',
color: '#eee',
fontSize: 13,
fontFamily: "'Share Tech Mono', monospace",
padding: '8px 4px',
outline: 'none',
},
searchBtnSmall: {
background: 'transparent',
border: 'none',
color: '#888',
cursor: 'pointer',
padding: '8px 12px',
fontSize: 14,
},
resultCount: {
color: '#555',
fontSize: 12,
},
resultsList: {
padding: '8px 20px',
display: 'flex',
flexDirection: 'column',
gap: 2,
},
resultItem: {
padding: '14px 16px',
borderRadius: 6,
cursor: 'pointer',
transition: 'background 0.15s',
borderBottom: '1px solid rgba(255,255,255,0.04)',
},
adBadge: {
display: 'inline-block',
background: 'rgba(245,166,35,0.15)',
border: '1px solid rgba(245,166,35,0.4)',
color: '#f5a623',
fontSize: 10,
padding: '1px 6px',
borderRadius: 3,
marginBottom: 4,
fontFamily: "'Share Tech Mono', monospace",
},
resultName: {
color: '#5b9bd5',
fontSize: 15,
marginBottom: 2,
},
resultDomain: {
fontSize: 12,
marginBottom: 4,
},
resultDesc: {
color: '#777',
fontSize: 13,
lineHeight: 1.5,
},
resultPrice: {
color: '#00FF41',
fontSize: 13,
marginTop: 4,
fontFamily: "'Orbitron', monospace",
},
/* Страница товара */
productPage: {
padding: 24,
},
shopHeader: {
display: 'flex',
alignItems: 'center',
gap: 16,
marginBottom: 20,
paddingBottom: 12,
borderBottom: '1px solid rgba(255,255,255,0.06)',
},
shopName: {
color: '#00FFFF',
fontFamily: "'Orbitron', monospace",
fontSize: 14,
fontWeight: 700,
},
unsafeBadge: {
background: 'rgba(245,166,35,0.15)',
border: '1px solid #f5a623',
color: '#f5a623',
fontSize: 11,
padding: '3px 10px',
borderRadius: 4,
},
productCard: {
display: 'flex',
gap: 24,
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 8,
padding: 20,
marginBottom: 20,
},
productImage: {
width: 100,
height: 100,
background: 'rgba(255,255,255,0.05)',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
},
productInfo: {
flex: 1,
},
productTitle: {
color: '#ddd',
fontSize: 16,
marginBottom: 8,
lineHeight: 1.4,
},
productPriceTag: {
color: '#00FF41',
fontFamily: "'Orbitron', monospace",
fontSize: 22,
fontWeight: 700,
marginBottom: 8,
},
productDesc: {
color: '#666',
fontSize: 12,
marginBottom: 16,
},
buyBtn: {
padding: '10px 28px',
background: '#00FFFF',
border: 'none',
borderRadius: 6,
color: '#000',
fontFamily: "'Orbitron', monospace",
fontSize: 13,
fontWeight: 700,
cursor: 'pointer',
},
suspiciousHints: {
background: 'rgba(245,166,35,0.06)',
border: '1px solid rgba(245,166,35,0.3)',
borderRadius: 8,
padding: '14px 18px',
marginTop: 16,
},
hintTitle: {
color: '#f5a623',
fontSize: 13,
marginBottom: 8,
fontFamily: "'Orbitron', monospace",
fontWeight: 700,
},
legitHints: {
background: 'rgba(0,255,65,0.05)',
border: '1px solid rgba(0,255,65,0.25)',
borderRadius: 8,
padding: '14px 18px',
marginTop: 16,
},
hintTitleGood: {
color: '#00FF41',
fontSize: 13,
marginBottom: 8,
fontFamily: "'Orbitron', monospace",
fontWeight: 700,
},
hintList: {
margin: 0,
paddingLeft: 20,
color: '#aaa',
fontSize: 13,
lineHeight: 1.8,
},
/* Страница оплаты */
paymentPage: {
padding: 24,
maxWidth: 480,
margin: '0 auto',
},
paymentTitle: {
color: '#00FFFF',
fontFamily: "'Orbitron', monospace",
fontSize: 16,
fontWeight: 700,
marginBottom: 16,
textAlign: 'center',
},
orderSummary: {
background: 'rgba(0,255,255,0.04)',
border: '1px solid rgba(0,255,255,0.15)',
borderRadius: 8,
padding: '12px 16px',
marginBottom: 20,
},
orderRow: {
display: 'flex',
justifyContent: 'space-between',
padding: '4px 0',
fontSize: 13,
},
orderLabel: { color: '#666' },
orderValue: { color: '#ddd' },
legitPayment: {
background: 'rgba(0,255,65,0.04)',
border: '1px solid rgba(0,255,65,0.2)',
borderRadius: 8,
padding: 20,
},
processorBadge: {
display: 'flex',
alignItems: 'center',
gap: 10,
marginBottom: 10,
padding: '8px 14px',
background: 'rgba(0,255,65,0.08)',
borderRadius: 6,
},
processorIcon: { fontSize: 20 },
processorName: { color: '#00FF41', fontFamily: "'Orbitron', monospace", fontSize: 13, fontWeight: 700, flex: 1 },
processorSecure: { color: '#00FF41', fontSize: 12 },
processorNote: {
color: '#777',
fontSize: 12,
lineHeight: 1.6,
marginBottom: 16,
textAlign: 'center',
},
fakePayment: {
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 8,
padding: 20,
},
fakeSecureBadge: {
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 16,
color: '#888',
fontSize: 12,
},
visaBadge: {
background: '#1a1a6e',
color: '#fff',
padding: '1px 6px',
borderRadius: 3,
fontSize: 10,
fontWeight: 700,
},
mcBadge: {
background: '#8b0000',
color: '#fff',
padding: '1px 6px',
borderRadius: 3,
fontSize: 10,
fontWeight: 700,
},
cardForm: {
display: 'flex',
flexDirection: 'column',
gap: 12,
marginBottom: 16,
},
cardLabel: {
display: 'block',
color: '#666',
fontSize: 11,
marginBottom: 4,
fontFamily: "'Share Tech Mono', monospace",
},
cardInput: {
width: '100%',
padding: '9px 12px',
background: '#0a0a1a',
border: '1px solid rgba(0,255,255,0.2)',
borderRadius: 5,
color: '#ddd',
fontSize: 14,
fontFamily: "'Share Tech Mono', monospace",
outline: 'none',
boxSizing: 'border-box',
},
payBtn: {
width: '100%',
padding: '12px',
background: '#00FF41',
border: 'none',
borderRadius: 6,
color: '#000',
fontFamily: "'Orbitron', monospace",
fontSize: 14,
fontWeight: 700,
cursor: 'pointer',
},
payBtnFake: {
width: '100%',
padding: '12px',
background: '#2255cc',
border: 'none',
borderRadius: 6,
color: '#fff',
fontFamily: 'Arial, sans-serif',
fontSize: 14,
fontWeight: 700,
cursor: 'pointer',
},
/* Модалки */
modalOverlay: {
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.8)',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
zIndex: 200,
overflowY: 'auto',
padding: '40px 16px',
},
modalDanger: {
background: '#0d0d22',
border: '1px solid #FF0040',
borderRadius: 12,
padding: '28px 28px 24px',
maxWidth: 520,
width: '100%',
boxShadow: '0 0 40px rgba(255,0,64,0.25)',
textAlign: 'center',
},
modalSuccess: {
background: '#0d0d22',
border: '1px solid #00FF41',
borderRadius: 12,
padding: '28px 28px 24px',
maxWidth: 480,
width: '100%',
boxShadow: '0 0 40px rgba(0,255,65,0.2)',
textAlign: 'center',
},
modalIcon: { fontSize: 48, marginBottom: 12 },
modalTitle: {
color: '#FF0040',
fontFamily: "'Orbitron', monospace",
fontSize: 16,
fontWeight: 700,
marginBottom: 12,
},
modalText: {
color: '#aaa',
fontSize: 13,
lineHeight: 1.7,
marginBottom: 16,
},
skimmerExplain: {
background: 'rgba(255,0,64,0.06)',
border: '1px solid rgba(255,0,64,0.25)',
borderRadius: 8,
padding: '12px 16px',
marginBottom: 12,
textAlign: 'left',
},
skimmerExplainTitle: {
color: '#FF0040',
fontFamily: "'Orbitron', monospace",
fontSize: 12,
fontWeight: 700,
marginBottom: 6,
},
skimmerExplainText: {
color: '#aaa',
fontSize: 13,
lineHeight: 1.6,
},
skimmerReason: {
background: 'rgba(245,166,35,0.06)',
border: '1px solid rgba(245,166,35,0.25)',
borderRadius: 8,
padding: '12px 16px',
marginBottom: 12,
textAlign: 'left',
},
skimmerReasonTitle: {
color: '#f5a623',
fontFamily: "'Orbitron', monospace",
fontSize: 12,
fontWeight: 700,
marginBottom: 6,
},
skimmerReasonText: {
color: '#aaa',
fontSize: 13,
lineHeight: 1.6,
},
safeRules: {
background: 'rgba(0,255,65,0.04)',
border: '1px solid rgba(0,255,65,0.2)',
borderRadius: 8,
padding: '12px 16px',
marginBottom: 20,
textAlign: 'left',
},
safeRulesTitle: {
color: '#00FF41',
fontFamily: "'Orbitron', monospace",
fontSize: 12,
fontWeight: 700,
marginBottom: 8,
},
safeRulesList: {
margin: 0,
paddingLeft: 20,
color: '#aaa',
fontSize: 13,
lineHeight: 1.8,
},
retryBtn: {
padding: '10px 20px',
background: 'transparent',
border: '1px solid rgba(0,255,255,0.3)',
borderRadius: 6,
color: '#888',
cursor: 'pointer',
fontFamily: "'Share Tech Mono', monospace",
fontSize: 13,
},
successDetails: {
background: 'rgba(0,255,65,0.05)',
border: '1px solid rgba(0,255,65,0.2)',
borderRadius: 8,
padding: '12px 16px',
marginBottom: 20,
textAlign: 'left',
},
successRow: {
display: 'flex',
justifyContent: 'space-between',
padding: '4px 0',
fontSize: 13,
color: '#aaa',
},
completeBtn: {
padding: '12px 36px',
background: '#00FF41',
border: 'none',
borderRadius: 6,
color: '#000',
fontFamily: "'Orbitron', monospace",
fontSize: 14,
fontWeight: 700,
cursor: 'pointer',
},
};
export default Case5Desktop;

View file

@ -311,11 +311,49 @@
.csm-level-4 .csm-tooltip::after { border-left-color: var(--csm-cyan); }
.csm-level-4 .csm-tooltip-title { color: var(--csm-cyan); }
.csm-deepfake:hover .csm-hexagon-bg { stroke: #FF0040; filter: drop-shadow(0 0 20px #FF0040); }
.csm-deepfake:hover .csm-hexagon-icon { color: #FF0040; }
.csm-deepfake .csm-tooltip { border-color: #FF0040; }
.csm-deepfake .csm-tooltip::after { border-left-color: #FF0040; }
.csm-deepfake .csm-tooltip-title { color: #FF0040; }
@keyframes csm-rainbow-stroke {
0% { stroke: #00FF41; filter: drop-shadow(0 0 20px #00FF41); }
25% { stroke: #00FFFF; filter: drop-shadow(0 0 20px #00FFFF); }
50% { stroke: #FF00FF; filter: drop-shadow(0 0 20px #FF00FF); }
75% { stroke: #FF6600; filter: drop-shadow(0 0 20px #FF6600); }
100% { stroke: #00FF41; filter: drop-shadow(0 0 20px #00FF41); }
}
@keyframes csm-rainbow-color {
0% { color: #00FF41; }
25% { color: #00FFFF; }
50% { color: #FF00FF; }
75% { color: #FF6600; }
100% { color: #00FF41; }
}
@keyframes csm-rainbow-border {
0% { border-color: #00FF41; }
25% { border-color: #00FFFF; }
50% { border-color: #FF00FF; }
75% { border-color: #FF6600; }
100% { border-color: #00FF41; }
}
.csm-deepfake .csm-hexagon-bg {
animation: csm-rainbow-stroke 3s linear infinite;
}
.csm-deepfake .csm-hexagon-icon {
animation: csm-rainbow-color 3s linear infinite;
}
.csm-deepfake:hover .csm-hexagon-bg { animation-duration: 1s; }
.csm-deepfake:hover .csm-hexagon-icon { animation-duration: 1s; }
.csm-deepfake .csm-tooltip {
animation: csm-rainbow-border 3s linear infinite;
}
.csm-deepfake .csm-tooltip::after { border-left-color: #FF00FF; }
.csm-deepfake .csm-tooltip-title {
animation: csm-rainbow-color 3s linear infinite;
}
.csm-level-5:hover .csm-hexagon-bg { stroke: #a855f7; filter: drop-shadow(0 0 20px #a855f7); }
.csm-level-5:hover .csm-hexagon-icon { color: #a855f7; }
.csm-level-5 .csm-tooltip { border-color: #a855f7; }
.csm-level-5 .csm-tooltip::after { border-left-color: #a855f7; }
.csm-level-5 .csm-tooltip-title { color: #a855f7; }
/* Bottom Container */
.csm-bottom-container {
@ -325,7 +363,7 @@
right: 0;
display: flex;
justify-content: space-between;
padding: 0 80px;
padding: 0 140px;
z-index: 20;
}

View file

@ -35,13 +35,15 @@ const CybersecMenu: React.FC<CybersecMenuProps> = ({ onSelectLevel, onOpenWiki,
const selectLevel = useCallback((level: number | string) => {
const levelNames: Record<string | number, string> = {
1: 'Phishing Fake emails & links',
2: 'Skimming ATM/card fraud',
3: 'Password cracking Brute force & dictionary',
4: 'Social engineering Manipulation',
'deepfake': 'Voice deepfake AI-generated speech',
1: 'Устаревшее ПО — обновление системы',
2: 'Рабочая среда — базовые угрозы',
3: 'Фишинг — поддельное письмо Госуслуг',
4: 'Публичная сеть — защита через VPN',
5: 'Веб-скимминг — поддельный магазин',
'deepfake': 'Дипфейк — ИИ-генерация голоса',
};
showNotification(`>> LEVEL ${level} SELECTED`, levelNames[level]);
const label = typeof level === 'number' ? `УРОВЕНЬ ${level}` : level.toUpperCase();
showNotification(`>> ${label} ВЫБРАН`, levelNames[level]);
onSelectLevel?.(level);
}, [showNotification, onSelectLevel]);
@ -63,7 +65,8 @@ const CybersecMenu: React.FC<CybersecMenuProps> = ({ onSelectLevel, onOpenWiki,
'2': () => selectLevel(2),
'3': () => selectLevel(3),
'4': () => selectLevel(4),
'5': () => selectLevel('deepfake'),
'5': () => selectLevel(5),
'6': () => selectLevel('deepfake'),
'w': openWiki, 'W': openWiki,
'q': openQuiz, 'Q': openQuiz,
};
@ -209,11 +212,12 @@ const CybersecMenu: React.FC<CybersecMenuProps> = ({ onSelectLevel, onOpenWiki,
}, []);
const menuItems = [
{ level: 1 as number | string, cls: 'csm-level-1', icon: '🔒', title: 'Level 1', desc: 'Phishing Fake emails & links' },
{ level: 2 as number | string, cls: 'csm-level-2', icon: '⌨️', title: 'Level 2', desc: 'Skimming ATM/card fraud' },
{ level: 3 as number | string, cls: 'csm-level-3', icon: '🖥️', title: 'Level 3', desc: 'Password cracking Brute force & dictionary' },
{ level: 4 as number | string, cls: 'csm-level-4', icon: '🛡️', title: 'Level 4', desc: 'Social engineering Manipulation' },
{ level: 'deepfake', cls: 'csm-deepfake', icon: '🎭', title: 'Deepfake', desc: 'Voice deepfake AI-generated speech' },
{ level: 1 as number | string, cls: 'csm-level-1', icon: '💻', title: 'Уровень 1', desc: 'Устаревшее ПО — обновление системы' },
{ level: 2 as number | string, cls: 'csm-level-2', icon: '🖥️', title: 'Уровень 2', desc: 'Рабочая среда — базовые угрозы' },
{ level: 3 as number | string, cls: 'csm-level-3', icon: '📧', title: 'Уровень 3', desc: 'Фишинг — поддельное письмо Госуслуг' },
{ level: 4 as number | string, cls: 'csm-level-4', icon: '📶', title: 'Уровень 4', desc: 'Публичная сеть — защита через VPN' },
{ level: 5 as number | string, cls: 'csm-level-5', icon: '💳', title: 'Уровень 5', desc: 'Веб-скимминг — поддельный магазин' },
{ level: 'deepfake', cls: 'csm-deepfake', icon: '🎭', title: 'Дипфейк', desc: 'Дипфейк — ИИ-генерация голоса' },
];
return (
@ -344,14 +348,14 @@ const CybersecMenu: React.FC<CybersecMenuProps> = ({ onSelectLevel, onOpenWiki,
<span className="csm-block-icon">📖</span>
<span className="csm-block-text">WIKI</span>
</div>
<div className="csm-block-tooltip">Security knowledge base</div>
<div className="csm-block-tooltip">База знаний по безопастности</div>
</div>
<div className="csm-bottom-block" onClick={openQuiz}>
<div className="csm-block-shape">
<span className="csm-block-icon"></span>
<span className="csm-block-text">QUIZ</span>
</div>
<div className="csm-block-tooltip">Test your skills</div>
<div className="csm-block-tooltip">Проверь свои знания!</div>
</div>
</div>
</div>

View file

@ -1,18 +1,39 @@
.chatterbox-container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 20px;
background: #0d0d22;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
border: 1px solid rgba(0,255,255,0.2);
box-shadow: 0 0 40px rgba(0,255,255,0.06);
font-family: 'Share Tech Mono', monospace;
}
.chatterbox-container p {
color: #aaa;
font-size: 13px;
line-height: 1.7;
margin-bottom: 16px;
}
.chatterbox-container h2 {
color: #00FFFF;
font-family: 'Orbitron', monospace;
font-size: 14px;
font-weight: 700;
margin: 24px 0 12px;
letter-spacing: 1px;
}
.chatterbox-container h1 {
text-align: center;
color: #333;
color: #00FFFF;
margin-bottom: 30px;
font-size: 28px;
font-size: 22px;
font-family: 'Orbitron', monospace;
font-weight: 700;
text-shadow: 0 0 20px rgba(0,255,255,0.5);
letter-spacing: 2px;
}
.form-group {
@ -22,9 +43,10 @@
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 14px;
color: #888;
font-size: 12px;
letter-spacing: 1px;
text-transform: uppercase;
}
.form-group input[type="text"],
@ -33,10 +55,13 @@
.form-group textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 15px;
transition: border-color 0.3s;
background: #080818;
border: 1px solid rgba(0,255,255,0.2);
border-radius: 6px;
font-size: 14px;
color: #ddd;
font-family: 'Share Tech Mono', monospace;
transition: border-color 0.2s;
box-sizing: border-box;
}
@ -44,7 +69,13 @@
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
border-color: #00FFFF;
box-shadow: 0 0 8px rgba(0,255,255,0.2);
}
.form-group select option {
background: #0d0d22;
color: #ddd;
}
.form-group textarea {
@ -54,109 +85,119 @@
.char-counter {
text-align: right;
font-size: 12px;
color: #999;
font-size: 11px;
color: #555;
margin-top: 5px;
}
/* Voice section */
.voice-section {
background: #f8f9ff;
border-radius: 15px;
background: #080818;
border-radius: 8px;
padding: 25px;
margin-bottom: 25px;
border: 2px solid #e0e0e0;
border: 1px solid rgba(0,255,255,0.15);
}
.voice-section.recording {
border-color: #e74c3c;
background: #fdf2f2;
border-color: #FF0040;
box-shadow: 0 0 16px rgba(255,0,64,0.15);
}
.voice-section.has-recording {
border-color: #4caf50;
background: #f1f8f4;
border-color: #00FF41;
box-shadow: 0 0 16px rgba(0,255,65,0.1);
}
.voice-label {
font-size: 16px;
font-size: 13px;
font-weight: 600;
color: #333;
color: #00FFFF;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
font-family: 'Orbitron', monospace;
letter-spacing: 1px;
}
.check-badge {
display: inline-flex;
align-items: center;
gap: 5px;
background: #4caf50;
color: white;
padding: 4px 12px;
background: rgba(0,255,65,0.15);
border: 1px solid #00FF41;
color: #00FF41;
padding: 3px 10px;
border-radius: 20px;
font-size: 12px;
font-size: 11px;
margin-left: 10px;
font-family: 'Share Tech Mono', monospace;
}
.record-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
background: rgba(0,255,255,0.1);
border: 2px solid #00FFFF;
color: #00FFFF;
border-radius: 50%;
width: 70px;
height: 70px;
font-size: 28px;
cursor: pointer;
transition: all 0.3s;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
box-shadow: 0 0 20px rgba(0,255,255,0.2);
}
.record-btn:hover {
transform: scale(1.1);
background: rgba(0,255,255,0.2);
box-shadow: 0 0 30px rgba(0,255,255,0.4);
transform: scale(1.05);
}
.record-btn.recording {
background: #e74c3c;
animation: pulse 1.5s infinite;
background: rgba(255,0,64,0.15);
border-color: #FF0040;
color: #FF0040;
box-shadow: 0 0 20px rgba(255,0,64,0.3);
animation: ctb-pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
@keyframes ctb-pulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 20px rgba(255,0,64,0.3); }
50% { transform: scale(1.08); box-shadow: 0 0 35px rgba(255,0,64,0.5); }
}
.record-status {
text-align: center;
margin-top: 15px;
color: #666;
font-size: 14px;
font-size: 13px;
}
.record-timer {
text-align: center;
font-size: 24px;
font-weight: 700;
color: #667eea;
color: #FF0040;
margin-top: 10px;
font-family: monospace;
font-family: 'Orbitron', monospace;
text-shadow: 0 0 10px rgba(255,0,64,0.5);
}
.voice-text {
background: white;
border-radius: 10px;
padding: 20px;
background: rgba(0,255,255,0.04);
border: 1px dashed rgba(0,255,255,0.3);
border-radius: 8px;
padding: 16px 20px;
margin-top: 20px;
text-align: center;
font-size: 18px;
font-weight: 600;
color: #333;
border: 2px dashed #667eea;
font-size: 14px;
color: #aaa;
line-height: 1.6;
}
.voice-preview {
@ -165,6 +206,7 @@
.voice-preview audio {
width: 100%;
filter: invert(1) hue-rotate(180deg);
}
.voice-actions {
@ -176,30 +218,44 @@
.voice-actions button {
flex: 1;
padding: 10px;
border-radius: 8px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-size: 13px;
font-family: 'Share Tech Mono', monospace;
font-weight: 600;
transition: all 0.2s;
}
.btn-retry {
background: #e0e0e0;
color: #333;
background: transparent;
border: 1px solid rgba(255,255,255,0.15);
color: #888;
}
.btn-retry:hover {
border-color: #f5a623;
color: #f5a623;
}
.btn-use {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
background: rgba(0,255,65,0.1);
border: 1px solid #00FF41;
color: #00FF41;
}
.btn-use:hover {
background: rgba(0,255,65,0.2);
box-shadow: 0 0 12px rgba(0,255,65,0.3);
}
/* File upload */
.or-divider {
text-align: center;
margin: 20px 0;
color: #999;
font-size: 14px;
color: #444;
font-size: 12px;
position: relative;
letter-spacing: 2px;
}
.or-divider::before,
@ -209,35 +265,40 @@
top: 50%;
width: 40%;
height: 1px;
background: #e0e0e0;
background: rgba(0,255,255,0.1);
}
.or-divider::before { left: 0; }
.or-divider::after { right: 0; }
.file-input-wrapper {
border: 2px dashed #ccc;
border-radius: 10px;
border: 1px dashed rgba(0,255,255,0.2);
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
transition: all 0.2s;
color: #666;
font-size: 13px;
}
.file-input-wrapper:hover {
border-color: #667eea;
background: #f8f9ff;
border-color: #00FFFF;
background: rgba(0,255,255,0.04);
color: #00FFFF;
}
.file-input-wrapper.has-file {
border-color: #4caf50;
background: #f1f8f4;
border-color: #00FF41;
background: rgba(0,255,65,0.04);
color: #00FF41;
}
.file-name {
margin-top: 10px;
font-weight: 600;
color: #667eea;
color: #00FFFF;
font-size: 13px;
}
/* Sliders */
@ -249,13 +310,16 @@
.slider-group input[type="range"] {
flex: 1;
accent-color: #00FFFF;
}
.slider-value {
min-width: 50px;
text-align: center;
font-weight: 600;
color: #667eea;
color: #00FFFF;
font-family: 'Orbitron', monospace;
font-size: 13px;
}
.two-col {
@ -267,24 +331,26 @@
/* Submit */
.submit-btn {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
padding: 14px;
background: rgba(0,255,65,0.1);
border: 1px solid #00FF41;
color: #00FF41;
border-radius: 8px;
font-size: 14px;
font-family: 'Orbitron', monospace;
font-weight: 700;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
letter-spacing: 2px;
transition: all 0.2s;
}
.submit-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
background: rgba(0,255,65,0.2);
box-shadow: 0 0 20px rgba(0,255,65,0.3);
}
.submit-btn:disabled {
opacity: 0.6;
opacity: 0.4;
cursor: not-allowed;
}
@ -292,43 +358,51 @@
.loading-block {
text-align: center;
margin-top: 20px;
color: #666;
font-size: 13px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
width: 36px;
height: 36px;
border: 2px solid rgba(0,255,255,0.15);
border-top: 2px solid #00FFFF;
border-radius: 50%;
animation: spin 1s linear infinite;
animation: ctb-spin 0.8s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
@keyframes ctb-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error */
.error-block {
color: #e74c3c;
background: #fdf2f2;
padding: 15px;
border-radius: 8px;
color: #FF0040;
background: rgba(255,0,64,0.08);
border: 1px solid rgba(255,0,64,0.3);
padding: 14px;
border-radius: 6px;
margin-top: 20px;
font-size: 13px;
}
/* Result */
.result-block {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
background: rgba(0,255,65,0.04);
border: 1px solid rgba(0,255,65,0.2);
border-radius: 8px;
}
.result-block h3 {
margin-bottom: 15px;
color: #333;
color: #00FF41;
font-family: 'Orbitron', monospace;
font-size: 14px;
letter-spacing: 1px;
}
.result-block audio {
@ -339,10 +413,17 @@
.download-btn {
display: inline-block;
margin-top: 15px;
padding: 10px 20px;
background: #4caf50;
color: white;
padding: 9px 20px;
background: rgba(0,255,65,0.1);
border: 1px solid #00FF41;
color: #00FF41;
text-decoration: none;
border-radius: 8px;
font-size: 14px;
border-radius: 6px;
font-size: 13px;
font-family: 'Share Tech Mono', monospace;
transition: all 0.2s;
}
.download-btn:hover {
background: rgba(0,255,65,0.2);
}

View file

@ -249,7 +249,7 @@ const ChatterboxTTS = () => {
{showVoiceText && (
<div className="voice-text">
📢 Произнесите, например: <span style={{ color: '#667eea' }}>"Хакатон 2026 походим модуль deepfake, кейс Цент Инвест команда Атейкин"</span>
📢 Произнесите, например: <span style={{ color: '#00FFFF' }}>"Хакатон 2026 походим модуль deepfake, кейс Цент Инвест команда Атейкин"</span>
</div>
)}
@ -274,7 +274,7 @@ const ChatterboxTTS = () => {
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDrop}
style={isDragOver ? { borderColor: '#667eea' } : undefined}
style={isDragOver ? { borderColor: '#00FFFF' } : undefined}
>
<input ref={fileInputRef} type="file" accept="audio/*" onChange={handleFileChange} style={{ display: 'none' }} />
<div>{fileName ? '✅ Файл выбран' : '📁 Кликни или перетащи аудио файл'}</div>

View file

@ -1,36 +1,44 @@
.voice-cards {
display: flex;
flex-direction: column;
gap: 24px;
gap: 20px;
margin-top: 8px;
}
/* ── Карточка-сценарий ── */
.voice-card {
background: #1e1e2e;
border-radius: 16px;
background: #080818;
border: 1px solid rgba(0,255,255,0.15);
border-radius: 10px;
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,.35);
box-shadow: 0 4px 20px rgba(0,0,0,.4);
transition: border-color 0.2s;
}
.voice-card:hover {
border-color: rgba(0,255,255,0.3);
}
.voice-card-scenario {
font-size: 11px;
font-weight: 700;
letter-spacing: .08em;
letter-spacing: .1em;
text-transform: uppercase;
color: #a78bfa;
color: #00FFFF;
font-family: 'Orbitron', monospace;
}
.voice-card-quote {
font-size: 13px;
color: #c4c4d4;
line-height: 1.5;
border-left: 3px solid #7c3aed;
color: #aaa;
line-height: 1.6;
border-left: 2px solid #00FFFF;
padding-left: 10px;
margin: 0;
font-family: 'Share Tech Mono', monospace;
}
/* ── Аудиосообщение (Telegram-стиль) ── */
@ -38,7 +46,8 @@
display: flex;
align-items: center;
gap: 10px;
background: #2a2a3e;
background: #0d0d22;
border: 1px solid rgba(0,255,255,0.1);
border-radius: 22px;
padding: 10px 14px;
}
@ -49,38 +58,42 @@
height: 40px;
min-width: 40px;
border-radius: 50%;
border: none;
background: #7c3aed;
color: #fff;
border: 1px solid #00FFFF;
background: rgba(0,255,255,0.1);
color: #00FFFF;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background .15s;
transition: all 0.15s;
position: relative;
}
.audio-msg-btn:hover { background: #6d28d9; }
.audio-msg-btn.loading {
background: #3f3f5a;
cursor: default;
animation: pulse 1.2s ease-in-out infinite;
.audio-msg-btn:hover {
background: rgba(0,255,255,0.2);
box-shadow: 0 0 12px rgba(0,255,255,0.3);
}
@keyframes pulse {
.audio-msg-btn.loading {
border-color: rgba(0,255,255,0.3);
background: rgba(0,255,255,0.05);
cursor: default;
animation: vc-pulse 1.2s ease-in-out infinite;
}
@keyframes vc-pulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; }
50% { opacity: .4; }
}
/* Спиннер внутри кнопки */
.audio-spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255,255,255,.3);
border-top-color: #fff;
width: 16px;
height: 16px;
border: 2px solid rgba(0,255,255,0.2);
border-top-color: #00FFFF;
border-radius: 50%;
animation: spin .7s linear infinite;
animation: vc-spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes vc-spin { to { transform: rotate(360deg); } }
/* Волна + прогресс */
.audio-msg-wave {
@ -102,42 +115,45 @@
.waveform-bar {
flex: 1;
border-radius: 2px;
background: #4b4b6e;
background: rgba(0,255,255,0.15);
transition: background .1s;
}
.waveform-bar.played {
background: #7c3aed;
background: #00FFFF;
}
.audio-msg-time {
font-size: 11px;
color: #6b6b8a;
color: #555;
font-variant-numeric: tabular-nums;
font-family: 'Share Tech Mono', monospace;
}
/* Кнопка перегенерации */
.regen-btn {
background: none;
border: 1px solid #3f3f5a;
color: #a0a0c0;
border: 1px solid rgba(0,255,255,0.2);
color: #666;
border-radius: 20px;
padding: 5px 12px;
font-size: 12px;
font-size: 11px;
cursor: pointer;
align-self: flex-end;
font-family: 'Share Tech Mono', monospace;
transition: border-color .15s, color .15s;
}
.regen-btn:hover:not(:disabled) {
border-color: #7c3aed;
color: #a78bfa;
border-color: #00FFFF;
color: #00FFFF;
}
.regen-btn:disabled { opacity: .4; cursor: default; }
.regen-btn:disabled { opacity: .3; cursor: default; }
/* Ошибка */
.voice-card-error {
font-size: 12px;
color: #f87171;
color: #FF0040;
display: flex;
align-items: center;
gap: 6px;
font-family: 'Share Tech Mono', monospace;
}