Homepage/index.html

443 lines
14 KiB
HTML
Raw Permalink Normal View History

2026-07-16 15:31:38 +02:00
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Simple homepage for self-hosted programs.">
<title>teto.dance</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=Sniglet:wght@400;800&family=DynaPuff:wght@400;700&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="images/favicon.svg">
<style>
:root {
--text-sm: clamp(0.875rem, 0.82rem + 0.25vw, 1rem);
--text-base: clamp(1rem, 0.96rem + 0.25vw, 1.125rem);
--text-lg: clamp(1.125rem, 1rem + 0.75vw, 1.5rem);
--text-xl: clamp(1.65rem, 1.1rem + 2vw, 2.8rem);
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--radius-full: 9999px;
--radius-lg: 1rem;
--radius-xl: 1.5rem;
--font-body: 'Sniglet', system-ui, sans-serif;
--font-heading: 'DynaPuff', system-ui, sans-serif;
--bg-image: url("images/wallpaper.png");
--toggle-size: 2.8rem;
}
:root,
[data-theme="light"] {
--color-bg: #eef3fb;
--color-surface: rgba(255, 255, 255, 0.66);
--color-border: rgba(28, 37, 54, 0.12);
--color-text: #162033;
--color-text-muted: rgba(22, 32, 51, 0.72);
--color-primary: #316dff;
--color-heading-subtle: #52627e;
--color-overlay: linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(233, 240, 250, 0.56));
--image-filter: blur(7px) brightness(0.84);
--shadow-lg: 0 20px 60px rgba(30, 51, 84, 0.18);
--toggle-bg: rgba(255, 255, 255, 0.74);
--toggle-border: rgba(28, 37, 54, 0.12);
}
[data-theme="dark"] {
--color-bg: #0d1016;
--color-surface: rgba(17, 19, 26, 0.46);
--color-border: rgba(255, 255, 255, 0.14);
--color-text: #f5f7fb;
--color-text-muted: rgba(245, 247, 251, 0.72);
--color-primary: #9dd6ff;
--color-heading-subtle: #bababa;
--color-overlay: linear-gradient(180deg, rgba(8, 10, 16, 0.3), rgba(8, 10, 16, 0.6));
--image-filter: blur(7px) brightness(0.42);
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.35);
--toggle-bg: rgba(17, 19, 26, 0.68);
--toggle-border: rgba(255, 255, 255, 0.12);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--color-bg: #0d1016;
--color-surface: rgba(17, 19, 26, 0.46);
--color-border: rgba(255, 255, 255, 0.14);
--color-text: #f5f7fb;
--color-text-muted: rgba(245, 247, 251, 0.72);
--color-primary: #9dd6ff;
--color-heading-subtle: #bababa;
--color-overlay: linear-gradient(180deg, rgba(8, 10, 16, 0.3), rgba(8, 10, 16, 0.6));
--image-filter: blur(7px) brightness(0.42);
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.35);
--toggle-bg: rgba(17, 19, 26, 0.68);
--toggle-border: rgba(255, 255, 255, 0.12);
}
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
html { background: var(--color-bg); }
body {
font-family: var(--font-body);
font-size: var(--text-base);
color: var(--color-text);
background: var(--color-bg);
overflow: hidden;
position: relative;
transition: background 220ms ease, color 220ms ease;
}
body::before {
content: "";
position: fixed;
inset: -2rem;
background-image: var(--bg-image);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: var(--image-filter);
transform: scale(1.06);
pointer-events: none;
z-index: 0;
transition: filter 220ms ease;
}
body::after {
content: "";
position: fixed;
inset: 0;
background: var(--color-overlay);
pointer-events: none;
z-index: -1;
transition: background 220ms ease;
}
.page {
position: relative;
min-height: 100dvh;
display: grid;
place-items: center;
padding: var(--space-8);
isolation: isolate;
}
.theme-toggle {
position: fixed;
top: var(--space-6);
right: var(--space-6);
width: var(--toggle-size);
height: var(--toggle-size);
display: grid;
place-items: center;
border-radius: var(--radius-full);
border: 1px solid var(--toggle-border);
background: var(--toggle-bg);
color: var(--color-text);
backdrop-filter: blur(14px);
box-shadow: var(--shadow-lg);
cursor: pointer;
z-index: 5;
transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1), background 220ms ease, border-color 220ms ease;
}
.theme-toggle:hover,
.theme-toggle:focus-visible {
transform: translateY(-2px);
outline: none;
}
.theme-toggle svg {
width: 1.2rem;
height: 1.2rem;
stroke: currentColor;
fill: none;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.panel {
width: min(100%, 34rem);
padding: clamp(var(--space-8), 6vw, var(--space-12));
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
background: var(--color-surface);
box-shadow: var(--shadow-lg);
backdrop-filter: blur(18px);
text-align: center;
transition: background 220ms ease, border-color 220ms ease, box-shadow 220ms ease;
}
.avatar {
width: clamp(7rem, 16vw, 9.5rem);
aspect-ratio: 1;
margin: 0 auto var(--space-6);
border-radius: var(--radius-full);
padding: 0.35rem;
background: #ffffff;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25);
overflow: hidden;
cursor: pointer;
user-select: none;
touch-action: manipulation;
will-change: transform;
transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.avatar-inner {
width: 100%;
height: 100%;
border-radius: var(--radius-full);
display: block;
object-fit: cover;
border: 1px solid rgba(255,255,255,0.16);
pointer-events: none;
will-change: transform;
}
h1 {
margin: 0 0 var(--space-2);
font: var(--font-heading);
font-size: var(--text-xl);
line-height: 1.1;
}
h2 {
margin: 0;
font-size: var(--text-lg);
line-height: 1.2;
font-weight: 300;
}
h4 {
margin: 0 0 var(--space-6);
color: var(--color-heading-subtle);
font-weight: 500;
transition: color 220ms ease;
}
.links {
list-style: none;
padding: 0;
margin: var(--space-6) 0 0;
display: grid;
gap: var(--space-4);
}
.links li {
opacity: 0;
transform: translateY(16px) scale(0.98);
animation: slideIn 700ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.links li:nth-child(2) { animation-delay: 120ms; }
.links li:nth-child(3) { animation-delay: 220ms; }
.link-card {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-4);
padding: var(--space-4) var(--space-6);
border-radius: var(--radius-lg);
color: var(--color-text);
text-decoration: none;
text-align: center;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(255,255,255,0.06);
backdrop-filter: blur(10px);
transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1), background 180ms ease, border-color 180ms ease, color 220ms ease;
min-height: 3.5rem;
}
[data-theme="light"] .link-card,
:root:not([data-theme]) .link-card {
border-color: rgba(28, 37, 54, 0.08);
background: rgba(255, 255, 255, 0.46);
}
.link-card:hover,
.link-card:focus-visible {
transform: translateY(-2px);
background: rgba(255,255,255,0.1);
border-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
outline: none;
}
.link-name {
font-weight: 300;
letter-spacing: 0.01em;
width: 100%;
text-align: center;
}
.credit {
margin: var(--space-8) 0 0;
color: var(--color-heading-subtle);
font-size: var(--text-sm);
transition: color 220ms ease;
}
@keyframes slideIn {
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@media (max-width: 480px) {
body { overflow: auto; }
.page { padding: var(--space-4); }
.panel { padding: var(--space-8) var(--space-6); }
.link-card { padding: var(--space-4); }
.theme-toggle {
top: var(--space-4);
right: var(--space-4);
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.links li { opacity: 1; transform: none; }
.avatar, .avatar-inner { transform: none !important; }
}
</style>
</head>
<body>
<button class="theme-toggle" type="button" aria-label="Toggle color mode" data-theme-toggle>
<span class="theme-toggle-icon" aria-hidden="true"></span>
</button>
<main class="page">
<section class="panel" aria-label="Self-hosted homepage">
<h1>Teto.Dance</h1>
<h4>Little Apps For Little Things</h4>
<div class="avatar" aria-label="Spinning Teto profile image" tabindex="0">
<img class="avatar-inner" src="images/pfp.jpg" alt="Dinosaur">
</div>
<h2>Applications</h2>
<ul class="links" role="list">
<li>
<a class="link-card" href="https://music.teto.dance" target="_blank" rel="noopener noreferrer">
<span class="link-name">Navidrome | 📻</span>
</a>
</li>
<li>
<a class="link-card" href="https://image.teto.dance" target="_blank" rel="noopener noreferrer">
<span class="link-name">Immich | 🖼️</span>
</a>
</li>
<li>
<a class="link-card" href="https://code.teto.dance" target="_blank" rel="noopener noreferrer">
<span class="link-name">Forgejo | 🧰</span>
</a>
</li>
<li>
<a class="link-card" href="https://vault.teto.dance" target="_blank" rel="noopener noreferrer">
<span class="link-name">Vaultwarden | 🔒</span>
</a>
</li>
</ul>
<p class="credit">♡ Made By WeeScottishMan ♡</p>
</section>
</main>
<script>
const avatar = document.querySelector('.avatar');
const avatarInner = document.querySelector('.avatar-inner');
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
const themeToggle = document.querySelector('[data-theme-toggle]');
const themeIcon = document.querySelector('.theme-toggle-icon');
const root = document.documentElement;
const schemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
let manualTheme = null;
const sunIcon = '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2.5M12 19.5V22M4.93 4.93l1.77 1.77M17.3 17.3l1.77 1.77M2 12h2.5M19.5 12H22M4.93 19.07l1.77-1.77M17.3 6.7l1.77-1.77"></path></svg>';
const moonIcon = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12.4A8.5 8.5 0 1 1 11.6 3a7 7 0 0 0 9.4 9.4Z"></path></svg>';
const applyTheme = (theme, manual = false) => {
if (manual) {
manualTheme = theme;
root.setAttribute('data-theme', theme);
} else if (manualTheme) {
root.setAttribute('data-theme', manualTheme);
} else {
root.setAttribute('data-theme', theme);
}
const activeTheme = root.getAttribute('data-theme');
themeIcon.innerHTML = activeTheme === 'dark' ? sunIcon : moonIcon;
themeToggle.setAttribute('aria-label', activeTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode');
};
applyTheme(schemeQuery.matches ? 'dark' : 'light');
schemeQuery.addEventListener('change', (event) => {
if (!manualTheme) {
applyTheme(event.matches ? 'dark' : 'light');
}
});
themeToggle.addEventListener('click', () => {
const current = root.getAttribute('data-theme') || (schemeQuery.matches ? 'dark' : 'light');
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next, true);
});
if (avatar && avatarInner && !reduceMotion.matches) {
let angle = 0;
let velocity = 0.18;
let rafId = null;
let lastTime = 0;
const maxVelocity = 1000;
const minVelocity = 0.12;
const boostAmount = 0.16;
const decay = 0.996;
const animate = (time) => {
if (!lastTime) lastTime = time;
const delta = Math.min(32, time - lastTime);
lastTime = time;
velocity *= Math.pow(decay, delta / 16.67);
velocity = Math.max(minVelocity, Math.min(maxVelocity, velocity));
angle += velocity * delta;
avatar.style.transform = `scale(${1 + Math.min((velocity - minVelocity) * 0.08, 0.08)})`;
avatarInner.style.transform = `rotate(${angle}deg)`;
rafId = requestAnimationFrame(animate);
};
const addBoost = () => {
velocity = Math.min(maxVelocity, velocity + boostAmount);
if (!rafId) rafId = requestAnimationFrame(animate);
};
avatar.addEventListener('mouseenter', addBoost);
avatar.addEventListener('mouseleave', addBoost);
avatar.addEventListener('focus', addBoost);
avatar.addEventListener('blur', addBoost);
avatar.addEventListener('touchstart', addBoost, { passive: true });
rafId = requestAnimationFrame(animate);
}
</script>
</body>
</html>
EOF && grep -n "DynaPuff\|data-theme-toggle\|prefers-color-scheme" output/simple-homepage/selfhosted-homepage.html | head -n 20