first commit - static hello page

This commit is contained in:
Илья Братских 2026-06-24 09:57:27 +03:00 committed by KBAHTNET
commit d86f7f1952
7 changed files with 810 additions and 0 deletions

16
assets/gitea.svg Normal file
View File

@ -0,0 +1,16 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="160" height="160" rx="44" fill="url(#bg)"/>
<path d="M49 60h53c16 0 28 12 28 28s-12 28-28 28H73c-22 0-40-18-40-40 0-9 7-16 16-16z" fill="white"/>
<path d="M65 76h34c8 0 14 6 14 14s-6 14-14 14H78c-13 0-24-11-24-24 0-2 1-4 4-4h7z" fill="#5D3A1A" fill-opacity=".16"/>
<circle cx="67" cy="86" r="7" fill="#5D3A1A"/>
<circle cx="101" cy="86" r="7" fill="#5D3A1A"/>
<path d="M75 102c7 5 16 5 24 0" stroke="#5D3A1A" stroke-width="6" stroke-linecap="round"/>
<path d="M103 57l13-13M113 45h14v14" stroke="#FEF3C7" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M49 60c-5-13-4-23 3-30 9 5 15 13 18 24" fill="white"/>
<defs>
<linearGradient id="bg" x1="18" y1="14" x2="140" y2="150" gradientUnits="userSpaceOnUse">
<stop stop-color="#A3E635"/>
<stop offset="1" stop-color="#65A30D"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 987 B

14
assets/mail.svg Normal file
View File

@ -0,0 +1,14 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="160" height="160" rx="44" fill="url(#bg)"/>
<rect x="32" y="45" width="96" height="70" rx="20" fill="white"/>
<path d="M38 55l35.5 29.5a10 10 0 0013 0L122 55" stroke="#EA580C" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M45 108l26-25M115 108L89 83" stroke="#FDBA74" stroke-width="8" stroke-linecap="round"/>
<circle cx="119" cy="42" r="14" fill="#FDE68A"/>
<path d="M113 42h12M119 36v12" stroke="#EA580C" stroke-width="4" stroke-linecap="round"/>
<defs>
<linearGradient id="bg" x1="18" y1="14" x2="140" y2="150" gradientUnits="userSpaceOnUse">
<stop stop-color="#FB7185"/>
<stop offset="1" stop-color="#F97316"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 826 B

17
assets/nextcloud.svg Normal file
View File

@ -0,0 +1,17 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="160" height="160" rx="44" fill="url(#bg)"/>
<circle cx="80" cy="80" r="38" fill="white" fill-opacity=".18"/>
<circle cx="80" cy="80" r="23" fill="white"/>
<circle cx="43" cy="82" r="20" fill="white"/>
<circle cx="117" cy="82" r="20" fill="white"/>
<circle cx="43" cy="82" r="10" fill="#1687F2"/>
<circle cx="117" cy="82" r="10" fill="#1687F2"/>
<circle cx="80" cy="80" r="12" fill="#1687F2"/>
<path d="M37 116c22 16 64 21 90-4" stroke="white" stroke-opacity=".38" stroke-width="8" stroke-linecap="round"/>
<defs>
<linearGradient id="bg" x1="18" y1="14" x2="140" y2="150" gradientUnits="userSpaceOnUse">
<stop stop-color="#38BDF8"/>
<stop offset="1" stop-color="#2563EB"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 860 B

18
assets/test-net.svg Normal file
View File

@ -0,0 +1,18 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="160" height="160" rx="44" fill="url(#bg)"/>
<path d="M42 96l38-42 38 42" stroke="white" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M42 96h76" stroke="white" stroke-opacity=".45" stroke-width="10" stroke-linecap="round"/>
<circle cx="42" cy="96" r="16" fill="white"/>
<circle cx="80" cy="54" r="16" fill="white"/>
<circle cx="118" cy="96" r="16" fill="white"/>
<circle cx="42" cy="96" r="7" fill="#0F766E"/>
<circle cx="80" cy="54" r="7" fill="#0F766E"/>
<circle cx="118" cy="96" r="7" fill="#0F766E"/>
<path d="M55 119c16 9 35 11 55 0" stroke="#A7F3D0" stroke-width="8" stroke-linecap="round"/>
<defs>
<linearGradient id="bg" x1="18" y1="14" x2="140" y2="150" gradientUnits="userSpaceOnUse">
<stop stop-color="#34D399"/>
<stop offset="1" stop-color="#0F766E"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 984 B

