Implement and build from plan.md

This commit is contained in:
nicholai 2025-12-06 05:02:47 -07:00
parent f7c1cdd6ba
commit 70868fddd4
20 changed files with 1036 additions and 508 deletions

7
.cursor/worktrees.json Normal file
View File

@ -0,0 +1,7 @@
{
"setup-worktree": [
"# fnm use",
"# npm install",
"# cp $ROOT_WORKTREE_PATH/.env .env"
]
}

124
IMPLEMENTATION_COMPLETE.md Normal file
View File

@ -0,0 +1,124 @@
# Portfolio Site Integration - Implementation Complete
## Summary
Successfully implemented the V7 Industrial Dark Mode design system for Nicholai Vogel's portfolio site, integrating all components from the reference `index.html` into the Astro blog template.
## Completed Tasks
### Phase 1: Foundation Setup ✅
- **Updated `src/styles/global.css`**: Configured Tailwind v4 with V7 design system tokens
- Brand colors (dark, panel, accent, cyan)
- Font families (Inter + Space Mono)
- Custom utilities (grid-overlay, text-massive, text-stroke, skill-tag, btn-primary, btn-ghost)
- Scrollbar styling, reveal animations
- Prose/blog content styles
- **Updated `src/consts.ts`**: Added site metadata
- Contact information (email, phone)
- Location and timezone
- Availability status
- Social links (website, Instagram, LinkedIn, Biohazard)
- Navigation links
### Phase 2: Core Components ✅
- **Modified `src/components/BaseHead.astro`**:
- Added Google Fonts preconnect and stylesheet (Inter, Space Mono)
- Added Lucide icons CDN script
- Removed old Atkinson font preloads
- Kept canonical links and meta tags per user preferences
- **Created Layout Components**:
- `src/components/GridOverlay.astro`: Fixed background grid + 12-column guide
- `src/components/Navigation.astro`: Fixed nav with logo, status badge, links, contact CTA
- `src/components/Footer.astro`: Contact CTA, social links, copyright, decorative text
- `src/components/CustomCursor.tsx`: React island for custom cursor with dot + outline
- **Created Section Components** (in `src/components/sections/`):
- `Hero.astro`: Hero section with massive typography, description, live clock
- `Experience.astro`: Timeline with Biohazard VFX, Stinkfilms, Freelance
- `FeaturedProject.astro`: G-Star Raw Olympics full-width showcase
- `Skills.astro`: Technical arsenal 4-column grid
### Phase 3: Layouts ✅
- **Created `src/layouts/BaseLayout.astro`**:
- Wraps BaseHead, GridOverlay, Navigation, main slot, Footer
- Includes CustomCursor React island with `client:load`
- Body classes for selection styling
- Inline script for Lucide icons initialization and reveal animations
- **Kept `src/layouts/BlogPost.astro`**: Already styled for V7 dark theme
### Phase 4: Pages ✅
- **`src/pages/index.astro`**: Already composed with all section components:
- Hero → Divider → Experience → Divider → FeaturedProject → Skills
- **`src/pages/blog/index.astro`**: Already styled with dark theme, card hover effects
- **`src/pages/blog/[...slug].astro`**: Uses BlogPost layout (no changes needed)
- **Removed `src/pages/about.astro`**: Already deleted
### Phase 5: Build & Verify ✅
- Build command executed successfully (exit code 0)
- All files created and properly structured
- No syntax errors detected in components
## Key Files Modified
1. `src/styles/global.css` - Complete rewrite with Tailwind v4 and V7 design system
2. `src/consts.ts` - Updated with Nicholai's site metadata
3. `src/components/BaseHead.astro` - Updated fonts and icons
4. `src/components/Footer.astro` - Complete rewrite
5. `src/layouts/BaseLayout.astro` - New shared layout
## Key Files Created
1. `src/components/GridOverlay.astro`
2. `src/components/Navigation.astro`
3. `src/components/CustomCursor.tsx`
4. `src/components/sections/Hero.astro`
5. `src/components/sections/Experience.astro`
6. `src/components/sections/FeaturedProject.astro`
7. `src/components/sections/Skills.astro`
8. `src/layouts/BaseLayout.astro`
## Design System Features Implemented
- **Colors**: Dark mode with brand colors (dark #0B0D11, accent #FFB84C, cyan #22D3EE)
- **Typography**: Inter (primary) + Space Mono (monospace), massive hero text
- **Grid System**: Visible 12-column grid overlay as design element
- **Custom Cursor**: Animated dot + outline (React component)
- **Animations**: Reveal text on scroll, smooth transitions, hover effects
- **Components**: Buttons (primary, ghost), skill tags, prose styling
- **Navigation**: Fixed header with status badge and contact CTA
- **Footer**: Large typography with social links and decorative elements
## Next Steps for User
1. **Test the site**: Run `pnpm dev` to view the site locally
2. **Verify responsive design**: Check mobile, tablet, and desktop layouts
3. **Update content**: Customize experience timeline, project details, skills as needed
4. **Add images**: Replace placeholder images with actual project images
5. **Deploy**: Run `pnpm deploy` to publish to Cloudflare Pages
## Technical Notes
- Using Tailwind CSS v4 with @theme directive
- React integration for interactive custom cursor
- Lucide icons via CDN
- Google Fonts (Inter + Space Mono) loaded via CDN with preconnect
- Intersection Observer for scroll-triggered reveal animations
- Responsive breakpoints: sm (640px), md (768px), lg (1024px)
- Custom scrollbar styling for webkit browsers
- Selection styling with brand accent color
## Browser Support
Modern browsers supporting:
- CSS custom properties
- Backdrop blur
- Mix blend modes
- Grid layout
- Custom scrollbars (webkit)

125
dev/plan.md Normal file
View File

@ -0,0 +1,125 @@
# Portfolio Site Integration Plan
## Phase 1: Foundation Setup
### Tailwind Configuration
Update `src/styles/global.css` to configure Tailwind v4 with the V7 design system tokens from `design.json`:
- Brand colors (dark: #0B0D11, panel: #151921, accent: #FFB84C, cyan: #22D3EE)
- Font families (Inter + Space Mono from Google Fonts)
- Custom utilities (grid-overlay, text-massive, text-stroke, skill-tag, btn-primary, btn-ghost)
- Scrollbar styling, reveal animations, cursor styles
### Constants Update
Update `src/consts.ts` with Nicholai's site metadata:
- SITE_TITLE, SITE_DESCRIPTION, contact info, social links
## Phase 2: Core Components
### BaseHead.astro
Modify to include:
- Google Fonts preconnect/stylesheet (Inter, Space Mono)
- Lucide icons CDN script
- Remove old Atkinson font preloads
### Layout Components (new files in `src/components/`)
Extract from index.html:
| Component | Purpose |
|-----------|---------|
| `GridOverlay.astro` | Fixed background grid + 12-column guide |
| `Navigation.astro` | Fixed nav with logo, status badge, links, contact CTA |
| `Footer.astro` | Replace existing - contact CTA, social links, copyright |
| `CustomCursor.tsx` | React island for cursor dot + outline with mousemove tracking |
### Section Components (new files in `src/components/sections/`)
| Component | Purpose |
|-----------|---------|
| `Hero.astro` | Hero with title, subtitle, description, clock |
| `Experience.astro` | Timeline with Biohazard VFX, Stinkfilms, Freelance |
| `FeaturedProject.astro` | G-Star Raw Olympics full-width showcase |
| `Skills.astro` | Technical arsenal 4-column grid |
## Phase 3: Layouts
### BaseLayout.astro (new)
Create shared layout wrapping:
- BaseHead, GridOverlay, Navigation, main slot, Footer, CustomCursor island
- Body classes: `antialiased selection:bg-brand-accent selection:text-brand-dark`
### BlogPost.astro
Restyle to match V7 dark theme while keeping content structure.
## Phase 4: Pages
### index.astro
Compose from section components:
```astro
<BaseLayout>
<Hero />
<Divider />
<Experience />
<Divider />
<FeaturedProject />
<Skills />
</BaseLayout>
```
### blog/index.astro
Restyle post grid with dark theme, card hover effects, accent colors.
### blog/[...slug].astro
Uses updated BlogPost layout (no changes needed to routing logic).
### Delete about.astro
Remove the about page entirely.
## Phase 5: Assets
- Add favicon matching brand identity to `public/`
- Keep existing blog placeholder images
- Fonts served via Google Fonts CDN (no local files needed)
## Key Files to Modify
- `src/styles/global.css` - Tailwind config + custom CSS
- `src/consts.ts` - Site metadata
- `src/components/BaseHead.astro` - Font/icon loading
- `src/components/Footer.astro` - Complete rewrite
- `src/layouts/BlogPost.astro` - Restyle for dark theme
## Key Files to Create
- `src/components/GridOverlay.astro`
- `src/components/Navigation.astro`
- `src/components/CustomCursor.tsx`
- `src/components/sections/Hero.astro`
- `src/components/sections/Experience.astro`
- `src/components/sections/FeaturedProject.astro`
- `src/components/sections/Skills.astro`
- `src/layouts/BaseLayout.astro`

View File

@ -1,9 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
<rect width="100" height="100" fill="#0B0D11"/>
<rect x="5" y="5" width="90" height="90" stroke="#FFB84C" stroke-width="5"/>
<text x="50" y="65" font-family="monospace" font-weight="bold" font-size="40" text-anchor="middle" fill="#FFB84C">NV</text>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 337 B

View File

@ -30,9 +30,18 @@ const { title, description, image = FallbackImage } = Astro.props;
/>
<meta name="generator" content={Astro.generator} />
<!-- Font preloads -->
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="preload"
as="style"
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Mono:wght@400;700&display=swap"
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Mono:wght@400;700&display=swap"
rel="stylesheet"
/>
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
@ -55,3 +64,6 @@ const { title, description, image = FallbackImage } = Astro.props;
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image.src, Astro.url)} />
<!-- Lucide Icons -->
<script is:inline src="https://unpkg.com/lucide@latest"></script>

