commit d86f7f1952b9ae6c8c86adb38759f3b3cea68fb2 Author: Илья Братских Date: Wed Jun 24 09:57:27 2026 +0300 first commit - static hello page diff --git a/assets/gitea.svg b/assets/gitea.svg new file mode 100644 index 0000000..cd4f7d2 --- /dev/null +++ b/assets/gitea.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/mail.svg b/assets/mail.svg new file mode 100644 index 0000000..f7b64f9 --- /dev/null +++ b/assets/mail.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/nextcloud.svg b/assets/nextcloud.svg new file mode 100644 index 0000000..1ca6c79 --- /dev/null +++ b/assets/nextcloud.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/test-net.svg b/assets/test-net.svg new file mode 100644 index 0000000..ec36b36 --- /dev/null +++ b/assets/test-net.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..7bc9fe0 --- /dev/null +++ b/index.html @@ -0,0 +1,166 @@ + + + + + + + Bratskih Tech Hub + + + + + + +
+
+ +
+
+
+

Personal ecosystem

+

Bratskih Tech Hub

+

+ Быстрый старт для твоих сервисов: облако, почта, тестовая сеть и + репозитории в одном аккуратном месте. +

+
+ +
+
+ Тема + + +
+ +
+ Вид + + +
+ +
+ Акцент + + + + +
+
+
+ +
+ + + +
+

Cloud storage

+

Nextcloud

+

+ Личное облако для файлов, заметок, календарей и синхронизации + между устройствами. +

+
+ +
+ + + + +
+

Mailbox

+

Mail

+

+ Почтовый шлюз для рабочей переписки, уведомлений и управления + сообщениями. +

+
+ +
+ + + + +
+

Lab network

+

Test Net

+

+ Песочница для экспериментов, тестовых окружений и проверки новых + идей. +

+
+ +
+ + + + +
+

Git forge

+

Gitea

+

+ Домашняя кузница кода: репозитории, задачи, ревью и история + проектов. +

+
+ +
+
+
+ + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..52af398 --- /dev/null +++ b/script.js @@ -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); +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..2515288 --- /dev/null +++ b/styles.css @@ -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; + } +}