studio/src/community/KubikonRules.jsx
МИН 61fba4e174
Some checks failed
CI / Lint + Format (push) Failing after 32s
CI / Build (push) Failing after 37s
CI / Secret scan (push) Failing after 37s
CI / PR size check (push) Has been skipped
fix: починка билда + studio.rublox.pro инфра
Большой консолидирующий коммит после поднятия studio.rublox.pro (28 мая 2026).
Содержит изменения которые делались в процессе подготовки прод-окружения:

Фиксы импортов после выноса из minecraftia:
- Массовая замена путей ../../components → ../components (40+ файлов в src/community/, src/admin-preview/)
- Замена ../KubikonEditor/ → ../editor/, ../KubikonStudio/ → ../community/, ../AdminPreview/ → ../admin-preview/
- API.js скопирован из минки целиком (было 8 экспортов, стало 312)
- Добавлены PLAYER_URL, MyButton_1, недостающие компоненты
- Заменены require() на статические ES-imports в BabylonScene, PrimitiveManager, GameRuntime (Vite не поддерживает CJS require)

Структура ассетов:
- public/kubikon-templates/ → public/assets/kubikon-templates/
- public/kubikon-learn/ → public/assets/kubikon-learn/
- (код искал в /assets/, файлы лежали без /assets/)

Навигация роутов внутри студии:
- /kubikon-studio/docs → /docs (90+ навигационных вызовов sed-replaced)
- /kubikon-editor/X → /edit/X, /kubikon/play/X → /play/X, /kubikon/gd/X → /gd/X