166
index.html Normal file
View File

@ -0,0 +1,166 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Стартовая страница для навигации по личной экосистеме Bratskih Tech."
/>
<title>Bratskih Tech Hub</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="ambient ambient-one"></div>
<div class="ambient ambient-two"></div>
<main class="shell">
<section class="hero" aria-labelledby="page-title">
<div class="hero-content">
<p class="eyebrow">Personal ecosystem</p>
<h1 id="page-title">Bratskih Tech Hub</h1>
<p class="lead">
Быстрый старт для твоих сервисов: облако, почта, тестовая сеть и
репозитории в одном аккуратном месте.
</p>
</div>
<div class="control-panel" aria-label="Настройки внешнего вида">
<div class="control-group">
<span>Тема</span>
<button class="segmented is-active" type="button" data-theme="dark">
Темная
</button>
<button class="segmented" type="button" data-theme="light">
Светлая
</button>
</div>
<div class="control-group">
<span>Вид</span>
<button class="segmented is-active" type="button" data-view="cards">
Карточки
</button>
<button class="segmented" type="button" data-view="list">
Список
</button>
</div>
<div class="control-group accent-group">
<span>Акцент</span>
<button
class="accent-swatch is-active"
type="button"
data-accent="violet"
aria-label="Фиолетовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="cyan"
aria-label="Голубой акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="lime"
aria-label="Лаймовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="rose"
aria-label="Розовый акцент"
></button>
</div>
</div>
</section>
<section class="services cards-view" aria-label="Сервисы экосистемы">
<a
class="service-card"
href="https://nextcloud.bratskih.tech"
target="_blank"
rel="noopener noreferrer"
>
<span class="shine"></span>
<img src="assets/nextcloud.svg" alt="" loading="lazy" />
<div>
<p class="service-kicker">Cloud storage</p>
<h2>Nextcloud</h2>
<p>
Личное облако для файлов, заметок, календарей и синхронизации
между устройствами.
</p>
</div>
<span class="arrow" aria-hidden="true">→</span>
</a>
<a
class="service-card"
href="https://mail.bratskih.tech"
target="_blank"
rel="noopener noreferrer"
>
<span class="shine"></span>
<img src="assets/mail.svg" alt="" loading="lazy" />
<div>
<p class="service-kicker">Mailbox</p>
<h2>Mail</h2>
<p>
Почтовый шлюз для рабочей переписки, уведомлений и управления
сообщениями.
</p>
</div>
<span class="arrow" aria-hidden="true">→</span>
</a>
<a
class="service-card"
href="https://test-net.bratskih.tech"
target="_blank"
rel="noopener noreferrer"
>
<span class="shine"></span>
<img src="assets/test-net.svg" alt="" loading="lazy" />
<div>
<p class="service-kicker">Lab network</p>
<h2>Test Net</h2>
<p>
Песочница для экспериментов, тестовых окружений и проверки новых
идей.
</p>
</div>
<span class="arrow" aria-hidden="true">→</span>
</a>
<a
class="service-card"
href="https://gitea.bratskih.tech"
target="_blank"
rel="noopener noreferrer"
>
<span class="shine"></span>
<img src="assets/gitea.svg" alt="" loading="lazy" />
<div>
<p class="service-kicker">Git forge</p>
<h2>Gitea</h2>
<p>
Домашняя кузница кода: репозитории, задачи, ревью и история
проектов.
</p>
</div>
<span class="arrow" aria-hidden="true">→</span>
</a>
</section>
</main>
<script src="script.js"></script>
</body>
</html>

117
script.js Normal file
View File