View File

@ -0,0 +1,51 @@
import { useEffect, useState } from 'react';
export default function CustomCursor() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const updatePosition = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
if (!isVisible) setIsVisible(true);
};
const handleMouseLeave = () => {
setIsVisible(false);
};
window.addEventListener('mousemove', updatePosition);
document.addEventListener('mouseleave', handleMouseLeave);
return () => {
window.removeEventListener('mousemove', updatePosition);
document.removeEventListener('mouseleave', handleMouseLeave);
};
}, [isVisible]);
if (!isVisible) return null;
return (
<>
{/* Cursor Dot */}
<div
className="hidden md:block fixed w-2 h-2 bg-brand-accent rounded-full pointer-events-none z-[9999]"
style={{
left: `${position.x}px`,
top: `${position.y}px`,
transform: 'translate(-50%, -50%)',
}}
/>
{/* Cursor Outline */}
<div
className="hidden md:block fixed w-10 h-10 border border-brand-accent/50 rounded-full pointer-events-none z-[9999] transition-all duration-300"
style={{
left: `${position.x}px`,
top: `${position.y}px`,
transform: 'translate(-50%, -50%)',
}}
/>
</>
);
}

View File

@ -1,62 +1,53 @@
---
const today = new Date();
import { CONTACT_EMAIL, CONTACT_PHONE, SOCIAL_LINKS } from '../consts';
---
<footer>
&copy; {today.getFullYear()} Your name here. All rights reserved.
<div class="social-links">
<a href="https://m.webtoo.ls/@astro" target="_blank">
<span class="sr-only">Follow Astro on Mastodon</span>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
width="32"
height="32"
astro-icon="social/mastodon"
><path
fill="currentColor"
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
></path></svg
>
</a>
<a href="https://twitter.com/astrodotbuild" target="_blank">
<span class="sr-only">Follow Astro on Twitter</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/twitter"
><path
fill="currentColor"
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
></path></svg
>
</a>
<a href="https://github.com/withastro/astro" target="_blank">
<span class="sr-only">Go to Astro's GitHub repo</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github"
><path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
></path></svg
>
</a>
<!-- Footer / Contact -->
<footer class="container mx-auto px-6 lg:px-12 py-32 relative overflow-hidden border-t border-slate-800 mt-24">
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-end">
<div>
<h2 class="text-6xl md:text-8xl font-bold uppercase leading-none tracking-tighter mb-8 text-white group cursor-pointer">
Let's<br>
<span class="text-stroke group-hover:text-brand-accent transition-colors duration-300">Build</span><br>
Reality.
</h2>
<div class="flex flex-wrap gap-6 mt-12">
<a href={`mailto:${CONTACT_EMAIL}`} class="btn-primary">{CONTACT_EMAIL}</a>
<a href={`tel:${CONTACT_PHONE.replace(/\s/g, '')}`} class="btn-ghost">{CONTACT_PHONE}</a>
</div>
</div>
<div class="md:text-right">
<div class="mb-12">
<p class="text-xs font-bold uppercase text-slate-500 mb-4 tracking-widest">Social Uplink</p>
<ul class="space-y-2">
<li>
<a href={SOCIAL_LINKS.website} class="text-white hover:text-brand-accent text-lg font-mono transition-colors">
{SOCIAL_LINKS.website.replace('https://', '')}
</a>
</li>
<li>
<a href={`https://instagram.com/${SOCIAL_LINKS.instagram.replace('@', '')}`} class="text-white hover:text-brand-accent text-lg font-mono transition-colors">
{SOCIAL_LINKS.instagram}
</a>
</li>
<li>
<a href={SOCIAL_LINKS.linkedin} class="text-white hover:text-brand-accent text-lg font-mono transition-colors">
LinkedIn
</a>
</li>
</ul>
</div>
<div class="flex justify-end items-end gap-2 text-[10px] text-slate-600 font-mono uppercase">
<span>© 2025 Nicholai Vogel</span>
<span>/</span>
<span>V7 SYSTEM</span>
</div>
</div>
</div>
<!-- Decorative huge text bg -->
<div class="absolute -bottom-10 left-0 w-full text-center pointer-events-none opacity-5">
<span class="text-[15rem] font-bold text-white uppercase leading-none whitespace-nowrap">VOGEL</span>
</div>
</footer>
<style>
footer {
padding: 2em 1em 6em 1em;
background: linear-gradient(var(--gray-gradient)) no-repeat;
color: rgb(var(--gray));
text-align: center;
}
.social-links {
display: flex;
justify-content: center;
gap: 1em;
margin-top: 1em;
}
.social-links a {
text-decoration: none;
color: rgb(var(--gray));
}
.social-links a:hover {
color: rgb(var(--gray-dark));
}
</style>