UI:
- Новый компонент StudioHeader (61px, как в минке) + копия favicon
- WithHeader wrapper в App.jsx для всех страниц кроме fullscreen-редактора/плеера
- SSO ticket-flow в AuthContext (auto-redeem #ticket= при загрузке)
- Тёмная тема карточек игр в ВИКИ (фон #1c2231 вместо #fff, картинка впритык)

Документация:
- docs/ONBOARDING.md — путь нового контрибьютора от нуля до PR
- docs/TUTORIAL_ADD_SCRIPT_API.md — как добавить game.* API
- API_USAGE.md — список эндпоинтов backend
- README в подпапках engine/, engine/terrain/, engine/voxel/, engine/robloxterrain/, engine/types/

.gitignore:
- public/wiki/ исключён (73МБ PNG, будут на CDN отдельной задачей)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 05:01:13 +03:00

986 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import cl from './KubikonStudio.module.css';
import RublocsLogo from '../components/RublocsLogo/RublocsLogo';
import useDeviceType from '../hooks/useDeviceType';
import KubikonDesktopOnlyStub from './KubikonDesktopOnlyStub';
import Icon from '../editor/Icon';
import DocIcon from './docsIcons';
/**
* KubikonRules — «Правила создания игр» платформы Рублокс.
* URL: /rules
*
* Отдельная страница: подробно расписано, какие игры можно создавать
* и публиковать, чтобы их не заблокировали и они были популярными.
* Опирается на реальную систему умной ленты Рублокса:
* - автопроверка скриптов при публикации (опасные → очередь review);
* - статусы игр draft / published / review / blocked;
* - алгоритм ленты hot_score (Wilson-рейтинг + вовлечённость +
* популярность + свежесть);
* - мягкое автоскрытие (demoted) игр с низким рейтингом.
*
* Стиль и вёрстка повторяют вику (KubikonDocs.jsx): своя боковая
* панель, hero, разделы с оглавлением. Все стили — в INLINE_STYLES,
* чтобы страница была самодостаточной. Иконки — самописные SVG.
*/
// ── Маленькие хелперы разметки ──────────────────────────────────────
// Плашка «можно» — зелёная
const Ok = ({ children }) => (
<div className="ruleBox ruleBox--ok">
<span className="ruleBox__ico"><DocIcon name="flag" size={18} /></span>
<div>{children}</div>
</div>
);
// Плашка «нельзя» — красная
const No = ({ children }) => (
<div className="ruleBox ruleBox--no">
<span className="ruleBox__ico"><DocIcon name="warning" size={18} /></span>
<div>{children}</div>
</div>
);
// Жёлтая плашка-подсказка
const Tip = ({ children }) => (
<div className="ruleBox ruleBox--tip">
<span className="ruleBox__ico"><DocIcon name="lightbulb" size={18} /></span>
<div>{children}</div>
</div>
);
// Карточка статуса игры
const Status = ({ color, name, children }) => (
<div className="statusRow">
<span className="statusDot" style={{ background: color }} />
<div>
<b className="statusName">{name}</b>
<span className="statusText"> {children}</span>
</div>
</div>
);
// ── Контент: разделы правил ─────────────────────────────────────────
// Каждый раздел: { id, icon, title, body }. body — готовый JSX.
const RULES = [
{
id: 'how-publish',
icon: 'rocket',
title: 'Как игра попадает в ленту',
body: (
<>
<p>
В Рублоксе <b>нет предварительной модерации каждой игры
человеком</b>. Ты сам нажимаешь «Опубликовать» и игра
почти сразу появляется в общей ленте. Это значит свободу,
но и ответственность: правила нужно соблюдать самому.
</p>
<p>Что происходит, когда ты жмёшь «Опубликовать»:</p>
<ol>
<li>
Игра автоматически проверяется система смотрит
твои скрипты на опасный код.
</li>
<li>
Если всё чисто игра <b>сразу в ленте</b>, её видят
все игроки.
</li>
<li>
Если в скриптах нашлось что-то подозрительное игра
уходит на ручную проверку к админу (это бывает
редко). После проверки её либо публикуют, либо
отправляют на доработку.
</li>
</ol>
<Tip>
Пока игра в статусе «черновик», её видишь только ты.
Тестируй сколько угодно публикуй, только когда игра
реально готова и в неё интересно играть.
</Tip>
</>
),
},
{
id: 'statuses',
icon: 'flag',
title: 'Статусы игры — что они значат',
body: (
<>
<p>У каждой твоей игры есть статус. Вот что они означают:</p>
<div className="statusList">
<Status color="#94a3b8" name="Черновик">
игра ещё не опубликована, её видишь только ты.
Можно свободно тестировать и переделывать.
</Status>
<Status color="#16a34a" name="Опубликована">
игра в общей ленте, в неё играют все. Главное
состояние успешной игры.
</Status>
<Status color="#f59e0b" name="На проверке">
в игре нашли подозрительные скрипты, её смотрит
админ. Пока проверяет игра не в ленте.
</Status>
<Status color="#dc2626" name="Заблокирована">
игра нарушила правила. Её убрали из ленты. Причину
админ пишет в комментарии исправь и можно
опубликовать заново.
</Status>
</div>
<p>
Отдельно есть «мягкое скрытие»: даже опубликованная игра
может <b>выпасть из ленты</b>, если в неё совсем не
играют или ставят сплошные дизлайки. Она не блокируется
её по-прежнему можно найти поиском и открыть по ссылке,
но в рекомендациях её не показывают. Подробнее в
разделе «Как стать популярным».
</p>
</>
),
},
{
id: 'allowed',
icon: 'sparkles',
title: 'Какие игры можно публиковать',
body: (
<>
<p>
Рублокс площадка для <b>детей и подростков</b>. Можно
публиковать любые игры, в которые было бы не стыдно
поиграть на уроке. Примеры хороших жанров:
</p>
<Ok>
<b>Платформеры и паркур</b> прыгай по платформам,
доберись до финиша, собирай монетки.
</Ok>
<Ok>
<b>Гонки и аркады</b> проедь трассу на время, обгони
соперников, уворачивайся от препятствий.
</Ok>
<Ok>
<b>Головоломки и квесты</b> лабиринты, кнопки и двери,
логические загадки.
</Ok>
<Ok>
<b>Тиры и «выживалки»</b> стрельба по мишеням,
мультяшные зомби, защита базы. Бой допустим, если он
мультяшный и без крови.
</Ok>
<Ok>
<b>Симуляторы и «тайкуны»</b> построй город, ферму,
магазин; собирай ресурсы, прокачивай постройки.
</Ok>
<Ok>
<b>Ролевые игры и приключения</b> исследуй мир,
говори с NPC, выполняй задания.
</Ok>
<Ok>
<b>Мини-игры и обучалки</b> викторины, игры на
реакцию, игры, которые чему-то учат.
</Ok>
<Tip>
Не знаешь, с чего начать открой вику, раздел «50
игр-уроков». Там готовые игры разных жанров с пошаговыми
инструкциями: бери за основу и делай свою.
</Tip>
</>
),
},
{
id: 'forbidden-content',
icon: 'warning',
title: 'Что публиковать запрещено — контент',
body: (
<>
<p>
Игру <b>заблокируют</b>, если в ней есть что-то из
списка ниже. Это правила про содержание игры текст,
картинки, название, тему.
</p>
<No>
<b>Жестокость и кровь.</b> Реалистичное насилие, кровь,
расчленёнка, сцены смерти людей. Мультяшный «бой» без
крови можно, реализм нельзя.
</No>
<No>
<b>Взрослый контент.</b> Любая эротика, намёки на секс,
раздетые персонажи. Площадка детская это под строгим
запретом.
</No>
<No>
<b>Мат и оскорбления.</b> Нецензурная брань в названии,
описании, в текстах внутри игры, на табличках и в
репликах NPC.
</No>
<No>
<b>Вражда и травля.</b> Оскорбление людей по
национальности, религии, полу; призывы кого-то
травить; буллинг конкретных игроков.
</No>
<No>
<b>Опасное и противозаконное.</b> Пропаганда наркотиков,
алкоголя, курения, оружия; инструкции, как навредить
себе или другим.
</No>
<No>
<b>Страшный «шок-контент».</b> Скример-игры, цель
которых напугать; крик и страшные лица «в лоб».
Лёгкая «страшилка» в мультяшном стиле можно.
</No>
<No>
<b>Реклама и обман.</b> Реклама посторонних сайтов,
«накрутка», просьбы прислать деньги или данные аккаунта,
фейковые «розыгрыши».
</No>
<No>
<b>Чужое.</b> Нельзя выдавать чужую игру за свою.
Скопировал чужую игру целиком и опубликовал как свою
это нарушение.
</No>
<Tip>
Простое правило: если ты не показал бы эту игру учителю
или родителям её не стоит публиковать.
</Tip>
</>
),
},
{
id: 'forbidden-scripts',
icon: 'shield',
title: 'Что запрещено в скриптах',
body: (
<>
<p>
Скрипты в игре выполняются в <b>песочнице</b> у них нет
доступа к «настоящему» интернету и к чужим данным. При
публикации система автоматически проверяет код. Если она
находит опасные команды игра уходит на ручную проверку.
</p>
<p>В скриптах нельзя использовать:</p>
<No>
<b>Выполнение «кода из строки»</b> <code>eval</code>,
{' '}<code>Function(...)</code>. Так прячут вредоносный
код, поэтому это запрещено.
</No>
<No>
<b>Сетевые запросы</b> <code>fetch</code>,
{' '}<code>XMLHttpRequest</code>, WebSocket к чужим
адресам. Игра не должна сама лазить в интернет.
</No>
<No>
<b>Доступ к странице браузера</b> <code>window</code>,
{' '}<code>document</code>, <code>localStorage</code>,
{' '}<code>cookie</code>. Скрипт игры работает только с
игровым миром через <code>game.*</code>, не со страницей.
</No>
<No>
<b>Попытки «зависить» игру нарочно</b> бесконечные
циклы без выхода, создание миллионов объектов, чтобы
у других всё затормозило.
</No>
<Tip>
Всё, что нужно для игры, уже есть в безопасном API
{' '}<code>game.*</code>: движение игрока, спавн
объектов, очки, звуки, интерфейс. Полный список в вике,
раздел про скрипты. Если пишешь игру по урокам из вики
запрещённый код туда просто не попадёт.
</Tip>
<p>
Важно: автопроверка не наказывает за случайность. Если
игра честная, а слово просто совпало админ увидит это
при ручной проверке и опубликует игру.
</p>
</>
),
},
{
id: 'reports',
icon: 'bug',
title: 'Жалобы игроков и блокировки',
body: (
<>
<p>
Под каждой игрой есть кнопка <b>«Пожаловаться»</b>. Если
игроки массово жалуются на твою игру её посмотрит
админ. Найдёт нарушение заблокирует.
</p>
<p>За что чаще всего прилетает жалоба и блокировка:</p>
<ul>
<li>в игре мат, грубость или обидные надписи;</li>
<li>
игра пустышка: зашёл, а играть не во что (нет цели,
нет геймплея);
</li>
<li>
название и обложка обещают одно, а в игре совсем
другое (обман игрока);
</li>
<li>игра намеренно тормозит или «вылетает»;</li>
<li>это копия чужой игры.</li>
</ul>
<p>Если твою игру заблокировали:</p>
<ol>
<li>прочитай комментарий админа там написана причина;</li>
<li>исправь то, на что указали;</li>
<li>опубликуй игру заново.</li>
</ol>
<No>
Не пытайся обойти блокировку: переименовать игру и залить
то же самое снова, спамить одинаковыми играми, делать
несколько аккаунтов. За это блокируют уже не игру, а
аккаунт.
</No>
</>
),
},
{
id: 'popular',
icon: 'trophy',
title: 'Как сделать игру популярной',
body: (
<>
<p>
В ленте Рублокса игры показываются не по порядку, а по
«рейтингу интереса». Чем интереснее игра, тем выше она в
рекомендациях. На рейтинг влияют четыре вещи:
</p>
<div className="factorList">
<div className="factorRow">
<span className="factorIco"><DocIcon name="star" size={18} /></span>
<div>
<b>Лайки и дизлайки.</b> Игроки голосуют после
игры. Важно не само число лайков, а их доля:
маленькая честная игра с одними лайками
обгоняет большую с дизлайками.
</div>
</div>
<div className="factorRow">
<span className="factorIco"><DocIcon name="run" size={18} /></span>
<div>
<b>Сколько в неё играют.</b> Чем больше игрок
проводит времени в игре и не закрывает её через
10 секунд тем выше рейтинг.
</div>
</div>
<div className="factorRow">
<span className="factorIco"><DocIcon name="globeIcon" size={18} /></span>
<div>
<b>Количество запусков.</b> Сколько раз игру
вообще открыли. Популярные игры поднимаются выше.
</div>
</div>
<div className="factorRow">
<span className="factorIco"><DocIcon name="sparkles" size={18} /></span>
<div>
<b>Свежесть.</b> Новым играм лента даёт «фору»
их какое-то время показывают чаще, чтобы их
успели заметить.
</div>
</div>
</div>
<p>
Что реально делать, чтобы игра нравилась людям:
</p>
<Tip>
<b>Дай игроку понятную цель.</b> С первых секунд должно
быть ясно: куда идти и что делать. «Добеги до флага»,
«собери 10 монет», «победи всех зомби».
</Tip>
<Tip>
<b>Сделай красивую обложку и честное название.</b> По
карточке игрок решает, заходить ли. Обложка должна
показывать саму игру, а не что-то постороннее.
</Tip>
<Tip>
<b>Добавь звук.</b> Игра без звука кажется «мёртвой».
Звук на прыжок, на монетку, на победу и проигрыш и
игра сразу живее.
</Tip>
<Tip>
<b>Сделай так, чтобы было не слишком сложно и не слишком
скучно.</b> Начало лёгкое, дальше постепенно сложнее.
Если игрок умирает на первой секунде, он закроет игру.
</Tip>
<Tip>
<b>Проверь игру перед публикацией.</b> Пройди её сам от
начала до конца. Нет ли мест, где застреваешь, не падаешь
ли сквозь пол, доходит ли дело до победы.
</Tip>
<Tip>
<b>Обновляй игру.</b> Чини баги, добавляй уровни. Лента
любит игры, которые живут и развиваются.
</Tip>
<No>
Не пытайся «накрутить» рейтинг: лайкать свою игру с
кучи аккаунтов, просить друзей спамить запуски. Система
это замечает, и игра наоборот падает в ленте.
</No>
</>
),
},
{
id: 'checklist',
icon: 'target',
title: 'Чек-лист перед публикацией',
body: (
<>
<p>
Пройдись по списку перед тем, как нажать «Опубликовать».
Если на все пункты ответ «да» игру можно публиковать.
</p>
<div className="checkList">
{[
'В игре есть понятная цель, и игрок сразу её понимает.',
'Игру можно пройти от начала до конца — я проверил сам.',
'Нигде нет мата, грубостей и обидных надписей.',
'Нет жестокости, крови и взрослого контента.',
'Название и обложка честно показывают, что это за игра.',
'Игра не тормозит и не вылетает.',
'В игре есть звук на главные события.',
'Это моя игра, а не копия чужой.',
'Я не использовал запрещённый код в скриптах.',
].map((t, i) => (
<div key={i} className="checkItem">
<span className="checkBox">
<Icon name="check" size={12} />
</span>
<span>{t}</span>
</div>
))}
</div>
<Tip>
Сомневаешься, можно ли публиковать игру лучше спроси
учителя или оставь её черновиком. Заблокированная игра
портит твой профиль, а исправить и переопубликовать
можно всегда.
</Tip>
</>
),
},
];
const KubikonRules = () => {
const navigate = useNavigate();
const { isDesktop } = useDeviceType();
const mainRef = useRef(null);
// активный раздел для подсветки в оглавлении
const [activeSec, setActiveSec] = useState(RULES[0].id);
// подсветка активного раздела при скролле
useEffect(() => {
const main = mainRef.current;
if (!main) return;
const onScroll = () => {
const top = main.scrollTop + 140;
let cur = RULES[0].id;
for (const r of RULES) {
const el = main.querySelector(`#rule-${r.id}`);
if (el && el.offsetTop <= top) cur = r.id;
}
setActiveSec(cur);
};
main.addEventListener('scroll', onScroll);
return () => main.removeEventListener('scroll', onScroll);
}, []);
if (!isDesktop) {
return <KubikonDesktopOnlyStub feature="Правила создания игр Рублокс" />;
}
const scrollToSec = (id) => {
const main = mainRef.current;
const el = main && main.querySelector(`#rule-${id}`);
if (el) main.scrollTo({ top: el.offsetTop - 16, behavior: 'smooth' });
};
return (
<div className={cl.studio}>
<style>{INLINE_STYLES}</style>
{/* === Левая боковая панель === */}
<aside className={cl.sidebar}>
<div className={cl.sidebarHeader}>
<div className={cl.sidebarLogo}>
<RublocsLogo size={44} bg="#16181d" />
</div>
<div className={cl.sidebarTitle}>
<div className={cl.brandName}>Рублокс</div>
<div className={cl.brandSub}>Студия</div>
</div>
</div>
{/* Меню — единое со страницей Studio (7 пунктов). */}
<nav className={cl.sidebarNav}>
<button
className={cl.navNewGame}
onClick={() => navigate('/edit/new')}
>
<span className={cl.navNewGameIco}><Icon name="plus" size={15} /></span>
<span>Новая игра</span>
</button>
<button className={cl.navItem} onClick={() => navigate('/?tab=recent')}>
<span className={cl.navIcon}><Icon name="clock" size={15} /></span>
<span>История</span>
</button>
<button className={cl.navItem} onClick={() => navigate('/?tab=home')}>
<span className={cl.navIcon}><Icon name="home" size={15} /></span>
<span>Главная</span>
</button>
<button className={cl.navItem} onClick={() => navigate('/?tab=my')}>
<span className={cl.navIcon}><Icon name="folder" size={15} /></span>
<span>Мои игры</span>
</button>
<button className={cl.navItem} onClick={() => navigate('/?tab=templates')}>
<span className={cl.navIcon}><Icon name="grid" size={15} /></span>
<span>Шаблоны</span>
</button>
<button className={cl.navItem} onClick={() => navigate('/?tab=archive')}>
<span className={cl.navIcon}><Icon name="archive" size={15} /></span>
<span>Архив</span>
</button>
<button className={cl.navItem} onClick={() => navigate('/docs')}>
<span className={cl.navIcon}><Icon name="book-open" size={15} /></span>
<span>ВИКИ</span>
</button>
</nav>
<div className={cl.sidebarFooter}>
<button
className={cl.docsBtn}
style={{
background: '#3357ff', color: '#fff', borderColor: 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
}}
onClick={() => navigate('/rules')}
>
<Icon name="shield" size={15} /> Правила создания игр
</button>
</div>
</aside>
{/* === Основной контент === */}
<main className={cl.main} ref={mainRef}>
<header className={cl.topBar}>
<div>
<h1 className={cl.pageTitle}>
<Icon name="shield" size={13} /> Правила создания игр
</h1>
<p className={cl.pageSub}>
Какие игры можно публиковать, чтобы их не
заблокировали и в них играли
</p>
</div>
<div className={cl.topBarActions}>
<button className={cl.searchBox} onClick={() => navigate('/')}>
В Studio
</button>
</div>
</header>
{/* Hero */}
<section className="rulesHero">
<div className="rulesHeroContent">
<div className="rulesHeroBadge">
<DocIcon name="shield" size={13} /> Правила платформы
</div>
<h2 className="rulesHeroTitle">
Создавай игры, которые полюбят
</h2>
<p className="rulesHeroDesc">
В Рублоксе ты публикуешь игры сам, без долгой
модерации. Эти правила помогут: твою игру не
заблокируют, а игроки будут в неё играть.
Прочитай их один раз и всё будет получаться.
</p>
</div>
<div className="rulesHeroEmoji"><DocIcon name="shield" size={88} /></div>
</section>
{/* Body: оглавление + разделы */}
<section className="rulesBody">
<aside className="rulesToc">
<div className="rulesTocTitle">
<DocIcon name="scroll" size={16} /> Разделы
</div>
{RULES.map((r) => (
<button
key={r.id}
className={`rulesTocItem ${activeSec === r.id ? 'rulesTocItemActive' : ''}`}
onClick={() => scrollToSec(r.id)}
>
{r.title}
</button>
))}
</aside>
<div className="rulesContent">
{RULES.map((r) => (
<article key={r.id} id={`rule-${r.id}`} className="ruleChapter">
<h3 className="ruleTitle">
<span className="ruleTitle__ico">
<DocIcon name={r.icon} size={20} />
</span>
{r.title}
</h3>
<div className="ruleText">{r.body}</div>
</article>
))}
{/* Финальный CTA */}
<div className="rulesCta">
<div className="rulesCtaIcon"><DocIcon name="rocket" size={48} /></div>
<div className="rulesCtaTitle">Готов создавать?</div>
<div className="rulesCtaText">
Открой вику с 50 играми-уроками там готовые
игры с инструкциями. Бери за основу и делай
свою по этим правилам.
</div>
<button
className="rulesCtaBtn"
onClick={() => navigate('/docs')}
>
Открыть вику
</button>
</div>
</div>
</section>
</main>
</div>
);
};
// ══════════════════════════════════════════════════════════════════
// Инлайн-стили — страница самодостаточна, не зависит от вики.
// ══════════════════════════════════════════════════════════════════
const INLINE_STYLES = `
.rulesHero {
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 60%, #6d28d9 100%);
background-size: 200% 200%;
animation: rulesGradient 16s ease-in-out infinite;
border-radius: 28px;
padding: 36px 40px;
margin-bottom: 28px;
color: #fff;
display: flex;
align-items: center;
gap: 24px;
box-shadow: 0 24px 56px rgba(51, 87, 255, 0.28);
}
@keyframes rulesGradient {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.rulesHeroContent { flex: 1; min-width: 0; }
.rulesHeroBadge {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.20);
border: 1px solid rgba(255, 255, 255, 0.30);
color: #fff;
padding: 5px 14px;
border-radius: 999px;
font-size: 11px;
font-weight: 800;
letter-spacing: 1.4px;
text-transform: uppercase;
margin-bottom: 12px;
}
.rulesHeroTitle {
margin: 0 0 10px;
font-size: 32px;
font-weight: 900;
color: #fff;
letter-spacing: -1px;
line-height: 1.1;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
}
.rulesHeroDesc {
margin: 0;
font-size: 15px;
color: rgba(255, 255, 255, 0.94);
line-height: 1.55;
max-width: 680px;
}
.rulesHeroEmoji {
flex-shrink: 0;
color: #fff;
opacity: 0.92;
animation: rulesFloat 6s ease-in-out infinite;
filter: drop-shadow(0 12px 24px rgba(0, 0, 0, 0.20));
}
@keyframes rulesFloat {
0%, 100% { transform: translateY(0) rotate(-2deg); }
50% { transform: translateY(-10px) rotate(3deg); }
}
/* === Body: оглавление + контент === */
.rulesBody {
display: grid;
grid-template-columns: 250px minmax(0, 1fr);
gap: 28px;
align-items: flex-start;
}
.rulesToc {
position: sticky;
top: 8px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 12px 8px;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
}
.rulesTocTitle {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px 10px;
font-size: 13px;
font-weight: 900;
color: #0f172a;
border-bottom: 1px solid #eef2f7;
margin-bottom: 6px;
}
.rulesTocTitle svg { color: #3357ff; flex-shrink: 0; }
.rulesTocItem {
display: block;
width: 100%;
padding: 8px 12px;
background: transparent;
border: none;
border-radius: 9px;
color: #475569;
font-size: 12.5px;
font-weight: 700;
cursor: pointer;
text-align: left;
transition: all 160ms ease;
font-family: inherit;
line-height: 1.4;
}
.rulesTocItem:hover { background: #f1f5f9; color: #0f172a; }
.rulesTocItemActive, .rulesTocItemActive:hover {
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
color: #fff;
}
.rulesContent {
min-width: 0;
display: flex;
flex-direction: column;
gap: 18px;
}
/* === Раздел правил === */
.ruleChapter {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 18px;
padding: 24px 28px;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
scroll-margin-top: 16px;
}
.ruleTitle {
display: flex;
align-items: center;
gap: 12px;
margin: 0 0 16px;
font-size: 20px;
font-weight: 900;
color: #0f172a;
letter-spacing: -0.4px;
padding-bottom: 12px;
border-bottom: 1px solid #eef2f7;
}
.ruleTitle__ico {
flex-shrink: 0;
width: 38px; height: 38px;
border-radius: 11px;
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
color: #fff;
display: flex; align-items: center; justify-content: center;
}
.ruleText { color: #334155; font-size: 14.5px; line-height: 1.65; }
.ruleText p { margin: 0 0 12px; }
.ruleText p:last-child { margin-bottom: 0; }
.ruleText ul, .ruleText ol { margin: 0 0 12px; padding-left: 22px; }
.ruleText li { margin-bottom: 8px; line-height: 1.55; }
.ruleText li:last-child { margin-bottom: 0; }
.ruleText b { color: #0f172a; font-weight: 800; }
.ruleText code {
background: #e0e8ff;
color: #3357ff;
padding: 2px 7px;
border-radius: 6px;
font-family: Consolas, Menlo, "Courier New", monospace;
font-size: 13px;
font-weight: 700;
}
/* === Плашки можно / нельзя / совет === */
.ruleBox {
display: flex;
gap: 11px;
align-items: flex-start;
border-radius: 11px;
padding: 12px 14px;
margin: 10px 0;
font-size: 13.5px;
line-height: 1.55;
border: 1px solid;
border-left-width: 4px;
}
.ruleBox__ico { flex-shrink: 0; line-height: 0; margin-top: 1px; }
.ruleBox--ok {
background: #f0fdf4;
border-color: #bbf7d0;
border-left-color: #16a34a;
color: #14532d;
}
.ruleBox--ok .ruleBox__ico { color: #16a34a; }
.ruleBox--no {
background: #fef2f2;
border-color: #fecaca;
border-left-color: #dc2626;
color: #7f1d1d;
}
.ruleBox--no .ruleBox__ico { color: #dc2626; }
.ruleBox--no code { background: #fee2e2; color: #b91c1c; }
.ruleBox--tip {
background: #fffbeb;
border-color: #fde68a;
border-left-color: #f59e0b;
color: #78350f;
}
.ruleBox--tip .ruleBox__ico { color: #b45309; }
.ruleBox--tip code { background: #fef3c7; color: #92400e; }
/* === Список статусов === */
.statusList {
display: flex;
flex-direction: column;
gap: 4px;
margin: 12px 0;
}
.statusRow {
display: flex;
gap: 11px;
align-items: flex-start;
background: #f8fafc;
border: 1px solid #eef2f7;
border-radius: 10px;
padding: 11px 14px;
}
.statusDot {
flex-shrink: 0;
width: 12px; height: 12px;
border-radius: 50%;
margin-top: 4px;
}
.statusName { color: #0f172a; font-weight: 800; }
.statusText { color: #475569; }
/* === Факторы рейтинга === */
.factorList {
display: flex;
flex-direction: column;
gap: 8px;
margin: 12px 0;
}
.factorRow {
display: flex;
gap: 12px;
align-items: flex-start;
background: #f5f7ff;
border: 1px solid #e0e8ff;
border-radius: 11px;
padding: 12px 14px;
}
.factorIco {
flex-shrink: 0;
width: 32px; height: 32px;
border-radius: 9px;
background: #fff;
border: 1px solid #d6e0ff;
color: #3357ff;
display: flex; align-items: center; justify-content: center;
}
/* === Чек-лист === */
.checkList {
display: flex;
flex-direction: column;
gap: 6px;
margin: 12px 0;
}
.checkItem {
display: flex;
gap: 11px;
align-items: center;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 10px;
padding: 10px 14px;
font-size: 13.5px;
color: #14532d;
line-height: 1.45;
}
.checkBox {
flex-shrink: 0;
width: 20px; height: 20px;
border-radius: 6px;
background: #16a34a;
color: #fff;
display: flex; align-items: center; justify-content: center;
}
/* === Финальный CTA === */
.rulesCta {
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
border-radius: 24px;
padding: 32px;
text-align: center;
color: #fff;
box-shadow: 0 16px 40px rgba(51, 87, 255, 0.32);
margin-top: 8px;
}
.rulesCtaIcon {
display: flex;
justify-content: center;
color: #fff;
margin-bottom: 10px;
}
.rulesCtaTitle { font-size: 22px; font-weight: 900; margin-bottom: 8px; }
.rulesCtaText {
font-size: 14px;
color: rgba(255, 255, 255, 0.90);
margin: 0 auto 20px;
max-width: 520px;
line-height: 1.55;
}
.rulesCtaBtn {
padding: 12px 26px;
background: #fff;
color: #3357ff;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 900;
cursor: pointer;
font-family: inherit;
}
.rulesCtaBtn:hover { transform: translateY(-2px); }
/* Адаптив */
@media (max-width: 980px) {
.rulesBody { grid-template-columns: 1fr; }
.rulesToc { position: static; }
.rulesHero { flex-direction: column; padding: 28px 24px; text-align: center; }
.ruleChapter { padding: 20px; }
}
`;
export default KubikonRules;