ux/content-corrections #23

Merged
Latte merged 3 commits from ux/content-corrections into dev 2026-03-04 19:49:57 +00:00
12 changed files with 871 additions and 84 deletions
+8 -4
View File
@@ -9,22 +9,26 @@ server {
gzip on; gzip on;
gzip_vary on; gzip_vary on;
gzip_min_length 1024; gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json image/svg+xml;
# Security headers # Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Cache static assets # Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y; expires 1y;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options "nosniff" always;
} }
# Main location # Main location
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ =404;
} }
# Custom error pages # Custom error pages
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+89
View File
@@ -0,0 +1,89 @@
---
const currentPath = Astro.url.pathname;
const links = [
{ href: "/", label: "home" },
{ href: "/about", label: "about" },
{ href: "/projects", label: "projects" },
{ href: "/blog", label: "blog" },
];
function isActive(href: string, current: string): boolean {
if (href === "/") return current === "/";
return current.startsWith(href);
}
---
<nav class="nav" aria-label="Main navigation">
<div class="nav-inner">
{links.map((link, i) => (
<>
{i > 0 && <span class="nav-sep" aria-hidden="true">·</span>}
<a
href={link.href}
class:list={["nav-link", { active: isActive(link.href, currentPath) }]}
aria-current={isActive(link.href, currentPath) ? "page" : undefined}
>
~/{link.label}
</a>
</>
))}
</div>
</nav>
<style>
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
padding: var(--space-sm) var(--space-md);
background: rgba(30, 30, 46, 0.85);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--color-surface);
}
.nav-inner {
max-width: 700px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-xs);
font-size: 0.85rem;
}
.nav-sep {
color: var(--color-surface);
user-select: none;
}
.nav-link {
color: var(--color-text-dim);
text-decoration: none;
padding: 2px 4px;
border-radius: 3px;
transition: color 0.2s ease;
}
.nav-link:hover {
color: var(--color-accent-bright);
}
.nav-link.active {
color: var(--color-accent);
text-decoration: underline;
text-underline-offset: 3px;
}
@media (max-width: 600px) {
.nav {
padding: var(--space-xs) var(--space-sm);
}
.nav-inner {
font-size: 0.8rem;
}
}
</style>
+35
View File
@@ -0,0 +1,35 @@
---
title: "Welcome to the Den"
date: 2026-03-04
description: "First proper post. Why I built this site, what it runs on, and what to expect."
draft: true
---
So I finally got around to setting up a proper blog. Welcome.
## Why This Exists
I wanted a place to write that wasn't owned by a corporation. No Medium, no Substack, no algorithm deciding who sees what. Just markdown files on my own server, served by nginx from a Docker container I control.
That's the whole point of the den — owning your own space on the internet.
## What It Runs On
This site is built with [Astro](https://astro.build), which spits out pure static HTML at build time. No JavaScript runtime, no hydration, no client-side framework. Just HTML and CSS.
It's served by nginx inside a Docker container, deployed from my Gitea instance. The whole pipeline is mine.
## What to Expect
I'll write when I have something to say. Probably a mix of:
- Self-hosting adventures and homelab stuff
- Privacy thoughts and digital autonomy
- Technical things I figured out the hard way
- Whatever else is on my mind
No schedule, no engagement metrics, no SEO optimization. Just writing.
---
*Thanks for reading. Welcome to the den.*
+38
View File
@@ -0,0 +1,38 @@
[
{
"name": "GuardDen",
"description": "Security and moderation tooling for keeping communities safe. Built for the den.",
"tags": ["bot", "self-hosted"],
"status": "stable",
"links": {
"gitea": "https://git.hiddenden.cafe/Hiddenden/GuardDen"
}
},
{
"name": "openrabbit",
"description": "An open-source project with a focus on accessibility and community-driven development.",
"tags": ["tool"],
"status": "stable",
"links": {
"gitea": "https://git.hiddenden.cafe/Hiddenden/openrabbit"
}
},
{
"name": "DevDen",
"description": "A development environment concept for the den ecosystem. Still in the planning phase.",
"tags": ["tool", "experiment"],
"status": "concept",
"links": {
"gitea": "https://git.hiddenden.cafe/Hiddenden/DevDen"
}
},
{
"name": "loyal_companion",
"description": "A companion bot — loyal, helpful, and always around when you need it.",
"tags": ["bot"],
"status": "wip",
"links": {
"gitea": "https://git.hiddenden.cafe/Hiddenden/loyal_companion"
}
}
]
+3
View File
@@ -6,6 +6,8 @@ interface Props {
canonicalURL?: string; canonicalURL?: string;
} }
import Nav from "../components/Nav.astro";
const { const {
title, title,
description = "Hidden Den Cafe - A cozy, self-hosted corner of the internet. Privacy-focused, furry-friendly, and built with love.", description = "Hidden Den Cafe - A cozy, self-hosted corner of the internet. Privacy-focused, furry-friendly, and built with love.",
@@ -80,6 +82,7 @@ const fullOgImage = new URL(ogImage, Astro.site).href;
<!-- <link rel="preconnect" href="https://example.com" crossorigin /> --> <!-- <link rel="preconnect" href="https://example.com" crossorigin /> -->
</head> </head>
<body> <body>
<Nav />
<slot /> <slot />
</body> </body>
</html> </html>
+1
View File
@@ -60,6 +60,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
justify-content: center; justify-content: center;
min-height: 100vh; min-height: 100vh;
padding: var(--space-md); padding: var(--space-md);
padding-top: calc(var(--space-md) + 3rem);
} }
.container { .container {
+236
View File
@@ -0,0 +1,236 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout
title="About — Hidden Den Cafe"
description="About Latte — IT wizard, self-hosting advocate, privacy enthusiast, and the person behind Hidden Den Cafe."
>
<div class="matrix-bg" aria-hidden="true"></div>
<main class="main">
<div class="container">
<header class="header fade-in">
<h1 class="title">About</h1>
<div class="divider">══════════════════════════════</div>
</header>
<section class="section fade-in">
<h2>The Den</h2>
<p class="desc">
Hidden Den Cafe is my little corner of the internet — self-hosted,
self-maintained, and free from corporate nonsense. No trackers, no ads,
no data harvesting. Just a cozy space that I built and control.
</p>
</section>
<section class="section fade-in">
<h2>Who I Am</h2>
<p class="desc">
I'm <span class="highlight">Latte</span> — an IT wizard with a homelab,
a love for privacy, and a deep distrust of companies that treat your data
like their product. I believe in owning your infrastructure, running your
own services, and keeping things under your own roof.
</p>
<p class="desc">
I'm a gay furry developer who builds things because I want them to exist —
not because some product manager told me to. My stack leans toward Python
and self-hosted tooling, but I'm always exploring new things.
</p>
</section>
<section class="section fade-in">
<h2>The Homelab</h2>
<p class="desc">
Most of what runs here lives on my own hardware — Gitea for code, Docker
for deployment, nginx for serving. Where physical infra doesn't make sense,
I rent VPS capacity from OVH and Play.hosting. For work, the Microsoft 365
ecosystem does what it needs to do.
</p>
<p class="desc">
The goal isn't purity — it's control. Keep data minimal, choose providers
you understand, avoid surveillance-adjacent platforms. Self-host what you
can; rent infra where it's practical; use what you need without pretending
you don't.
</p>
</section>
<section class="section fade-in">
<h2>Ethos</h2>
<ul class="values">
<li><span class="value-key">privacy:</span> Your data is yours. Period.</li>
<li><span class="value-key">self-hosting:</span> If you can run it yourself, you should.</li>
<li><span class="value-key">open source:</span> Knowledge should be shared.</li>
<li><span class="value-key">small web:</span> The internet is better when it's personal.</li>
</ul>
</section>
<footer class="footer fade-in">
<p>Made with love by Latte</p>
</footer>
</div>
</main>
</BaseLayout>
<style>
.matrix-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.03;
background:
linear-gradient(var(--color-accent) 1px, transparent 1px),
linear-gradient(90deg, var(--color-accent) 1px, transparent 1px);
background-size: 50px 50px;
animation: grid-move 20s linear infinite;
}
@keyframes grid-move {
0% { transform: translate(0, 0); }
100% { transform: translate(50px, 50px); }
}
.main {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-lg) var(--space-md);
padding-top: calc(var(--space-lg) + 3rem);
}
.container {
max-width: 700px;
width: 100%;
background: rgba(30, 30, 46, 0.8);
backdrop-filter: blur(10px);
border: 1px solid var(--color-surface);
border-radius: 8px;
padding: var(--space-xl);
}
.header {
text-align: center;
margin-bottom: var(--space-lg);
}
.title {
font-size: 2rem;
font-weight: 700;
letter-spacing: 2px;
}
.divider {
color: var(--color-surface);
text-align: center;
font-size: 0.75rem;
margin: var(--space-md) 0;
user-select: none;
}
.section {
margin: var(--space-lg) 0;
}
.section h2 {
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: var(--space-sm);
color: var(--color-accent);
}
.desc {
color: var(--color-text-dim);
line-height: 1.7;
margin-bottom: var(--space-sm);
}
.desc:last-child {
margin-bottom: 0;
}
.highlight {
color: var(--color-accent-bright);
font-weight: 700;
}
.values {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.values li {
color: var(--color-text-dim);
line-height: 1.6;
}
.value-key {
color: var(--color-accent);
font-weight: 600;
}
.footer {
margin-top: var(--space-xl);
text-align: center;
}
.footer p {
color: var(--color-text-dim);
font-size: 0.85rem;
}
.fade-in {
animation: fadeIn 0.6s ease-out forwards;
opacity: 0;
}
.fade-in:nth-child(1) { animation-delay: 0.1s; }
.fade-in:nth-child(2) { animation-delay: 0.2s; }
.fade-in:nth-child(3) { animation-delay: 0.3s; }
.fade-in:nth-child(4) { animation-delay: 0.4s; }
.fade-in:nth-child(5) { animation-delay: 0.5s; }
.fade-in:nth-child(6) { animation-delay: 0.6s; }
.fade-in:nth-child(7) { animation-delay: 0.7s; }
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 600px) {
.container {
padding: var(--space-md);
}
.title {
font-size: 1.5rem;
}
.divider {
font-size: 0.6rem;
}
}
@media (prefers-reduced-motion: reduce) {
.matrix-bg {
animation: none;
}
.fade-in {
animation: none;
opacity: 1;
}
}
</style>
+2 -17
View File
@@ -27,8 +27,6 @@ function formatDate(date: Date) {
<main class="main"> <main class="main">
<div class="container"> <div class="container">
<header class="header fade-in"> <header class="header fade-in">
<p class="back"><a href="/blog">← back to blog</a></p>
<div class="divider">══════════════════════════════</div>
<h1 class="title">{post.data.title}</h1> <h1 class="title">{post.data.title}</h1>
<p class="date">{formatDate(post.data.date)}</p> <p class="date">{formatDate(post.data.date)}</p>
<div class="divider">══════════════════════════════</div> <div class="divider">══════════════════════════════</div>
@@ -39,7 +37,6 @@ function formatDate(date: Date) {
</article> </article>
<footer class="footer fade-in"> <footer class="footer fade-in">
<div class="divider">══════════════════════════════</div>
<p>Made with love by Latte</p> <p>Made with love by Latte</p>
</footer> </footer>
</div> </div>
@@ -73,6 +70,7 @@ function formatDate(date: Date) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: var(--space-lg) var(--space-md); padding: var(--space-lg) var(--space-md);
padding-top: calc(var(--space-lg) + 3rem);
} }
.container { .container {
@@ -89,19 +87,6 @@ function formatDate(date: Date) {
margin-bottom: var(--space-lg); margin-bottom: var(--space-lg);
} }
.back {
margin-bottom: var(--space-sm);
font-size: 0.85rem;
}
.back a {
color: var(--color-text-dim);
}
.back a:hover {
color: var(--color-accent);
}
.divider { .divider {
color: var(--color-surface); color: var(--color-surface);
text-align: center; text-align: center;
@@ -224,7 +209,7 @@ function formatDate(date: Date) {
} }
.footer { .footer {
margin-top: var(--space-lg); margin-top: var(--space-xl);
text-align: center; text-align: center;
} }
+2 -16
View File
@@ -19,7 +19,6 @@ function formatDate(date: Date) {
<main class="main"> <main class="main">
<div class="container"> <div class="container">
<header class="header fade-in"> <header class="header fade-in">
<p class="back"><a href="/">← back to home</a></p>
<h1 class="title">Blog</h1> <h1 class="title">Blog</h1>
<div class="divider">══════════════════════════════</div> <div class="divider">══════════════════════════════</div>
</header> </header>
@@ -45,7 +44,6 @@ function formatDate(date: Date) {
)} )}
<footer class="footer fade-in"> <footer class="footer fade-in">
<div class="divider">══════════════════════════════</div>
<p>Made with love by Latte</p> <p>Made with love by Latte</p>
</footer> </footer>
</div> </div>
@@ -79,6 +77,7 @@ function formatDate(date: Date) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: var(--space-lg) var(--space-md); padding: var(--space-lg) var(--space-md);
padding-top: calc(var(--space-lg) + 3rem);
} }
.container { .container {
@@ -95,19 +94,6 @@ function formatDate(date: Date) {
margin-bottom: var(--space-lg); margin-bottom: var(--space-lg);
} }
.back {
margin-bottom: var(--space-sm);
font-size: 0.85rem;
}
.back a {
color: var(--color-text-dim);
}
.back a:hover {
color: var(--color-accent);
}
.title { .title {
font-size: 2rem; font-size: 2rem;
font-weight: 700; font-weight: 700;
@@ -176,7 +162,7 @@ function formatDate(date: Date) {
} }
.footer { .footer {
margin-top: var(--space-lg); margin-top: var(--space-xl);
text-align: center; text-align: center;
} }
+135 -47
View File
@@ -17,6 +17,11 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
<div class="container"> <div class="container">
<!-- Header --> <!-- Header -->
<header class="header fade-in"> <header class="header fade-in">
<div class="hero-avatar">
<div class="avatar-circle">
<img src="/images/avatar.png" alt="Latte avatar" />
</div>
</div>
<h1 class="title">Hidden Den Cafe</h1> <h1 class="title">Hidden Den Cafe</h1>
<div class="divider">══════════════════════════════</div> <div class="divider">══════════════════════════════</div>
</header> </header>
@@ -28,9 +33,9 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
</p> </p>
<p class="tagline">gay furry wizard</p> <p class="tagline">gay furry wizard</p>
<p class="desc"> <p class="desc">
IT wizard with a homelab. I believe in self-hosting, privacy, IT wizard with a homelab. I believe in self-hosting,
and keeping control of my own data. Companies don't get to sell privacy, and keeping control of my own data. Companies don't
or misuse what's mine. get to sell or misuse what's mine.
</p> </p>
<div class="meta"> <div class="meta">
<span><span class="meta-key">age:</span> {age}</span> <span><span class="meta-key">age:</span> {age}</span>
@@ -38,29 +43,37 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
</div> </div>
</section> </section>
<div class="divider">══════════════════════════════</div>
<!-- Cryptographic Keys --> <!-- Cryptographic Keys -->
<section class="section fade-in"> <section class="section fade-in">
<h2>Cryptographic Keys</h2> <h2>Cryptographic Keys</h2>
<div class="keys"> <div class="keys">
<div class="key-item"> <div class="key-item">
<span class="key-label">pgp fingerprint:</span> <span class="key-label">pgp fingerprint:</span>
<code class="key-value">50DAAAABFD6D76B86507C46E723D1C7AB893AEBE</code> <code class="key-value"
>50DAAAABFD6D76B86507C46E723D1C7AB893AEBE</code
>
</div> </div>
<div class="key-item"> <div class="key-item">
<span class="key-label">pgp key:</span> <span class="key-label">pgp key:</span>
<a href="https://git.hiddenden.cafe/Latte.gpg" target="_blank" rel="noopener noreferrer" class="key-link">latte.gpg</a> <a
href="https://git.hiddenden.cafe/Latte.gpg"
target="_blank"
rel="noopener noreferrer"
class="key-link">latte.gpg</a
>
</div> </div>
<div class="key-item"> <div class="key-item">
<span class="key-label">ssh public key:</span> <span class="key-label">ssh public key:</span>
<a href="https://git.hiddenden.cafe/Latte.keys" target="_blank" rel="noopener noreferrer" class="key-link">latte.keys</a> <a
href="https://git.hiddenden.cafe/Latte.keys"
target="_blank"
rel="noopener noreferrer"
class="key-link">latte.keys</a
>
</div> </div>
</div> </div>
</section> </section>
<div class="divider">══════════════════════════════</div>
<!-- Games --> <!-- Games -->
<section class="section fade-in"> <section class="section fade-in">
<h2>Games</h2> <h2>Games</h2>
@@ -69,61 +82,90 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
</p> </p>
</section> </section>
<div class="divider">══════════════════════════════</div>
<!-- Socials --> <!-- Socials -->
<section class="section fade-in"> <section class="section fade-in">
<h2>Socials</h2> <h2>Socials</h2>
<ul class="links"> <ul class="links">
<li><a href="mailto:latte@hiddenden.cafe">email</a> — latte@hiddenden.cafe</li> <li>
<li><a href="https://signal.me/#eu/SkC6qz0-CzbhpnKwhkkvV6QGFL-NJ_FHfQqafsWqwHfW3QZGuO5D-KlCF2NBE6RA" target="_blank" rel="noopener noreferrer">signal</a></li> <a href="mailto:latte@hiddenden.cafe">email</a> — latte@hiddenden.cafe
<li><a href="https://discord.com/users/714955493923225639" target="_blank" rel="noopener noreferrer">discord</a> — <a href="https://discord.gg/kjwwDtSXYY" target="_blank" rel="noopener noreferrer">join my server</a></li> </li>
<li><a href="https://bsky.app/profile/hiddenden.cafe" target="_blank" rel="noopener noreferrer">bluesky</a></li> <li>
<li><a href="https://steamcommunity.com/profiles/76561198292838319/" target="_blank" rel="noopener noreferrer">steam</a></li> <a
<li><a href="https://git.hiddenden.cafe/Latte" target="_blank" rel="noopener noreferrer">gitea</a></li> href="https://signal.me/#eu/SkC6qz0-CzbhpnKwhkkvV6QGFL-NJ_FHfQqafsWqwHfW3QZGuO5D-KlCF2NBE6RA"
target="_blank"
rel="noopener noreferrer">signal</a
>
</li>
<li>
<a
href="https://discord.com/users/714955493923225639"
target="_blank"
rel="noopener noreferrer">discord</a
> — <a
href="https://discord.gg/kjwwDtSXYY"
target="_blank"
rel="noopener noreferrer">join my server</a
>
</li>
<li>
<a
href="https://bsky.app/profile/hiddenden.cafe"
target="_blank"
rel="noopener noreferrer">bluesky</a
>
</li>
<li>
<a
href="https://steamcommunity.com/profiles/76561198292838319/"
target="_blank"
rel="noopener noreferrer">steam</a
>
</li>
<li>
<a
href="https://git.hiddenden.cafe/Latte"
target="_blank"
rel="noopener noreferrer">gitea</a
>
</li>
</ul> </ul>
</section> </section>
<div class="divider">══════════════════════════════</div>
<!-- Services --> <!-- Services -->
<section class="section fade-in"> <section class="section fade-in">
<h2>Services</h2> <h2>Services</h2>
<ul class="links"> <ul class="links">
<li><a href="https://git.hiddenden.cafe" target="_blank" rel="noopener noreferrer">Gitea</a> — self-hosted git server</li> <li>
<a
href="https://git.hiddenden.cafe"
target="_blank"
rel="noopener noreferrer">Gitea</a
> — self-hosted git server
</li>
</ul> </ul>
</section> </section>
<div class="divider">══════════════════════════════</div>
<!-- Donate --> <!-- Donate -->
<section class="section fade-in"> <section class="section fade-in">
<h2>Support</h2> <h2>Support</h2>
<div class="donate"> <div class="donate">
<div class="crypto"> <div class="crypto">
<span class="crypto-label">[XMR]</span> <span class="crypto-label">[XMR]</span>
<code class="crypto-addr">41uiUeBru8jhtzjQz3M5CKV1uFpern7juStdfveNQS52LQ9aw3mNkdbc8akM81YnxuE2RT9K2Cmyp9cfyi1osrbVBjBbzQ3</code> <code class="crypto-addr"
>41uiUeBru8jhtzjQz3M5CKV1uFpern7juStdfveNQS52LQ9aw3mNkdbc8akM81YnxuE2RT9K2Cmyp9cfyi1osrbVBjBbzQ3</code
>
</div> </div>
<div class="crypto"> <div class="crypto">
<span class="crypto-label">[ETH]</span> <span class="crypto-label">[ETH]</span>
<code class="crypto-addr">0x3Dfc92458267b91BFa6bF8f6c86bAE809Ab76Cb4</code> <code class="crypto-addr"
>0x3Dfc92458267b91BFa6bF8f6c86bAE809Ab76Cb4</code
>
</div> </div>
</div> </div>
</section> </section>
<div class="divider">══════════════════════════════</div>
<!-- Blog -->
<section class="section fade-in">
<h2>Blog</h2>
<ul class="links">
<li><a href="/blog">blog</a> — thoughts and things</li>
</ul>
</section>
<!-- Footer --> <!-- Footer -->
<footer class="footer fade-in"> <footer class="footer fade-in">
<div class="divider">══════════════════════════════</div>
<p>Made with love by Latte</p> <p>Made with love by Latte</p>
</footer> </footer>
</div> </div>
@@ -148,8 +190,12 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
} }
@keyframes grid-move { @keyframes grid-move {
0% { transform: translate(0, 0); } 0% {
100% { transform: translate(50px, 50px); } transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
} }
.main { .main {
@@ -158,6 +204,7 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: var(--space-lg) var(--space-md); padding: var(--space-lg) var(--space-md);
padding-top: calc(var(--space-lg) + 3rem);
} }
.container { .container {
@@ -175,6 +222,28 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
margin-bottom: var(--space-lg); margin-bottom: var(--space-lg);
} }
.hero-avatar {
display: flex;
justify-content: center;
margin-bottom: 12px;
}
.avatar-circle {
width: 128px;
height: 128px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}
.avatar-circle img {
width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.08);
transform-origin: center;
}
.title { .title {
font-size: 2rem; font-size: 2rem;
font-weight: 700; font-weight: 700;
@@ -190,7 +259,7 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
} }
.section { .section {
margin: var(--space-md) 0; margin: var(--space-lg) 0;
} }
.section h2 { .section h2 {
@@ -322,7 +391,7 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
} }
.footer { .footer {
margin-top: var(--space-lg); margin-top: var(--space-xl);
text-align: center; text-align: center;
} }
@@ -337,13 +406,27 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
opacity: 0; opacity: 0;
} }
.fade-in:nth-child(1) { animation-delay: 0.1s; } .fade-in:nth-child(1) {
.fade-in:nth-child(2) { animation-delay: 0.2s; } animation-delay: 0.1s;
.fade-in:nth-child(3) { animation-delay: 0.3s; } }
.fade-in:nth-child(4) { animation-delay: 0.4s; } .fade-in:nth-child(2) {
.fade-in:nth-child(5) { animation-delay: 0.5s; } animation-delay: 0.2s;
.fade-in:nth-child(6) { animation-delay: 0.6s; } }
.fade-in:nth-child(7) { animation-delay: 0.7s; } .fade-in:nth-child(3) {
animation-delay: 0.3s;
}
.fade-in:nth-child(4) {
animation-delay: 0.4s;
}
.fade-in:nth-child(5) {
animation-delay: 0.5s;
}
.fade-in:nth-child(6) {
animation-delay: 0.6s;
}
.fade-in:nth-child(7) {
animation-delay: 0.7s;
}
@keyframes fadeIn { @keyframes fadeIn {
from { from {
@@ -362,6 +445,11 @@ if (now < new Date(now.getFullYear(), 6, 8)) age--;
padding: var(--space-md); padding: var(--space-md);
} }
.avatar-circle {
width: 100px;
height: 100px;
}
.title { .title {
font-size: 1.5rem; font-size: 1.5rem;
} }
+322
View File
@@ -0,0 +1,322 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import projects from "../data/projects.json";
type Project = {
name: string;
description: string;
tags: string[];
status: "stable" | "wip" | "concept";
links: {
site?: string;
gitea?: string;
github?: string;
};
};
const statusColors: Record<string, string> = {
stable: "var(--color-green)",
wip: "var(--color-peach)",
concept: "var(--color-text-dim)",
};
const typedProjects = projects as Project[];
function getPrimaryLink(project: Project): string | undefined {
return project.links.site || project.links.gitea || project.links.github;
}
---
<BaseLayout
title="Projects — Hidden Den Cafe"
description="Things Latte has built — bots, tools, self-hosted services, and experiments from the den."
>
<div class="matrix-bg" aria-hidden="true"></div>
<main class="main">
<div class="container">
<header class="header fade-in">
<h1 class="title">Projects</h1>
<div class="divider">══════════════════════════════</div>
</header>
<section class="section fade-in">
<ul class="project-list">
{typedProjects.map((project) => (
<li class="project-item">
{getPrimaryLink(project) && (
<a
class="project-card-link"
href={getPrimaryLink(project)}
target="_blank"
rel="noopener noreferrer"
aria-label={project.name}
></a>
)}
<div class="project-header">
<h3 class="project-name">{project.name}</h3>
<span
class="project-status"
style={`color: ${statusColors[project.status]}`}
>
[{project.status}]
</span>
</div>
<p class="project-desc">{project.description}</p>
<div class="project-meta">
<div class="project-tags">
{project.tags.map((tag) => (
<span class="tag">{tag}</span>
))}
</div>
<div class="project-links">
{project.links.site && (
<a class="project-link-badge" href={project.links.site} target="_blank" rel="noopener noreferrer">site</a>
)}
{project.links.gitea && (
<a class="project-link-badge" href={project.links.gitea} target="_blank" rel="noopener noreferrer">gitea</a>
)}
{project.links.github && (
<a class="project-link-badge" href={project.links.github} target="_blank" rel="noopener noreferrer">github</a>
)}
</div>
</div>
</li>
))}
</ul>
</section>
<footer class="footer fade-in">
<p>Made with love by Latte</p>
</footer>
</div>
</main>
</BaseLayout>
<style>
.matrix-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.03;
background:
linear-gradient(var(--color-accent) 1px, transparent 1px),
linear-gradient(90deg, var(--color-accent) 1px, transparent 1px);
background-size: 50px 50px;
animation: grid-move 20s linear infinite;
}
@keyframes grid-move {
0% { transform: translate(0, 0); }
100% { transform: translate(50px, 50px); }
}
.main {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-lg) var(--space-md);
padding-top: calc(var(--space-lg) + 3rem);
}
.container {
max-width: 700px;
width: 100%;
background: rgba(30, 30, 46, 0.8);
backdrop-filter: blur(10px);
border: 1px solid var(--color-surface);
border-radius: 8px;
padding: var(--space-xl);
}
.header {
text-align: center;
margin-bottom: var(--space-lg);
}
.title {
font-size: 2rem;
font-weight: 700;
letter-spacing: 2px;
}
.divider {
color: var(--color-surface);
text-align: center;
font-size: 0.75rem;
margin: var(--space-md) 0;
user-select: none;
}
.section {
margin: var(--space-lg) 0;
}
.project-list {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.project-item {
position: relative;
border: 1px solid var(--color-surface);
border-radius: 6px;
padding: var(--space-md);
background: var(--color-bg);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.project-item:hover {
border-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 0 1px var(--color-accent);
transform: translateY(-2px);
}
.project-item:hover .project-name {
text-decoration: underline;
text-underline-offset: 3px;
}
/* Stretched link covers the full card */
.project-card-link {
position: absolute;
inset: 0;
z-index: 0;
border-radius: 6px;
}
.project-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-sm);
margin-bottom: var(--space-xs);
}
.project-name {
font-size: 1rem;
color: var(--color-accent-bright);
font-weight: 700;
}
.project-status {
font-size: 0.75rem;
white-space: nowrap;
}
.project-desc {
color: var(--color-text-dim);
font-size: 0.85rem;
line-height: 1.6;
margin-bottom: var(--space-sm);
}
.project-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
flex-wrap: wrap;
}
.project-tags {
display: flex;
gap: var(--space-xs);
flex-wrap: wrap;
}
.tag {
font-size: 0.7rem;
color: var(--color-accent);
border: 1px solid var(--color-surface);
padding: 1px 6px;
border-radius: 3px;
}
.project-links {
display: flex;
gap: var(--space-sm);
position: relative;
z-index: 1;
}
.project-link-badge {
color: var(--color-blue);
font-size: 0.8rem;
}
.project-link-badge:hover {
color: var(--color-accent-bright);
}
.footer {
margin-top: var(--space-xl);
text-align: center;
}
.footer p {
color: var(--color-text-dim);
font-size: 0.85rem;
}
.fade-in {
animation: fadeIn 0.6s ease-out forwards;
opacity: 0;
}
.fade-in:nth-child(1) { animation-delay: 0.1s; }
.fade-in:nth-child(2) { animation-delay: 0.2s; }
.fade-in:nth-child(3) { animation-delay: 0.3s; }
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 600px) {
.container {
padding: var(--space-md);
}
.title {
font-size: 1.5rem;
}
.divider {
font-size: 0.6rem;
}
.project-header {
flex-direction: column;
gap: 2px;
}
.project-meta {
flex-direction: column;
align-items: flex-start;
}
}
@media (prefers-reduced-motion: reduce) {
.matrix-bg {
animation: none;
}
.fade-in {
animation: none;
opacity: 1;
}
}
</style>