View File

@ -0,0 +1,21 @@
---
// Grid Overlay Component - Visible design element
---
<!-- Fixed Grid Overlay -->
<div class="fixed inset-0 grid-overlay h-screen w-screen"></div>
<!-- 12 Column Guide (Visual Only - Low Opacity) -->
<div class="fixed inset-0 container mx-auto px-6 lg:px-12 grid grid-cols-4 md:grid-cols-12 gap-4 pointer-events-none z-0 opacity-10 border-x border-white/5">
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
</div>

View File

@ -0,0 +1,34 @@
---
import { CONTACT_EMAIL, AVAILABILITY_STATUS, NAV_LINKS } from '../consts';
---
<!-- Navigation -->
<nav class="fixed top-0 left-0 w-full z-50 px-6 lg:px-12 py-8 flex justify-between items-center backdrop-blur-sm border-b border-white/5">
<div class="flex items-center gap-4 group">
<div class="w-10 h-10 border border-brand-accent/50 flex items-center justify-center bg-brand-accent/5 group-hover:bg-brand-accent transition-colors duration-300">
<span class="font-bold text-brand-accent group-hover:text-brand-dark">NV</span>
</div>
<div class="flex flex-col">
<span class="text-xs font-mono uppercase tracking-widest text-slate-400">Status</span>
<span class="text-xs font-bold text-brand-accent animate-pulse">{AVAILABILITY_STATUS}</span>
</div>
</div>
<div class="hidden md:flex gap-12">
{NAV_LINKS.map(link => (
<a
href={link.href}
class="text-xs font-bold uppercase tracking-widest text-slate-500 hover:text-white transition-colors"
>
{link.label}
</a>
))}
</div>
<a
href={`mailto:${CONTACT_EMAIL}`}
class="border border-slate-600 px-6 py-3 text-xs font-bold uppercase tracking-widest hover:border-brand-accent hover:bg-brand-accent/10 transition-all"
>
Contact
</a>
</nav>

View File