@ -0,0 +1,117 @@
const STORAGE_KEY = "bratskih-hub-preferences";
const defaultPreferences = {
theme: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark",
view: "cards",
accent: "violet",
};
const preferences = {
...defaultPreferences,
...readPreferences(),
};
const services = document.querySelector(".services");
const themeButtons = document.querySelectorAll("[data-theme]");
const viewButtons = document.querySelectorAll("[data-view]");
const accentButtons = document.querySelectorAll("[data-accent]");
const cards = document.querySelectorAll(".service-card");
applyPreferences(preferences);
themeButtons.forEach((button) => {
button.addEventListener("click", () => {
preferences.theme = button.dataset.theme;
applyPreferences(preferences);
savePreferences(preferences);
});
});
viewButtons.forEach((button) => {
button.addEventListener("click", () => {
preferences.view = button.dataset.view;
applyPreferences(preferences);
savePreferences(preferences);
});
});
accentButtons.forEach((button) => {
button.addEventListener("click", () => {
preferences.accent = button.dataset.accent;
applyPreferences(preferences);
savePreferences(preferences);
});
});
cards.forEach((card) => {
let leaveTimer;
card.addEventListener("pointerenter", () => {
window.clearTimeout(leaveTimer);
card.classList.add("is-hovered");
});
card.addEventListener("pointermove", (event) => {
window.clearTimeout(leaveTimer);
card.classList.add("is-hovered");
const rect = card.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const xPercent = clamp((x / rect.width) * 100, 8, 92);
const yPercent = clamp((y / rect.height) * 100, 8, 92);
const rotateY = ((xPercent - 50) / 50) * 2.6;
const rotateX = ((50 - yPercent) / 50) * 2.6;
card.style.setProperty("--card-x", `${xPercent}%`);
card.style.setProperty("--card-y", `${yPercent}%`);
card.style.setProperty("--tilt-x", `${rotateX}deg`);
card.style.setProperty("--tilt-y", `${rotateY}deg`);
});
card.addEventListener("pointerleave", () => {
leaveTimer = window.setTimeout(() => {
card.classList.remove("is-hovered");
card.style.setProperty("--card-x", "50%");
card.style.setProperty("--card-y", "50%");
card.style.setProperty("--tilt-x", "0deg");
card.style.setProperty("--tilt-y", "0deg");
}, 80);
});
});
function applyPreferences(nextPreferences) {
document.documentElement.dataset.theme = nextPreferences.theme;
document.documentElement.dataset.accent = nextPreferences.accent;
services.classList.toggle("cards-view", nextPreferences.view === "cards");
services.classList.toggle("list-view", nextPreferences.view === "list");
setActiveButton(themeButtons, "theme", nextPreferences.theme);
setActiveButton(viewButtons, "view", nextPreferences.view);
setActiveButton(accentButtons, "accent", nextPreferences.accent);
}
function setActiveButton(buttons, key, value) {
buttons.forEach((button) => {
const isActive = button.dataset[key] === value;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});
}
function readPreferences() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
} catch {
return {};
}
}
function savePreferences(nextPreferences) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(nextPreferences));
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}

462
styles.css Normal file
View File

