.Add projects collection, dev page, navigation, hero fixes

- Create structured projects collection with schema
- Add new dev page and navigation link
- Refactor hero animation for view transitions
- Update project descriptions and tags
- Add new projects: Summit Painting, United Tattoos, etc.
- Trapped in endless work, despise this commit

Hubert The Eunuch
This commit is contained in:
Nicholai 2026-01-02 02:31:53 -07:00
parent 7a24dcc387
commit 13ad01a247
9 changed files with 523 additions and 99 deletions

View File

@ -133,4 +133,96 @@ export async function updateClasses(
### Next Steps
- [ ] Monitor for any script re-execution issues common with View Transitions.
- [ ] Consider adding custom transition animations for specific elements if needed.
- [ ] Consider adding custom transition animations for specific elements if needed.
## 2026-01-02 - Work Page & Projects Collection Implementation
### Changes Made
- Updated `src/content.config.ts` to include a new `projects` collection with schema for title, description, link, category, and tags.
- Created `src/content/projects/` directory and added `united-tattoos.mdx` and `the-highering-agency.mdx`.
- Created `src/components/ProjectCard.astro` for modular project display.
- Created `src/pages/work.astro` to list development and design projects.
- Updated `src/components/Navigation.astro` to include "Work" in desktop and mobile menus.
### Decisions
- Created a separate `projects` collection rather than overloading `pages` to allow for structured, queryable project data.
- Placed "Work" between "Home" and "Blog" in navigation to emphasize the portfolio aspect.
- Included a VFX/Technical Art section at the bottom of the Work page to bridge the gap between web dev and the existing VFX portfolio.
### How to Test
1. Navigate to the new `/work` page.
2. Verify both "United Tattoos" and "The Highering Agency" are displayed with correct information and links.
3. Test navigation from Home -> Work and Work -> Blog using the new Client Router.
4. Verify mobile menu contains the "Work" link and functions correctly.
### Next Steps
- [ ] Add screenshots/images to the projects in `src/content/projects/`.
- [ ] Expand the `ProjectCard` to support hover effects or mini-galleries if desired.
## 2026-01-02 - Project Descriptions Update
### Changes Made
- Updated `src/content/projects/the-highering-agency.mdx` description to reflect its status as a cannabis staffing agency with specific features like direct hire and executive search.
- Updated `src/content/projects/united-tattoos.mdx` description and tags based on its official README, highlighting Astro 5, booking system features, and editorial design.
### Decisions
- Ensured project descriptions are accurate and highlight the specific technical and business value of each project.
- Added relevant tags like "Astro", "GSAP", "Booking System" to United Tattoo to showcase technical depth.
### How to Test
1. Navigate to `/work`.
2. Verify the text for "The Highering Agency" mentions cannabis staffing and recruitment services.
3. Verify "United Tattoo" description mentions "Official marketing website", "Astro 5", and "Booking System".
## 2026-01-02 - Hero Animation Fix for Client Router
### Changes Made
- Refactored `src/components/sections/Hero.astro` script.
- Wrapped initialization logic (intro, clock, grid, pulse) in `initHero()` function.
- Added event listener for `astro:page-load` to run `initHero()` on every navigation.
- Added `cleanup()` function triggered on `astro:before-swap` to clear timers (`clockTimer`, `pulseInterval`).
### Decisions
- Moving logic to `astro:page-load` is required for View Transitions (ClientRouter) because standard `window.onload` only fires on the first page visit.
- Explicit cleanup prevents memory leaks and double-firing timers when navigating back and forth.
### How to Test
1. Load the homepage directly. Verify Hero animations (text fade-in, grid pulse) work.
2. Navigate to another page (e.g., `/work`).
3. Click "Home" in the navigation.
4. Verify Hero animations re-trigger correctly without a full page reload.
5. Check console for any errors (none expected).
## 2026-01-02 - Dev Page Redesign & Rename
### Changes Made
- Renamed `src/pages/work.astro` to `src/pages/dev.astro`.
- Updated `src/components/Navigation.astro` to link to `/dev` labeled "DEV".
- Redesigned `src/pages/dev.astro` completely:
- **Layout:** Switched from a sparse grid to a dense, 2-column "System Module" layout.
- **Aesthetics:** Aligned with the homepage's Industrial Dark Mode (scanlines, mono fonts, technical overlays).
- **Visuals:** Added CSS-based animated mesh backgrounds and SVG decorators to cards, eliminating the need for screenshots while maintaining a high-tech feel.
- **Typography:** Updated headers to be massive and uppercase, matching the Hero section.
### Decisions
- Renaming to "Dev" better reflects the technical nature of the portfolio and distinguishes it from the VFX work.
- The previous card layout was too simple and looked "sloppy" without images. The new "System Module" design uses CSS/SVG abstracts to look finished and professional without requiring assets.
- Integrated "secondary specialization" section (VFX) with better styling to act as a bridge between the two portfolio halves.
### How to Test
1. Navigate to `/dev` via the new navigation link.
2. Verify the page title is "DEV LOG".
3. Check that the project cards look like technical modules with animated backgrounds.
4. Hover over cards to see the accent color shift.
5. Verify the "Initialize Uplink" buttons link to the correct project URLs.
## 2026-01-02 - Project Added: Summit Painting & Handyman
### Changes Made
- Created `src/content/projects/summit-painting.mdx` with details from its official README.
- Set the preview link to `https://summit-painting-and-handyman-services.pages.dev/`.
- Updated tags and description to highlight Astro 5 and React 19 usage.
### Decisions
- Added as the third project in the Dev list to maintain a chronological or curated flow.
- Highlights the transition to Astro 5 and content-driven design patterns common in recent work.

View File

@ -25,6 +25,17 @@ import ThemeToggle from './ThemeToggle.astro';
Astro.url.pathname === '/' ? "w-full" : "w-0 group-hover:w-full"
]}></span>
</a>
<a href="/dev"
class:list={[
"relative text-xs font-semibold uppercase tracking-[0.15em] transition-all duration-300 py-2 group",
Astro.url.pathname.startsWith('/dev') ? "text-[var(--theme-text-primary)]" : "text-[var(--theme-text-muted)] hover:text-[var(--theme-text-primary)]"
]}>
<span class="relative z-10">Dev</span>
<span class:list={[
"absolute bottom-0 left-0 h-[1px] bg-brand-accent transition-all duration-300 ease-out",
Astro.url.pathname.startsWith('/dev') ? "w-full" : "w-0 group-hover:w-full"
]}></span>
</a>
<a href="/blog"
class:list={[
"relative text-xs font-semibold uppercase tracking-[0.15em] transition-all duration-300 py-2 group",
@ -84,6 +95,12 @@ import ThemeToggle from './ThemeToggle.astro';
>
Home
</a>
<a
href="/dev"
class="mobile-nav-link text-3xl font-bold uppercase tracking-wider text-[var(--theme-text-primary)] hover:text-brand-accent transition-colors duration-300"
>
Dev
</a>
<a
href="/blog"
class="mobile-nav-link text-3xl font-bold uppercase tracking-wider text-[var(--theme-text-primary)] hover:text-brand-accent transition-colors duration-300"

View File

@ -0,0 +1,82 @@
---
interface Props {
title: string;
description: string;
link: string;
category: string;
tags?: string[];
order?: number;
}
const { title, description, link, category, tags = [], order = 0 } = Astro.props;
---
<div class="group relative flex flex-col h-full border border-[var(--theme-border-primary)] bg-[var(--theme-hover-bg)] hover:border-brand-accent/40 transition-all duration-500 overflow-hidden">
<!-- Accent indicator strip -->
<div class="absolute top-0 left-0 w-1 h-full bg-brand-accent"></div>
<div class="absolute top-0 left-0 w-full h-1 bg-brand-accent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="p-8 lg:p-10 flex flex-col h-full">
<!-- Technical header -->
<div class="flex items-center gap-3 mb-6">
<span class="text-[10px] font-mono text-brand-accent uppercase tracking-widest font-bold">
PRJ.0{order || 'X'}
</span>
<span class="h-px w-8 bg-[var(--theme-border-strong)]"></span>
<span class="text-[10px] font-mono text-[var(--theme-text-muted)] uppercase tracking-widest">
{category}
</span>
</div>
<!-- Title -->
<a href={link} target="_blank" rel="noopener noreferrer">
<h2 class="text-3xl lg:text-4xl font-bold text-[var(--theme-text-primary)] uppercase tracking-tight mb-6 group-hover:text-brand-accent transition-colors duration-300 leading-tight">
{title}
</h2>
</a>
<!-- Description -->
<p class="text-[var(--theme-text-secondary)] text-base font-light leading-relaxed mb-8 flex-grow">
{description}
</p>
<!-- Tags -->
{tags && tags.length > 0 && (
<div class="flex flex-wrap gap-2 mb-10">
{tags.map((tag) => (
<span class="px-3 py-1.5 text-[10px] font-mono uppercase border border-[var(--theme-border-primary)] text-[var(--theme-text-muted)] group-hover:border-[var(--theme-border-strong)] transition-colors">
{tag}
</span>
))}
</div>
)}
<!-- Link -->
<div class="pt-6 border-t border-[var(--theme-border-primary)] mt-auto">
<a
href={link}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-4 text-xs font-bold uppercase tracking-widest text-[var(--theme-text-primary)] hover:text-brand-accent transition-all duration-300 group/link"
>
Launch Project
<span class="block w-8 h-[1px] bg-[var(--theme-border-strong)] group-hover/link:bg-brand-accent group-hover/link:w-12 transition-all duration-300"></span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="group-hover/link:translate-x-1 transition-transform duration-300"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</a>
</div>
</div>
</div>

View File

@ -146,40 +146,52 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false;
const finePointer = window.matchMedia?.('(pointer: fine) and (hover: hover)')?.matches ?? false;
// ===== CLOCK (pause on hidden tab, align to second boundaries) =====
let clockTimer = 0;
// Track active timers to clear them on navigation/cleanup
let clockTimer: number | undefined;
let pulseInterval: number | undefined;
function updateClockOnce() {
const clock = document.getElementById('clock');
if (!clock) return;
const now = new Date();
const timeString = now.toLocaleTimeString('en-US', { hour12: false, timeZone: 'America/Denver' });
clock.textContent = `${timeString} MST`;
function cleanup() {
if (clockTimer) window.clearTimeout(clockTimer);
if (pulseInterval) window.clearInterval(pulseInterval);
clockTimer = undefined;
pulseInterval = undefined;
}
function startClock() {
if (clockTimer) window.clearTimeout(clockTimer);
function initHero() {
// Clean up previous instances first
cleanup();
const tick = () => {
if (document.hidden) {
clockTimer = window.setTimeout(tick, 1000);
return;
// ===== CLOCK =====
const clock = document.getElementById('clock');
if (clock) {
function updateClockOnce() {
if (!clock) return;
const now = new Date();
const timeString = now.toLocaleTimeString('en-US', { hour12: false, timeZone: 'America/Denver' });
clock.textContent = `${timeString} MST`;
}
updateClockOnce();
// Align to the next second boundary to reduce drift.
const msToNextSecond = 1000 - (Date.now() % 1000);
clockTimer = window.setTimeout(tick, msToNextSecond);
};
function startClock() {
const tick = () => {
// Stop if element is gone
if (!document.body.contains(clock)) return;
tick();
}
if (document.hidden) {
clockTimer = window.setTimeout(tick, 1000);
return;
}
startClock();
updateClockOnce();
// Align to the next second boundary to reduce drift.
const msToNextSecond = 1000 - (Date.now() % 1000);
clockTimer = window.setTimeout(tick, msToNextSecond);
};
tick();
}
startClock();
}
// Intro Animation Sequence
window.addEventListener('load', () => {
// ===== INTRO ANIMATIONS =====
// Trigger Intro Elements
const introElements = document.querySelectorAll('.intro-element');
introElements.forEach(el => {
@ -192,9 +204,11 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
portrait.classList.add('portrait-visible');
}
const section = document.getElementById('hero');
const cells = document.querySelectorAll('.grid-cell');
// Trigger Grid Ripple (skip if reduced motion)
if (!reduceMotion) {
const cells = document.querySelectorAll('.grid-cell');
if (!reduceMotion && cells.length > 0) {
// Diagonal sweep effect
cells.forEach((cell, i) => {
const row = Math.floor(i / 10);
@ -209,93 +223,100 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
}, delay);
});
}
});
// ===== GRID INTERACTION =====
if (section && cells.length > 0) {
// Throttle mousemove work to one update per frame.
let latestX = 0;
let latestY = 0;
let pending = false;
let lastIndex = -1;
const timeouts: number[] = new Array(cells.length).fill(0);
// Robust Grid Interaction
const section = document.getElementById('hero');
const cells = document.querySelectorAll('.grid-cell');
const process = () => {
pending = false;
if (!finePointer || reduceMotion) return;
// Safety check if section is still valid
if (!document.body.contains(section)) return;
if (section) {
// Throttle mousemove work to one update per frame.
let latestX = 0;
let latestY = 0;
let pending = false;
let lastIndex = -1;
const timeouts: number[] = new Array(cells.length).fill(0);
const rect = section.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
if (width <= 0 || height <= 0) return;
const process = () => {
pending = false;
if (!finePointer || reduceMotion) return;
const x = latestX - rect.left;
const y = latestY - rect.top;
const rect = section.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
if (width <= 0 || height <= 0) return;
const col = Math.floor((x / width) * 10);
const row = Math.floor((y / height) * 10);
const x = latestX - rect.left;
const y = latestY - rect.top;
if (col < 0 || col >= 10 || row < 0 || row >= 10) return;
const col = Math.floor((x / width) * 10);
const row = Math.floor((y / height) * 10);
const index = row * 10 + col;
if (index === lastIndex) return;
lastIndex = index;
if (col < 0 || col >= 10 || row < 0 || row >= 10) return;
const cell = cells[index] as HTMLElement | undefined;
if (!cell) return;
const index = row * 10 + col;
if (index === lastIndex) return;
lastIndex = index;
cell.classList.add('active');
const cell = cells[index] as HTMLElement | undefined;
if (!cell) return;
const prev = timeouts[index];
if (prev) window.clearTimeout(prev);
cell.classList.add('active');
// Shorter hold time for a quicker trail.
timeouts[index] = window.setTimeout(() => {
cell.classList.remove('active');
timeouts[index] = 0;
}, 35);
};
const prev = timeouts[index];
if (prev) window.clearTimeout(prev);
section.addEventListener('mousemove', (e) => {
latestX = e.clientX;
latestY = e.clientY;
if (pending) return;
pending = true;
window.requestAnimationFrame(process);
}, { passive: true });
}
// Shorter hold time for a quicker trail.
timeouts[index] = window.setTimeout(() => {
cell.classList.remove('active');
timeouts[index] = 0;
}, 35);
};
// ===== PULSE ANIMATION =====
if (finePointer && !reduceMotion && cells.length > 0) {
pulseInterval = window.setInterval(() => {
if (document.hidden) return;
// Stop if elements are gone
if (cells.length > 0 && !document.body.contains(cells[0])) {
clearInterval(pulseInterval);
return;
}
section.addEventListener('mousemove', (e) => {
latestX = e.clientX;
latestY = e.clientY;
if (pending) return;
pending = true;
window.requestAnimationFrame(process);
}, { passive: true });
const randomIndex = Math.floor(Math.random() * cells.length);
const cell = cells[randomIndex] as HTMLElement | undefined;
if (!cell) return;
cell.classList.add('active');
window.setTimeout(() => {
cell.classList.remove('active');
}, 160);
}, 1200);
}
}
// Random pulse for liveliness
let pulseInterval = 0;
function startPulse() {
if (pulseInterval) window.clearInterval(pulseInterval);
if (!finePointer || reduceMotion) return;
pulseInterval = window.setInterval(() => {
if (document.hidden) return;
const randomIndex = Math.floor(Math.random() * cells.length);
const cell = cells[randomIndex] as HTMLElement | undefined;
if (!cell) return;
cell.classList.add('active');
window.setTimeout(() => {
cell.classList.remove('active');
}, 160);
}, 1200);
}
startPulse();
// Run on every navigation
document.addEventListener('astro:page-load', initHero);
// Clean up before swap
document.addEventListener('astro:before-swap', cleanup);
// Visibility change handler for clock updates (keep persistent)
document.addEventListener('visibilitychange', () => {
// Keep timers light in background.
if (!document.hidden) {
updateClockOnce();
const clock = document.getElementById('clock');
if (!document.hidden && clock) {
const now = new Date();
const timeString = now.toLocaleTimeString('en-US', { hour12: false, timeZone: 'America/Denver' });
clock.textContent = `${timeString} MST`;
}
});
</script>

View File

@ -106,4 +106,18 @@ const pages = defineCollection({
}),
});
export const collections = { blog, sections, pages };
const projects = defineCollection({
loader: glob({ base: './src/content/projects', pattern: '**/*.{md,mdx}' }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
link: z.string(),
category: z.string(),
tags: z.array(z.string()).optional(),
image: image().optional(),
order: z.number().optional().default(0),
}),
});
export const collections = { blog, sections, pages, projects };

View File

@ -0,0 +1,12 @@
---
title: "Kampüs Cadıları"
description: "A modern Astro migration of the Kampüs Cadıları feminist collective platform. Converted from a React SPA to a high-performance Astro 5 application with React islands, bilingual routing (TR/EN), and brutalist design aesthetics."
link: "https://thehigheringagency.com"
category: "Astro Migration"
tags:
- "Astro 5"
- "React Islands"
- "i18n"
- "Brutalist Design"
order: 2
---

View File

@ -0,0 +1,12 @@
---
title: "Summit Painting & Handyman"
description: "The official digital platform for a Colorado Springs-based service provider. Built with Astro 5 and React 19, the site features a content-driven architecture, automated image optimization, and an elegant design system tailored for craftsmanship showcase."
link: "https://summit-painting-and-handyman-services.pages.dev/"
category: "Website Design & Development"
tags:
- "Astro 5"
- "React 19"
- "Cloudflare Pages"
- "Content Collections"
order: 3
---

View File

@ -0,0 +1,12 @@
---
title: "United Tattoo"
description: "The official marketing website for United Tattoo in Fountain, Colorado. Built with Astro 5, it features dynamic artist portfolios, a multi-step booking system with file uploads, and a modern editorial design aesthetic powered by GSAP and Lenis animations."
link: "https://united-tattoos.com"
category: "Website Design & Development"
tags:
- "Astro"
- "GSAP"
- "Booking System"
- "Editorial Design"
order: 1
---

162
src/pages/dev.astro Normal file
View File

@ -0,0 +1,162 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '../layouts/BaseLayout.astro';
import { SITE_TITLE } from '../consts';
// Fetch all projects sorted by order
const allProjects = (await getCollection('projects')).sort(
(a, b) => a.data.order - b.data.order,
);
const pageTitle = `Dev | ${SITE_TITLE}`;
---
<BaseLayout title={pageTitle} description="Development projects and system architecture.">
<!-- Global Background Elements -->
<div class="fixed inset-0 z-0 pointer-events-none">
<div class="absolute inset-0 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,112,0.06))] bg-[length:100%_2px,3px_100%] opacity-[0.04]"></div>
<div class="absolute inset-0 bg-[linear-gradient(var(--theme-grid-line)_1px,transparent_1px),linear-gradient(90deg,var(--theme-grid-line)_1px,transparent_1px)] bg-[size:100px_100px] pointer-events-none opacity-10"></div>
</div>
<!-- Page Hero -->
<section class="relative z-10 px-6 lg:px-12 pt-32 lg:pt-48 pb-20 border-b border-[var(--theme-border-primary)]">
<!-- Back Navigation -->
<div class="absolute top-12 lg:top-24 left-6 lg:left-12">
<a href="/" class="inline-flex items-center gap-3 text-xs font-mono font-bold uppercase tracking-widest text-[var(--theme-text-muted)] hover:text-brand-accent transition-colors duration-300 group">
<span class="text-brand-accent group-hover:-translate-x-1 transition-transform duration-300">&lt;</span>
<span>RETURN_TO_HOME</span>
</a>
</div>
<div class="max-w-7xl mx-auto">
<div class="flex items-center gap-3 mb-8 animate-on-scroll fade-in">
<div class="w-2 h-2 bg-brand-accent animate-pulse"></div>
<span class="font-mono text-[10px] uppercase tracking-[0.3em] text-brand-accent">SYS.DEV /// INDEX</span>
</div>
<h1 class="text-6xl md:text-8xl lg:text-9xl font-bold uppercase tracking-tighter leading-[0.85] mb-12 animate-on-scroll slide-up">
<span class="block text-[var(--theme-text-primary)]">DEV</span>
<span class="block text-brand-accent">LOG</span>
</h1>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 animate-on-scroll slide-up stagger-1">
<div class="lg:col-span-8">
<p class="text-[var(--theme-text-secondary)] text-lg md:text-xl font-light leading-relaxed border-l border-brand-accent/30 pl-6 max-w-2xl">
Deploying scalable web solutions and high-performance applications.
</p>
</div>
</div>
</div>
</section>
<!-- Projects Grid -->
<section class="relative z-10 py-24 container mx-auto px-6 lg:px-12">
<div class="grid grid-cols-1 gap-24">
{allProjects.map((project, index) => (
<article class={`group relative border border-[var(--theme-border-primary)] bg-[var(--theme-bg-secondary)] overflow-hidden flex flex-col lg:flex-row animate-on-scroll slide-up stagger-${(index % 4) + 1} min-h-[600px]`}>
<!-- Left: Info Panel -->
<div class="relative z-10 w-full lg:w-1/3 p-8 lg:p-12 flex flex-col justify-between bg-[var(--theme-bg-secondary)]/95 backdrop-blur-sm border-b lg:border-b-0 lg:border-r border-[var(--theme-border-primary)]">
<!-- Header -->
<div>
<div class="flex justify-between items-start mb-8">
<div class="font-mono text-[10px] text-brand-accent uppercase tracking-[0.2em] flex items-center gap-3">
<span>PRJ.0{project.data.order}</span>
<span class="w-1.5 h-1.5 bg-brand-accent rounded-full animate-pulse"></span>
<span>LIVE_FEED</span>
</div>
</div>
<h2 class="text-3xl lg:text-5xl font-bold uppercase tracking-tight text-[var(--theme-text-primary)] mb-6 group-hover:text-brand-accent transition-colors duration-300">
{project.data.title}
</h2>
<p class="text-[var(--theme-text-secondary)] text-base leading-relaxed font-light mb-8">
{project.data.description}
</p>
<!-- Stack -->
<div class="font-mono text-[10px] text-[var(--theme-text-subtle)] uppercase tracking-[0.2em] mb-4">
/// STACK_MANIFEST
</div>
{project.data.tags && (
<div class="flex flex-wrap gap-2 mb-8">
{project.data.tags.map((tag) => (
<span class="px-3 py-1.5 text-[10px] font-mono uppercase bg-[var(--theme-bg-tertiary)] border border-[var(--theme-border-primary)] text-[var(--theme-text-muted)] group-hover:text-[var(--theme-text-secondary)] group-hover:border-[var(--theme-border-strong)] transition-colors">
{tag}
</span>
))}
</div>
)}
</div>
<a
href={project.data.link}
target="_blank"
rel="noopener noreferrer"
class="w-full flex items-center justify-between px-6 py-4 bg-[var(--theme-hover-bg)] border border-[var(--theme-border-primary)] text-xs font-bold uppercase tracking-widest text-[var(--theme-text-primary)] hover:bg-brand-accent hover:text-brand-dark hover:border-brand-accent transition-all duration-300 group/btn mt-auto"
>
<span>Initialize Project</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" class="group-hover/btn:translate-x-1 transition-transform">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
</div>
<!-- Right: Live Preview (Iframe) -->
<div class="relative w-full lg:w-2/3 h-[400px] lg:h-auto overflow-hidden bg-[var(--theme-bg-primary)] group-hover:border-brand-accent/50 transition-colors duration-500">
<!-- Iframe Container with Scale Hack for Desktop View -->
<div class="w-[200%] h-[200%] origin-top-left transform scale-50 pointer-events-none">
<iframe
src={project.data.link}
class="w-full h-full border-none grayscale-[0.8] group-hover:grayscale-0 transition-all duration-700 opacity-60 group-hover:opacity-100"
loading="lazy"
title={`Live preview of ${project.data.title}`}
></iframe>
</div>
<!-- Scanline Overlay -->
<div class="absolute inset-0 pointer-events-none z-10 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,112,0.06))] bg-[length:100%_2px,3px_100%] opacity-20"></div>
<!-- Vignette -->
<div class="absolute inset-0 pointer-events-none z-10 shadow-[inset_0_0_100px_rgba(0,0,0,0.8)]"></div>
<!-- Hover "Engage" Overlay -->
<div class="absolute inset-0 z-20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none">
<div class="px-6 py-3 bg-brand-accent/10 border border-brand-accent backdrop-blur-md text-brand-accent font-mono text-xs font-bold uppercase tracking-widest">
Live Connection
</div>
</div>
</div>
</article>
))}
</div>
</section>
<!-- VFX Bridge Section -->
<section class="relative z-10 py-32 border-t border-[var(--theme-border-primary)] bg-[var(--theme-bg-secondary)]">
<div class="container mx-auto px-6 lg:px-12">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
<div class="lg:col-span-7">
<div class="flex items-center gap-3 mb-6">
<span class="font-mono text-[10px] uppercase tracking-[0.3em] text-brand-accent">/// SECONDARY_SPECIALIZATION</span>
</div>
<h2 class="text-4xl md:text-5xl font-bold uppercase text-[var(--theme-text-primary)] mb-6 tracking-tight">
Visual Effects & <br/> <span class="text-brand-accent">Technical Art</span>
</h2>
<p class="text-[var(--theme-text-secondary)] text-lg font-light leading-relaxed max-w-xl">
Beyond traditional web development, I specialize in high-end VFX production and pipeline automation.
</p>
</div>
<div class="lg:col-span-5 flex justify-end">
<a href="/#work" class="group relative inline-flex items-center justify-center gap-6 px-10 py-5 bg-brand-accent/5 border border-brand-accent/30 hover:bg-brand-accent hover:border-brand-accent transition-all duration-300">
<span class="relative z-10 font-mono text-xs font-bold uppercase tracking-[0.2em] text-brand-accent group-hover:text-brand-dark transition-colors">View VFX Portfolio</span>
<div class="relative z-10 w-8 h-8 flex items-center justify-center border border-brand-accent/20 group-hover:border-brand-dark/30 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" class="text-brand-accent group-hover:text-brand-dark group-hover:translate-x-1 transition-all">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
</a>
</div>
</div>
</div>
</section>
</BaseLayout>