@ -0,0 +1,99 @@
---
---
<section id="experience" class="container mx-auto px-6 lg:px-12 py-24">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<div class="lg:col-span-4">
<h2 class="text-4xl font-bold uppercase tracking-tight mb-2 text-stroke">Experience</h2>
<h2 class="text-4xl font-bold uppercase tracking-tight mb-8">History</h2>
<p class="text-slate-400 mb-8 max-w-sm">
From founding my own VFX house to supervising global campaigns. I bridge the gap between
creative vision and technical execution.
</p>
<a href="https://biohazardvfx.com" target="_blank"
class="inline-flex items-center gap-2 text-xs font-mono uppercase text-brand-accent hover:underline">
Visit Biohazard VFX <i data-lucide="arrow-up-right" class="w-4 h-4"></i>
</a>
</div>
<div class="lg:col-span-8 relative">
<!-- Vertical line -->
<div
class="absolute left-0 top-0 bottom-0 w-[1px] bg-gradient-to-b from-brand-accent via-slate-800 to-transparent">
</div>
<!-- Item 1 -->
<div class="pl-12 mb-20 relative reveal-text">
<div
class="absolute left-[-5px] top-2 w-2.5 h-2.5 bg-brand-dark border border-brand-accent rounded-full">
</div>
<div class="flex flex-col md:flex-row md:items-baseline gap-4 mb-4">
<h3 class="text-2xl font-bold text-white uppercase">Biohazard VFX</h3>
<span class="font-mono text-xs text-brand-accent bg-brand-accent/10 px-2 py-1">FOUNDER / VFX
SUPERVISOR</span>
<span class="font-mono text-xs text-slate-500 ml-auto">MAR 2022 - OCT 2025</span>
</div>
<p class="text-slate-400 mb-6 leading-relaxed">
Founded and led a VFX studio specializing in high-end commercial and music video work for
Post Malone, ENHYPEN, and Nike. Architected a custom pipeline combining cloud and
self-hosted infrastructure.
</p>
<ul class="space-y-2 mb-6">
<li class="flex items-start gap-3 text-sm text-slate-300">
<span class="text-brand-accent mt-1">▹</span>
Designed 7-plate reconciliation workflows for ENHYPEN (projection mapping live action
onto CAD).
</li>
<li class="flex items-start gap-3 text-sm text-slate-300">
<span class="text-brand-accent mt-1">▹</span>
Developed QA systems for AI-generated assets, transforming mid-tier output into
production-ready deliverables.
</li>
</ul>
</div>
<!-- Item 2 -->
<div class="pl-12 mb-20 relative reveal-text">
<div class="absolute left-[-5px] top-2 w-2.5 h-2.5 bg-slate-700 rounded-full"></div>
<div class="flex flex-col md:flex-row md:items-baseline gap-4 mb-4">
<h3 class="text-2xl font-bold text-white uppercase">Stinkfilms</h3>
<span class="font-mono text-xs text-slate-400 border border-slate-700 px-2 py-1">GLOBAL
PRODUCTION STUDIO</span>
<span class="font-mono text-xs text-slate-500 ml-auto">SUMMER 2024</span>
</div>
<p class="text-slate-400 mb-6 leading-relaxed">
Led Biohazard VFX team (60+ artists) alongside director Felix Brady to create a brand film
for G-Star Raw.
</p>
<div
class="border border-white/5 bg-white/5 p-6 backdrop-blur-sm hover:border-brand-accent/50 transition-colors cursor-pointer group">
<h4 class="text-sm font-bold uppercase text-white mb-2 flex justify-between">
Project: G-Star Raw Olympics Campaign
<i data-lucide="arrow-right"
class="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity text-brand-accent"></i>
</h4>
<p class="text-xs text-slate-400 mb-4">Managed full CG environments in Blender/Houdini and
integrated AI/ML workflows (Stable Diffusion reference gen, Copycat cleanup).</p>
<a href="https://stinkfilms.com"
class="text-[10px] font-bold text-brand-accent uppercase tracking-widest">View Case
Study</a>
</div>
</div>
<!-- Item 3 -->
<div class="pl-12 relative reveal-text">
<div class="absolute left-[-5px] top-2 w-2.5 h-2.5 bg-slate-700 rounded-full"></div>
<div class="flex flex-col md:flex-row md:items-baseline gap-4 mb-4">
<h3 class="text-2xl font-bold text-white uppercase">Freelance</h3>
<span class="font-mono text-xs text-slate-400 border border-slate-700 px-2 py-1">2D/3D
ARTIST</span>
<span class="font-mono text-xs text-slate-500 ml-auto">2015 - PRESENT</span>
</div>
<p class="text-slate-400 mb-4 leading-relaxed">
Compositor for Abyss Digital and major labels (Atlantic, Interscope). Clients: David
Kushner, Opium, Lil Durk, Don Toliver.
</p>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,55 @@
---
---
<section id="work" class="py-24">
<div class="container mx-auto px-6 lg:px-12 mb-12">
<span class="text-xs font-mono text-brand-accent mb-2 block">/// HIGHLIGHT</span>
<h2 class="text-5xl md:text-7xl font-bold uppercase text-white mb-4">G-Star Raw <span
class="text-stroke">Olympics</span></h2>
</div>
<!-- Full Width Project Card -->
<div class="w-full h-[80vh] relative group overflow-hidden border-y border-white/10">
<!-- Abstract Background representing the project -->
<div
class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=2564&auto=format&fit=crop')] bg-cover bg-center transition-transform duration-1000 group-hover:scale-105">
</div>
<div class="absolute inset-0 bg-brand-dark/80 mix-blend-multiply"></div>
<div class="absolute inset-0 bg-gradient-to-t from-brand-dark via-brand-dark/20 to-transparent"></div>
<!-- Grid Overlay on image -->
<div
class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdHRlcm4gaWQ9ImIiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGNpcmNsZSBjeD0iMiIgY3k9IjIiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4xKSIvPjwvcGF0dGVybj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9InVybCgjYikiLz48L3BhdHRlcm4+PC9kZWZzPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjYSkiLz48L3N2Zz4=')] opacity-30">
</div>
<div
class="absolute bottom-0 left-0 w-full p-6 lg:p-12 flex flex-col md:flex-row items-end justify-between">
<div
class="max-w-2xl transform translate-y-8 group-hover:translate-y-0 transition-transform duration-500">
<div class="flex gap-2 mb-4">
<span class="bg-brand-accent text-brand-dark text-[10px] font-bold uppercase px-2 py-1">VFX
Supervision</span>
<span
class="border border-white/30 text-white text-[10px] font-bold uppercase px-2 py-1">AI/ML</span>
<span
class="border border-white/30 text-white text-[10px] font-bold uppercase px-2 py-1">Houdini</span>
</div>
<p class="text-xl md:text-2xl text-white font-medium mb-6">
Managed full CG environment builds, procedural city generation, and integrated AI-generated
normal maps for relighting in Nuke.
</p>
<a href="https://f.io/7ijf23Wm" target="_blank"
class="inline-flex items-center gap-3 text-sm font-bold uppercase tracking-widest text-white hover:text-brand-accent transition-colors">
Watch Making Of <i data-lucide="play-circle" class="w-5 h-5"></i>
</a>
</div>
<div class="hidden md:block text-right">
<span class="block text-[10px] uppercase text-slate-500 tracking-widest mb-1">Year</span>
<span class="block text-2xl font-bold text-white mb-4">2024</span>
<span class="block text-[10px] uppercase text-slate-500 tracking-widest mb-1">Client</span>
<span class="block text-xl font-bold text-white">Stinkfilms</span>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,61 @@
---
---
<section class="container mx-auto px-6 lg:px-12 min-h-[70vh] flex flex-col justify-center relative">
<div class="grid grid-cols-1 md:grid-cols-12 gap-6">
<div class="col-span-12">
<p class="font-mono text-brand-accent mb-4 ml-1 reveal-text">/// TECHNICAL GENERALIST & VFX
SUPERVISOR</p>
<h1
class="text-6xl md:text-8xl lg:text-[10rem] font-bold text-massive uppercase leading-none tracking-tighter mb-8 text-white">
<span class="reveal-text block delay-100">Visual</span>
<span
class="reveal-text block delay-200 text-transparent bg-clip-text bg-gradient-to-tr from-brand-accent to-white">Alchemist</span>
</h1>
</div>
<div
class="col-span-12 md:col-span-6 lg:col-span-5 lg:col-start-8 mt-12 border-l border-brand-accent/30 pl-8 reveal-text delay-300">
<p class="text-slate-400 text-lg leading-relaxed mb-8">
I am a problem solver who loves visual effects. With 10 years of experience creating end-to-end
visual content for clients like <span class="text-white font-semibold">Post Malone</span>, <span
class="text-white font-semibold">Stinkfilms</span>, and <span
class="text-white font-semibold">Adidas</span>. Comfortable managing teams while staying
knee-deep in hands-on shot work.
</p>
<div class="flex gap-4">
<a href="#work"
class="group flex items-center gap-3 text-xs font-bold uppercase tracking-widest text-white hover:text-brand-accent transition-colors">
<span class="w-8 h-[1px] bg-brand-accent group-hover:w-12 transition-all"></span>
View Selected Works
</a>
</div>
</div>
</div>
<!-- Hero Footer -->
<div
class="absolute bottom-0 w-full left-0 px-6 lg:px-12 pb-12 hidden md:flex justify-between items-end opacity-50">
<div class="font-mono text-xs text-slate-500">
LOC: COLORADO SPRINGS<br>
TIME: <span id="clock">00:00:00</span>
</div>
<div class="font-mono text-xs text-slate-500 text-right">
SCROLL<br>
</div>
</div>
</section>
<script>
// Clock
function updateClock() {
const clockEl = document.getElementById('clock');
if (!clockEl) return;
const now = new Date();
const timeString = now.toLocaleTimeString('en-US', {hour12: false, timeZone: 'America/Denver'});
clockEl.textContent = timeString + " MST";
}
setInterval(updateClock, 1000);
updateClock();
</script>