@ -0,0 +1,462 @@
:root {
color-scheme: dark;
--bg: #080a12;
--bg-soft: #111827;
--panel: rgba(15, 23, 42, 0.72);
--panel-strong: rgba(15, 23, 42, 0.9);
--text: #f8fafc;
--muted: #aeb8c8;
--border: rgba(255, 255, 255, 0.12);
--shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
--accent: #8b5cf6;
--accent-2: #22d3ee;
--accent-rgb: 139, 92, 246;
--card-x: 50%;
--card-y: 50%;
}
html[data-theme="light"] {
color-scheme: light;
--bg: #f5f7fb;
--bg-soft: #ffffff;
--panel: rgba(255, 255, 255, 0.76);
--panel-strong: rgba(255, 255, 255, 0.96);
--text: #111827;
--muted: #5f6b7a;
--border: rgba(15, 23, 42, 0.12);
--shadow: 0 24px 70px rgba(56, 65, 91, 0.18);
}
html[data-accent="cyan"] {
--accent: #06b6d4;
--accent-2: #3b82f6;
--accent-rgb: 6, 182, 212;
}
html[data-accent="lime"] {
--accent: #84cc16;
--accent-2: #14b8a6;
--accent-rgb: 132, 204, 22;
}
html[data-accent="rose"] {
--accent: #f43f5e;
--accent-2: #f97316;
--accent-rgb: 244, 63, 94;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
overflow-x: hidden;
background:
radial-gradient(circle at top left, rgba(var(--accent-rgb), 0.24), transparent 36rem),
radial-gradient(circle at bottom right, rgba(34, 211, 238, 0.18), transparent 32rem),
linear-gradient(135deg, var(--bg), var(--bg-soft));
color: var(--text);
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
transition: background 220ms ease, color 220ms ease;
}
button,
a {
font: inherit;
}
a {
color: inherit;
}
.ambient {
position: fixed;
z-index: -1;
width: 28rem;
height: 28rem;
border-radius: 999px;
filter: blur(18px);
opacity: 0.42;
pointer-events: none;
}
.ambient-one {
top: -10rem;
right: -7rem;
background: rgba(var(--accent-rgb), 0.45);
}
.ambient-two {
bottom: -12rem;
left: -10rem;
background: rgba(34, 211, 238, 0.32);
}
.shell {
width: min(1180px, calc(100% - 32px));
margin: 0 auto;
padding: 48px 0;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 430px);
gap: 28px;
align-items: end;
margin-bottom: 28px;
}
.hero-content,
.control-panel,
.service-card {
border: 1px solid var(--border);
background: var(--panel);
box-shadow: var(--shadow);
backdrop-filter: blur(22px);
}
.hero-content {
min-height: 330px;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: clamp(28px, 5vw, 56px);
border-radius: 38px;
position: relative;
overflow: hidden;
}
.hero-content::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(120deg, rgba(var(--accent-rgb), 0.28), transparent 34%),
url("data:image/svg+xml,%3Csvg width='140' height='140' viewBox='0 0 140 140' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.09'%3E%3Cpath d='M0 70h140M70 0v140'/%3E%3Ccircle cx='70' cy='70' r='38'/%3E%3C/g%3E%3C/svg%3E");
mask-image: linear-gradient(to bottom, black, transparent 88%);
pointer-events: none;
}
.hero-content > * {
position: relative;
}
.eyebrow,
.service-kicker {
margin: 0;
color: var(--accent);
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
h1 {
max-width: 760px;
margin: 14px 0 18px;
font-size: clamp(2.8rem, 9vw, 6.8rem);
line-height: 0.88;
letter-spacing: -0.08em;
}
.lead {
max-width: 660px;
margin: 0;
color: var(--muted);
font-size: clamp(1rem, 2vw, 1.22rem);
line-height: 1.7;
}
.control-panel {
display: grid;
gap: 14px;
padding: 18px;
border-radius: 28px;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
min-height: 48px;
padding: 8px;
border: 1px solid var(--border);
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
}
.control-group span {
min-width: 68px;
padding-left: 6px;
color: var(--muted);
font-size: 0.82rem;
font-weight: 700;
}
.segmented,
.accent-swatch {
border: 0;
cursor: pointer;
transition: transform 160ms ease, background 160ms ease, color 160ms ease;
}
.segmented {
flex: 1;
min-height: 34px;
border-radius: 12px;
background: transparent;
color: var(--muted);
font-size: 0.88rem;
font-weight: 800;
}
.segmented:hover,
.accent-swatch:hover {
transform: translateY(-1px);
}
.segmented.is-active {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: white;
}
.accent-group {
justify-content: space-between;
}
.accent-swatch {
width: 36px;
height: 36px;
border: 2px solid transparent;
border-radius: 999px;
box-shadow: inset 0 0 0 4px rgba(255, 255, 255, 0.18);
}
.accent-swatch[data-accent="violet"] {
background: linear-gradient(135deg, #8b5cf6, #22d3ee);
}
.accent-swatch[data-accent="cyan"] {
background: linear-gradient(135deg, #06b6d4, #3b82f6);
}
.accent-swatch[data-accent="lime"] {
background: linear-gradient(135deg, #84cc16, #14b8a6);
}
.accent-swatch[data-accent="rose"] {
background: linear-gradient(135deg, #f43f5e, #f97316);
}
.accent-swatch.is-active {
border-color: var(--text);
transform: scale(1.05);
}
.services {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
}
.service-card {
--tilt-x: 0deg;
--tilt-y: 0deg;
position: relative;
min-height: 300px;
display: flex;
flex-direction: column;
gap: 22px;
padding: 24px;
border-radius: 30px;
text-decoration: none;
overflow: hidden;
transform-origin: center;
transform: perspective(900px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
will-change: transform;
transition:
transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
border-color 220ms ease,
background 220ms ease,
box-shadow 220ms ease;
}
.service-card::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(
circle at var(--card-x) var(--card-y),
rgba(var(--accent-rgb), 0.24),
transparent 34%
);
opacity: 0;
transition: opacity 180ms ease;
pointer-events: none;
}
.service-card.is-hovered,
.service-card:focus-visible {
border-color: rgba(var(--accent-rgb), 0.52);
background: var(--panel-strong);
box-shadow: 0 28px 90px rgba(var(--accent-rgb), 0.18);
outline: none;
}
.service-card.is-hovered::before,
.service-card:focus-visible::before {
opacity: 1;
}
.service-card img {
width: 72px;
height: 72px;
object-fit: contain;
filter: drop-shadow(0 16px 28px rgba(0, 0, 0, 0.24));
}
.service-card h2 {
margin: 7px 0 10px;
font-size: 1.45rem;
letter-spacing: -0.04em;
}
.service-card p:not(.service-kicker) {
margin: 0;
color: var(--muted);
line-height: 1.62;
}
.arrow {
position: absolute;
right: 22px;
bottom: 18px;
width: 42px;
height: 42px;
display: grid;
place-items: center;
border-radius: 999px;
background: rgba(var(--accent-rgb), 0.14);
color: var(--accent);
font-size: 1.35rem;
transition: transform 180ms ease, background 180ms ease, color 180ms ease;
}
.service-card.is-hovered .arrow,
.service-card:focus-visible .arrow {
transform: translateX(4px);
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: white;
}
.shine {
position: absolute;
inset: 0;
background: linear-gradient(
110deg,
transparent 0%,
rgba(255, 255, 255, 0.16) 42%,
transparent 64%
);
transform: translateX(-130%);
transition: transform 540ms ease;
pointer-events: none;
}
.service-card.is-hovered .shine,
.service-card:focus-visible .shine {
transform: translateX(130%);
}
.services.list-view {
grid-template-columns: 1fr;
}
.services.list-view .service-card {
min-height: 132px;
display: grid;
grid-template-columns: 78px minmax(0, 1fr) 52px;
align-items: center;
gap: 22px;
}
.services.list-view .arrow {
position: static;
justify-self: end;
}
@media (max-width: 1040px) {
.hero,
.services {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hero-content {
grid-column: 1 / -1;
}
}
@media (max-width: 680px) {
.shell {
width: min(100% - 22px, 1180px);
padding: 22px 0;
}
.hero,
.services {
grid-template-columns: 1fr;
}
.hero-content {
min-height: 300px;
border-radius: 28px;
}
.control-panel {
border-radius: 24px;
}
.control-group {
flex-wrap: wrap;
}
.control-group span {
width: 100%;
min-width: 0;
}
.accent-group span {
width: auto;
}
.services.list-view .service-card {
grid-template-columns: 58px minmax(0, 1fr);
min-height: 150px;
}
.services.list-view .arrow {
position: absolute;
}
.service-card {
min-height: 250px;
}
.service-card img {
width: 62px;
height: 62px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}