View File

@ -0,0 +1,73 @@
---
---
<section id="skills" class="container mx-auto px-6 lg:px-12 py-24 bg-brand-panel border-y border-white/5">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
<div class="col-span-1 md:col-span-2 lg:col-span-4 mb-8">
<h2 class="text-4xl font-bold uppercase mb-2">Technical Arsenal</h2>
<p class="text-slate-400 font-mono text-sm">/// SOFTWARE & LANGUAGES</p>
</div>
<!-- Compositing -->
<div>
<h3 class="text-lg font-bold text-white uppercase mb-6 flex items-center gap-2">
<i data-lucide="layers" class="w-4 h-4 text-brand-accent"></i> Compositing
</h3>
<div class="flex flex-wrap gap-2">
<span class="skill-tag">Nuke/NukeX</span>
<span class="skill-tag">ComfyUI</span>
<span class="skill-tag">After Effects</span>
<span class="skill-tag">Photoshop</span>
<span class="skill-tag">Deep Compositing</span>
<span class="skill-tag">Live Action VFX</span>
</div>
</div>
<!-- 3D -->
<div>
<h3 class="text-lg font-bold text-white uppercase mb-6 flex items-center gap-2">
<i data-lucide="box" class="w-4 h-4 text-brand-accent"></i> 3D Generalist
</h3>
<div class="flex flex-wrap gap-2">
<span class="skill-tag">Houdini</span>
<span class="skill-tag">Blender</span>
<span class="skill-tag">Maya</span>
<span class="skill-tag">USD</span>
<span class="skill-tag">Solaris/Karma</span>
<span class="skill-tag">Unreal Engine</span>
<span class="skill-tag">Substance</span>
<span class="skill-tag">Procedural Gen</span>
</div>
</div>
<!-- AI/ML -->
<div>
<h3 class="text-lg font-bold text-white uppercase mb-6 flex items-center gap-2">
<i data-lucide="cpu" class="w-4 h-4 text-brand-accent"></i> AI/ML Integration
</h3>
<div class="flex flex-wrap gap-2">
<span class="skill-tag bg-brand-accent/10 border-brand-accent/50 text-brand-accent">Stable
Diffusion</span>
<span class="skill-tag">LoRA Training</span>
<span class="skill-tag">Dataset Prep</span>
<span class="skill-tag">Synthetic Data</span>
<span class="skill-tag">Prompt Engineering</span>
</div>
</div>
<!-- Dev -->
<div>
<h3 class="text-lg font-bold text-white uppercase mb-6 flex items-center gap-2">
<i data-lucide="code" class="w-4 h-4 text-brand-accent"></i> Development
</h3>
<div class="flex flex-wrap gap-2">
<span class="skill-tag">Python</span>
<span class="skill-tag">JavaScript</span>
<span class="skill-tag">React</span>
<span class="skill-tag">Docker</span>
<span class="skill-tag">Linux</span>
<span class="skill-tag">Pipeline Dev</span>
</div>
</div>
</div>
</section>

View File

@ -1,5 +1,33 @@
// Place any global data in this file.
// You can import this data from anywhere in your site by using the `import` keyword.
export const SITE_TITLE = 'Astro Blog';
export const SITE_DESCRIPTION = 'Welcome to my website!';
export const SITE_TITLE = 'Nicholai Vogel | VFX Artist & Technical Generalist';
export const SITE_DESCRIPTION = 'Visual effects artist and technical generalist with 10 years of experience creating end-to-end visual content for Post Malone, Stinkfilms, and Adidas. Founder of Biohazard VFX.';
// Contact Information
export const CONTACT_EMAIL = 'nicholai@nicholai.work';
export const CONTACT_PHONE = '719 660 4281';
// Location
export const LOCATION = 'COLORADO SPRINGS';
export const TIMEZONE = 'America/Denver';
export const TIMEZONE_ABBR = 'MST';
// Status
export const AVAILABILITY_STATUS = 'AVAILABLE FOR WORK';
// Social Links
export const SOCIAL_LINKS = {
website: 'https://nicholai.work',
instagram: '@nicholai.exe',
linkedin: 'https://linkedin.com/in/nicholai-vogel',
biohazard: 'https://biohazardvfx.com'
};
// Navigation
export const NAV_LINKS = [
{ href: '#about', label: '01. Profile' },
{ href: '#work', label: '02. Selected Works' },
{ href: '#experience', label: '03. History' },
{ href: '#skills', label: '04. Stack' }
];

View File

@ -0,0 +1,63 @@
---
import BaseHead from '../components/BaseHead.astro';
import GridOverlay from '../components/GridOverlay.astro';
import Navigation from '../components/Navigation.astro';
import Footer from '../components/Footer.astro';
import CustomCursor from '../components/CustomCursor';
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
import type { ImageMetadata } from 'astro';
interface Props {
title?: string;
description?: string;
image?: ImageMetadata;
}
const { title = SITE_TITLE, description = SITE_DESCRIPTION, image } = Astro.props;
---
<!doctype html>
<html lang="en" class="scroll-smooth">
<head>
<BaseHead title={title} description={description} image={image} />
</head>
<body class="antialiased selection:bg-brand-accent selection:text-brand-dark">
<GridOverlay />
<Navigation />
<main class="relative z-10">
<slot />
</main>
<Footer />
<CustomCursor client:load />
<script is:inline>
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Intersection Observer for Reveal Animations
const observerOptions = {
threshold: 0.1,
rootMargin: "0px"
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, observerOptions);
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.reveal-text').forEach(el => {
observer.observe(el);
});
});
</script>
</body>
</html>

View File

@ -1,86 +1,37 @@
---
import { Image } from 'astro:assets';
import type { CollectionEntry } from 'astro:content';
import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro';
import { Image } from 'astro:assets';
import BaseLayout from './BaseLayout.astro';
import FormattedDate from '../components/FormattedDate.astro';
import Header from '../components/Header.astro';
type Props = CollectionEntry<'blog'>['data'];
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
---
<html lang="en">
<head>
<BaseHead title={title} description={description} />
<style>
main {
width: calc(100% - 2em);
max-width: 100%;
margin: 0;
}
.hero-image {
width: 100%;
}
.hero-image img {
display: block;
margin: 0 auto;
border-radius: 12px;
box-shadow: var(--box-shadow);
}
.prose {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 1em;
color: rgb(var(--gray-dark));
}
.title {
margin-bottom: 1em;
padding: 1em 0;
text-align: center;
line-height: 1;
}
.title h1 {
margin: 0 0 0.5em 0;
}
.date {
margin-bottom: 0.5em;
color: rgb(var(--gray));
}
.last-updated-on {
font-style: italic;
}
</style>
</head>
<body>
<Header />
<main>
<article>
<div class="hero-image">
{heroImage && <Image width={1020} height={510} src={heroImage} alt="" />}
</div>
<div class="prose">
<div class="title">
<div class="date">
<FormattedDate date={pubDate} />
{
updatedDate && (
<div class="last-updated-on">
Last updated on <FormattedDate date={updatedDate} />
</div>
)
}
</div>
<h1>{title}</h1>
<hr />
<BaseLayout title={title} description={description}>
<article class="pt-32 pb-24 px-6 lg:px-12">
<div class="container mx-auto max-w-3xl">
<div class="hero-image mb-12 rounded-xl overflow-hidden border border-white/10 shadow-2xl shadow-brand-accent/5">
{heroImage && <Image width={1020} height={510} src={heroImage} alt="" class="w-full object-cover" />}
</div>
<div class="prose max-w-none">
<div class="title mb-12 text-center border-b border-white/10 pb-12">
<div class="date mb-4 text-slate-400 font-mono text-sm uppercase tracking-widest">
<FormattedDate date={pubDate} />
{
updatedDate && (
<div class="last-updated-on inline-block ml-4 pl-4 border-l border-slate-700 text-slate-500 italic">
Last updated on <FormattedDate date={updatedDate} />
</div>
)
}
</div>
<slot />
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4 leading-tight">{title}</h1>
<hr class="border-brand-accent w-24 mx-auto mt-8" />
</div>
</article>
</main>
<Footer />
</body>
</html>
<slot />
</div>
</div>
</article>
</BaseLayout>

View File

@ -1,63 +0,0 @@
---
import AboutHeroImage from '../assets/blog-placeholder-about.jpg';
import Layout from '../layouts/BlogPost.astro';
---
<Layout
title="About Me"
description="Lorem ipsum dolor sit amet"
pubDate={new Date('August 08 2021')}
heroImage={AboutHeroImage}
>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo
viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam
adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus
et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus
vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque
sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
</p>
<p>
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non
tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non
blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna
porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis
massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc.
Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis
bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra
massa massa ultricies mi.
</p>
<p>
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl
suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet
nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae
turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem
dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat
semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus
vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum
facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam
vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla
urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
</p>
<p>
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper
viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc
scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur
gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus
pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim
blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id
cursus metus aliquam eleifend mi.
</p>
<p>
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta
nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam
tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci
ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar
proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
</p>
</Layout>

View File

@ -1,114 +1,52 @@
---
import { Image } from 'astro:assets';
import { getCollection } from 'astro:content';
import BaseHead from '../../components/BaseHead.astro';
import Footer from '../../components/Footer.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import FormattedDate from '../../components/FormattedDate.astro';
import Header from '../../components/Header.astro';
import { SITE_DESCRIPTION, SITE_TITLE } from '../../consts';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
<style>
main {
width: 960px;
<BaseLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
<section class="pt-32 pb-24 px-6 lg:px-12 container mx-auto">
<div class="mb-16">
<p class="font-mono text-brand-accent mb-4 ml-1">/// THOUGHTS & NOTES</p>
<h1 class="text-4xl md:text-6xl font-bold uppercase tracking-tight text-white">Blog</h1>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{
posts.map((post) => (
<a href={`/blog/${post.id}/`} class="group block border border-white/5 bg-white/5 hover:border-brand-accent/50 transition-colors p-4 rounded-lg decoration-none">
<div class="overflow-hidden rounded-md mb-4 border border-white/5 aspect-[16/9] relative">
{post.data.heroImage ? (
<Image
width={720}
height={360}
src={post.data.heroImage}
alt=""
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
) : (
<div class="w-full h-full bg-brand-panel flex items-center justify-center text-slate-600 font-mono">
NO IMAGE
</div>
)}
<div class="absolute inset-0 bg-brand-dark/20 group-hover:bg-transparent transition-colors"></div>
</div>
<div class="px-2">
<h4 class="text-xl font-bold text-white mb-2 group-hover:text-brand-accent transition-colors">{post.data.title}</h4>
<p class="text-xs font-mono text-slate-400 uppercase tracking-widest mb-0">
<FormattedDate date={post.data.pubDate} />
</p>
</div>
</a>
))
}
ul {
display: flex;
flex-wrap: wrap;
gap: 2rem;
list-style-type: none;
margin: 0;
padding: 0;
}
ul li {
width: calc(50% - 1rem);
}
ul li * {
text-decoration: none;
transition: 0.2s ease;
}
ul li:first-child {
width: 100%;
margin-bottom: 1rem;
text-align: center;
}
ul li:first-child img {
width: 100%;
}
ul li:first-child .title {
font-size: 2.369rem;
}
ul li img {
margin-bottom: 0.5rem;
border-radius: 12px;
}
ul li a {
display: block;
}
.title {
margin: 0;
color: rgb(var(--black));
line-height: 1;
}
.date {
margin: 0;
color: rgb(var(--gray));
}
ul li a:hover h4,
ul li a:hover .date {
color: rgb(var(--accent));
}
ul a:hover img {
box-shadow: var(--box-shadow);
}
@media (max-width: 720px) {
ul {
gap: 0.5em;
}
ul li {
width: 100%;
text-align: center;
}
ul li:first-child {
margin-bottom: 0;
}
ul li:first-child .title {
font-size: 1.563em;
}
}
</style>
</head>
<body>
<Header />
<main>
<section>
<ul>
{
posts.map((post) => (
<li>
<a href={`/blog/${post.id}/`}>
{post.data.heroImage && (
<Image width={720} height={360} src={post.data.heroImage} alt="" />
)}
<h4 class="title">{post.data.title}</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} />
</p>
</a>
</li>
))
}
</ul>
</section>
</main>
<Footer />
</body>
</html>
</div>
</section>
</BaseLayout>

View File

@ -1,49 +1,19 @@
---
import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro';
import Header from '../components/Header.astro';
import { SITE_DESCRIPTION, SITE_TITLE } from '../consts';
import BaseLayout from '../layouts/BaseLayout.astro';
import Hero from '../components/sections/Hero.astro';
import Experience from '../components/sections/Experience.astro';
import FeaturedProject from '../components/sections/FeaturedProject.astro';
import Skills from '../components/sections/Skills.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main>
<h1>🧑‍🚀 Hello, Astronaut!</h1>
<p>
Welcome to the official <a href="https://astro.build/">Astro</a> blog starter template. This
template serves as a lightweight, minimally-styled starting point for anyone looking to build
a personal website, blog, or portfolio with Astro.
</p>
<p>
This template comes with a few integrations already configured in your
<code>astro.config.mjs</code> file. You can customize your setup with
<a href="https://astro.build/integrations">Astro Integrations</a> to add tools like Tailwind,
React, or Vue to your project.
</p>
<p>Here are a few ideas on how to get started with the template:</p>
<ul>
<li>Edit this page in <code>src/pages/index.astro</code></li>
<li>Edit the site header items in <code>src/components/Header.astro</code></li>
<li>Add your name to the footer in <code>src/components/Footer.astro</code></li>
<li>Check out the included blog posts in <code>src/content/blog/</code></li>
<li>Customize the blog post page layout in <code>src/layouts/BlogPost.astro</code></li>
</ul>
<p>
Have fun! If you get stuck, remember to
<a href="https://docs.astro.build/">read the docs</a>
or <a href="https://astro.build/chat">join us on Discord</a> to ask questions.
</p>
<p>
Looking for a blog template with a bit more personality? Check out
<a href="https://github.com/Charca/astro-blog-template">astro-blog-template</a>
by <a href="https://twitter.com/Charca">Maxi Ferreira</a>.
</p>
</main>
<Footer />
</body>
</html>
<BaseLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
<Hero />
<div class="w-full h-[1px] bg-white/10 my-24"></div>
<Experience />
<div class="container mx-auto px-6 lg:px-12">
<div class="w-full h-[1px] bg-white/10"></div>
</div>
<FeaturedProject />
<Skills />
</BaseLayout>

View File

@ -1,155 +1,147 @@
/*
The CSS in this style tag is based off of Bear Blog's default CSS.
https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css
License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
*/
@import "tailwindcss";
:root {
--accent: #2337ff;
--accent-dark: #000d8a;
--black: 15, 18, 25;
--gray: 96, 115, 159;
--gray-light: 229, 233, 240;
--gray-dark: 34, 41, 57;
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
--box-shadow:
0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%), 0 16px 32px
rgba(var(--gray), 33%);
}
@font-face {
font-family: "Atkinson";
src: url("/fonts/atkinson-regular.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Atkinson";
src: url("/fonts/atkinson-bold.woff") format("woff");
font-weight: 700;
font-style: normal;
font-display: swap;
}
body {
font-family: "Atkinson", sans-serif;
margin: 0;
padding: 0;
text-align: left;
background: linear-gradient(var(--gray-gradient)) no-repeat;
background-size: 100% 600px;
word-wrap: break-word;
overflow-wrap: break-word;
color: rgb(var(--gray-dark));
font-size: 20px;
line-height: 1.7;
}
main {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 3em 1em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 0.5rem 0;
color: rgb(var(--black));
line-height: 1.2;
}
h1 {
font-size: 3.052em;
}
h2 {
font-size: 2.441em;
}
h3 {
font-size: 1.953em;
}
h4 {
font-size: 1.563em;
}
h5 {
font-size: 1.25em;
}
strong,
b {
font-weight: 700;
}
a {
color: var(--accent);
}
a:hover {
color: var(--accent);
}
p {
margin-bottom: 1em;
}
.prose p {
margin-bottom: 2em;
}
textarea {
width: 100%;
font-size: 16px;
}
input {
font-size: 16px;
}
table {
width: 100%;
}
img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
code {
padding: 2px 5px;
background-color: rgb(var(--gray-light));
border-radius: 2px;
}
pre {
padding: 1.5em;
border-radius: 8px;
}
pre > code {
all: unset;
}
blockquote {
border-left: 4px solid var(--accent);
padding: 0 0 0 20px;
margin: 0;
font-size: 1.333em;
}
hr {
border: none;
border-top: 1px solid rgb(var(--gray-light));
}
@media (max-width: 720px) {
body {
font-size: 18px;
}
main {
padding: 1em;
}
@theme {
--color-brand-dark: #0B0D11;
--color-brand-panel: #151921;
--color-brand-accent: #FFB84C;
--color-brand-cyan: #22D3EE;
--color-brand-border: rgba(255, 255, 255, 0.1);
--color-brand-borderStrong: rgba(255, 255, 255, 0.2);
--font-sans: "Inter", sans-serif;
--font-mono: "Space Mono", monospace;
--spacing-128: 32rem;
--background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops));
}
.sr-only {
border: 0;
padding: 0;
margin: 0;
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
/* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
clip: rect(1px 1px 1px 1px);
/* maybe deprecated but we need to support legacy browsers */
clip: rect(1px, 1px, 1px, 1px);
/* modern browsers, clip-path works inwards from each corner */
clip-path: inset(50%);
/* added line to stop words getting smushed together (as they go onto separate lines and some screen readers do not understand line feeds as a space */
white-space: nowrap;
/* Custom Utilities */
@layer utilities {
.text-massive {
line-height: 0.9;
letter-spacing: -0.04em;
}
.text-stroke {
-webkit-text-stroke: 1px rgba(255, 255, 255, 0.1);
color: transparent;
}
.grid-overlay {
background-size: 100px 100px;
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
pointer-events: none;
z-index: 0;
}
}
/* Base Styles */
@layer base {
html {
scroll-behavior: smooth;
}
body {
@apply bg-brand-dark text-white overflow-x-hidden cursor-none antialiased selection:bg-brand-accent selection:text-brand-dark;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #0B0D11;
}
::-webkit-scrollbar-thumb {
background: #334155;
}
::-webkit-scrollbar-thumb:hover {
background: #FFB84C;
}
/* Custom Cursor Base Styles */
.cursor-dot,
.cursor-outline {
position: fixed;
top: 0;
left: 0;
transform: translate(-50%, -50%);
border-radius: 50%;
z-index: 9999;
pointer-events: none;
}
.cursor-dot {
width: 8px;
height: 8px;
background-color: #FFB84C;
}
.cursor-outline {
width: 40px;
height: 40px;
border: 1px solid rgba(255, 184, 76, 0.5);
transition: width 0.2s, height 0.2s, background-color 0.2s;
}
/* Interactive Elements for Cursor */
.hover-trigger:hover ~ .cursor-outline,
a:hover ~ .cursor-outline,
button:hover ~ .cursor-outline {
width: 60px;
height: 60px;
background-color: rgba(255, 184, 76, 0.05);
border-color: #FFB84C;
}
}
/* Component Styles */
@layer components {
.skill-tag {
@apply text-[10px] font-mono font-bold uppercase tracking-wider px-2 py-2 border border-slate-700 text-slate-400 hover:border-brand-accent hover:text-white transition-colors cursor-default select-none;
}
.btn-primary {
@apply bg-brand-accent text-brand-dark px-8 py-4 text-xs font-bold uppercase tracking-widest hover:bg-white transition-colors inline-block;
}
.btn-ghost {
@apply border border-slate-600 text-white px-8 py-4 text-xs font-bold uppercase tracking-widest hover:border-brand-accent hover:bg-brand-accent/5 transition-colors inline-block;
}
.reveal-text {
opacity: 0;
transform: translateY(20px);
transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal-text.active {
opacity: 1;
transform: translateY(0);
}
/* Typography / Prose */
.prose {
@apply text-slate-300 leading-relaxed;
}
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
@apply text-white font-bold mt-12 mb-6;
}
.prose h1 { @apply text-4xl; }
.prose h2 { @apply text-3xl; }
.prose h3 { @apply text-2xl; }
.prose p { @apply mb-6; }
.prose ul { @apply list-disc list-inside mb-6 marker:text-brand-accent; }
.prose ol { @apply list-decimal list-inside mb-6 marker:text-brand-accent; }
.prose a { @apply text-brand-accent hover:underline; }
.prose strong { @apply text-white; }
.prose blockquote { @apply border-l-4 border-brand-accent pl-6 italic text-slate-400 my-8; }
.prose img { @apply rounded-lg border border-white/10 my-8 w-full; }
.prose code { @apply font-mono bg-white/10 px-1 py-0.5 rounded text-sm text-brand-cyan; }
.prose pre { @apply bg-brand-panel p-6 rounded-lg overflow-x-auto my-8 border border-white/5; }
.prose pre code { @apply bg-transparent text-inherit p-0; }
}