Add new blog components for enhanced navigation and reading experience

- Introduced PostNavigation, ReadingProgress, RelatedPosts, and TableOfContents components to improve user navigation and engagement within blog posts.
- Updated BlogPost layout to incorporate new components, providing a cohesive reading experience with navigation links to previous and next posts, a reading progress bar, and related articles.
- Enhanced global CSS styles for better typography and layout consistency across blog components.
This commit is contained in:
Nicholai 2025-12-08 04:15:31 -07:00
parent 058655f23d
commit 6625112e2c
7 changed files with 859 additions and 72 deletions

View File

@ -0,0 +1,139 @@
---
import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
interface NavPost {
title: string;
href: string;
heroImage?: ImageMetadata;
}
interface Props {
prevPost?: NavPost;
nextPost?: NavPost;
}
const { prevPost, nextPost } = Astro.props;
---
{(prevPost || nextPost) && (
<nav class="post-navigation mt-20 pt-12 border-t border-white/10" aria-label="Post navigation">
<div class="flex items-center gap-4 mb-8">
<span class="text-[10px] font-mono text-slate-500 uppercase tracking-widest font-bold">
/// CONTINUE READING
</span>
<span class="h-px flex-grow bg-white/10"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Previous Post -->
{prevPost ? (
<a
href={prevPost.href}
class="group relative flex items-center gap-6 p-6 border border-white/10 bg-white/[0.02] hover:border-brand-accent/40 hover:bg-white/[0.04] transition-all duration-500"
>
<div class="absolute top-0 left-0 w-1 h-full bg-slate-700 opacity-50 group-hover:bg-brand-accent group-hover:opacity-100 transition-all duration-500"></div>
<!-- Arrow -->
<div class="flex-shrink-0 w-10 h-10 flex items-center justify-center border border-white/10 group-hover:border-brand-accent/50 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="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-slate-500 group-hover:text-brand-accent transition-colors group-hover:-translate-x-1 transition-transform duration-300"
>
<path d="M19 12H5" />
<path d="m12 19-7-7 7-7" />
</svg>
</div>
<!-- Content -->
<div class="flex-grow min-w-0">
<span class="text-[10px] font-mono text-slate-500 uppercase tracking-widest mb-2 block">
Previous
</span>
<h4 class="text-sm font-bold text-white uppercase tracking-tight truncate group-hover:text-brand-accent transition-colors">
{prevPost.title}
</h4>
</div>
<!-- Thumbnail -->
{prevPost.heroImage && (
<div class="hidden sm:block flex-shrink-0 w-16 h-16 overflow-hidden border border-white/10">
<Image
src={prevPost.heroImage}
alt=""
width={64}
height={64}
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
</div>
)}
</a>
) : (
<div></div>
)}
<!-- Next Post -->
{nextPost ? (
<a
href={nextPost.href}
class="group relative flex items-center gap-6 p-6 border border-white/10 bg-white/[0.02] hover:border-brand-accent/40 hover:bg-white/[0.04] transition-all duration-500"
>
<div class="absolute top-0 right-0 w-1 h-full bg-slate-700 opacity-50 group-hover:bg-brand-accent group-hover:opacity-100 transition-all duration-500"></div>
<!-- Thumbnail -->
{nextPost.heroImage && (
<div class="hidden sm:block flex-shrink-0 w-16 h-16 overflow-hidden border border-white/10">
<Image
src={nextPost.heroImage}
alt=""
width={64}
height={64}
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
</div>
)}
<!-- Content -->
<div class="flex-grow min-w-0 text-right">
<span class="text-[10px] font-mono text-slate-500 uppercase tracking-widest mb-2 block">
Next
</span>
<h4 class="text-sm font-bold text-white uppercase tracking-tight truncate group-hover:text-brand-accent transition-colors">
{nextPost.title}
</h4>
</div>
<!-- Arrow -->
<div class="flex-shrink-0 w-10 h-10 flex items-center justify-center border border-white/10 group-hover:border-brand-accent/50 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="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-slate-500 group-hover:text-brand-accent transition-colors group-hover:translate-x-1 transition-transform duration-300"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</div>
</a>
) : (
<div></div>
)}
</div>
</nav>
)}

View File

@ -0,0 +1,50 @@
---
// Reading progress bar that tracks scroll position
---
<div id="reading-progress-container" class="fixed top-0 left-0 w-full h-[3px] z-[100] bg-brand-dark/50">
<div id="reading-progress-bar" class="h-full bg-brand-accent w-0 transition-[width] duration-100 ease-out"></div>
</div>
<script>
function initReadingProgress() {
const progressBar = document.getElementById('reading-progress-bar');
if (!progressBar) return;
function updateProgress() {
const article = document.querySelector('article');
if (!article) return;
const articleRect = article.getBoundingClientRect();
const articleTop = window.scrollY + articleRect.top;
const articleHeight = article.offsetHeight;
const windowHeight = window.innerHeight;
const scrollY = window.scrollY;
// Calculate progress based on article position
const start = articleTop;
const end = articleTop + articleHeight - windowHeight;
const current = scrollY;
let progress = 0;
if (current >= start && current <= end) {
progress = ((current - start) / (end - start)) * 100;
} else if (current > end) {
progress = 100;
}
progressBar.style.width = `${Math.min(100, Math.max(0, progress))}%`;
}
window.addEventListener('scroll', updateProgress, { passive: true });
window.addEventListener('resize', updateProgress, { passive: true });
updateProgress();
}
// Initialize on page load
initReadingProgress();
// Re-initialize on Astro page transitions
document.addEventListener('astro:page-load', initReadingProgress);
</script>

View File

@ -0,0 +1,48 @@
---
import BlogCard from './BlogCard.astro';
import type { ImageMetadata } from 'astro';
interface RelatedPost {
title: string;
description: string;
pubDate: Date;
heroImage?: ImageMetadata;
category?: string;
tags?: string[];
href: string;
}
interface Props {
posts: RelatedPost[];
class?: string;
}
const { posts, class: className = '' } = Astro.props;
---
{posts.length > 0 && (
<section class:list={['related-posts mt-20 pt-12 border-t border-white/10', className]}>
<div class="flex items-center gap-4 mb-8">
<span class="text-[10px] font-mono text-slate-500 uppercase tracking-widest font-bold">
/// RELATED ARTICLES
</span>
<span class="h-px flex-grow bg-white/10"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.slice(0, 3).map((post) => (
<BlogCard
title={post.title}
description={post.description}
pubDate={post.pubDate}
heroImage={post.heroImage}
category={post.category}
tags={post.tags}
href={post.href}
variant="compact"
/>
))}
</div>
</section>
)}

View File

@ -0,0 +1,121 @@
---
interface Props {
headings: Array<{
depth: number;
slug: string;
text: string;
}>;
class?: string;
}
const { headings, class: className = '' } = Astro.props;
// Filter to only H2 and H3 headings
const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
---
{tocHeadings.length > 0 && (
<nav class:list={['toc', className]} data-toc aria-label="Table of contents">
<div class="flex items-center gap-3 mb-6">
<span class="text-[10px] font-mono text-slate-500 uppercase tracking-widest font-bold">
/// CONTENTS
</span>
<span class="h-px flex-grow bg-white/10"></span>
</div>
<ul class="space-y-3">
{tocHeadings.map((heading) => (
<li>
<a
href={`#${heading.slug}`}
data-toc-link={heading.slug}
class:list={[
'toc-link block text-sm transition-all duration-300 hover:text-white',
heading.depth === 2
? 'text-slate-400 font-medium'
: 'text-slate-500 pl-4 text-xs',
]}
>
<span class="flex items-center gap-2">
{heading.depth === 2 && (
<span class="w-1.5 h-1.5 bg-slate-600 rounded-full toc-indicator transition-colors duration-300"></span>
)}
{heading.text}
</span>
</a>
</li>
))}
</ul>
</nav>
)}
<script>
function initTableOfContents() {
const tocLinks = document.querySelectorAll('[data-toc-link]');
if (tocLinks.length === 0) return;
const headings = Array.from(tocLinks).map((link) => {
const slug = (link as HTMLElement).dataset.tocLink;
return document.getElementById(slug || '');
}).filter(Boolean) as HTMLElement[];
let currentActive: Element | null = null;
function updateActiveLink() {
const scrollY = window.scrollY;
const offset = 150; // Offset for when to activate
let activeHeading: HTMLElement | null = null;
for (const heading of headings) {
const rect = heading.getBoundingClientRect();
const top = rect.top + scrollY;
if (scrollY >= top - offset) {
activeHeading = heading;
}
}
if (activeHeading && currentActive !== activeHeading) {
// Remove active state from all links
tocLinks.forEach((link) => {
link.classList.remove('text-brand-accent', 'text-white');
link.classList.add('text-slate-400');
const indicator = link.querySelector('.toc-indicator');
if (indicator) {
indicator.classList.remove('bg-brand-accent');
indicator.classList.add('bg-slate-600');
}
});
// Add active state to current link
const activeLink = document.querySelector(`[data-toc-link="${activeHeading.id}"]`);
if (activeLink) {
activeLink.classList.remove('text-slate-400');
activeLink.classList.add('text-brand-accent');
const indicator = activeLink.querySelector('.toc-indicator');
if (indicator) {
indicator.classList.remove('bg-slate-600');
indicator.classList.add('bg-brand-accent');
}
}
currentActive = activeHeading;
}
}
window.addEventListener('scroll', updateActiveLink, { passive: true });
updateActiveLink();
}
// Initialize on page load
initTableOfContents();
// Re-initialize on Astro page transitions
document.addEventListener('astro:page-load', initTableOfContents);
</script>
<style>
.toc-link:hover .toc-indicator {
background-color: var(--color-brand-accent);
}
</style>

View File

@ -1,51 +1,230 @@
---
import type { CollectionEntry } from 'astro:content';
import type { ImageMetadata } from 'astro';
import BaseLayout from './BaseLayout.astro';
import FormattedDate from '../components/FormattedDate.astro';
import ReadingProgress from '../components/ReadingProgress.astro';
import TableOfContents from '../components/TableOfContents.astro';
import PostNavigation from '../components/PostNavigation.astro';
import RelatedPosts from '../components/RelatedPosts.astro';
import { Image } from 'astro:assets';
type Props = CollectionEntry<'blog'>['data'];
interface NavPost {
title: string;
href: string;
heroImage?: ImageMetadata;
}
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
interface RelatedPost {
title: string;
description: string;
pubDate: Date;
heroImage?: ImageMetadata;
category?: string;
tags?: string[];
href: string;
}
interface Props {
title: string;
description: string;
pubDate: Date;
updatedDate?: Date;
heroImage?: ImageMetadata;
category?: string;
tags?: string[];
headings?: Array<{ depth: number; slug: string; text: string }>;
prevPost?: NavPost;
nextPost?: NavPost;
relatedPosts?: RelatedPost[];
}
const {
title,
description,
pubDate,
updatedDate,
heroImage,
category,
tags,
headings = [],
prevPost,
nextPost,
relatedPosts = [],
} = Astro.props;
// Estimated read time (rough calculation)
const readTime = '5 min read';
---
<BaseLayout title={title} description={description}>
<article class="container mx-auto px-6 lg:px-12 max-w-4xl">
<!-- Back Navigation -->
<a href="/blog" class="inline-flex items-center gap-3 text-xs font-semibold uppercase tracking-widest text-slate-500 hover:text-white transition-colors duration-300 mb-12 group">
<span class="w-8 h-[1px] bg-slate-600 group-hover:bg-brand-accent group-hover:w-12 transition-all duration-300"></span>
Back to Blog
</a>
<BaseLayout title={title} description={description} image={heroImage}>
<ReadingProgress />
<article class="relative pb-24">
<!-- All content in same grid structure for consistent width -->
<div class="container mx-auto px-6 lg:px-12">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12">
<!-- Main Column -->
<div class="lg:col-span-8 lg:col-start-3">
<!-- Back Navigation -->
<div class="mb-8">
<a href="/blog" class="inline-flex items-center gap-3 text-xs font-semibold uppercase tracking-widest text-slate-500 hover:text-white transition-colors duration-300 group">
<span class="w-8 h-[1px] bg-slate-600 group-hover:bg-brand-accent group-hover:w-12 transition-all duration-300"></span>
Back to Blog
</a>
</div>
<div class="mb-12 animate-on-scroll slide-up">
<div class="mb-8">
{heroImage && <Image width={1020} height={510} src={heroImage} alt="" class="w-full h-auto border border-white/10" />}
</div>
<div class="border-b border-white/10 pb-10 mb-10">
<div class="flex items-center gap-4 text-xs font-mono text-slate-500 uppercase tracking-widest mb-6">
<FormattedDate date={pubDate} />
{
updatedDate && (
<span class="text-slate-600">
(Updated: <FormattedDate date={updatedDate} />)
</span>
)
}
<!-- Hero Section: Side-by-Side Layout -->
<header class="mb-16 lg:mb-20">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
<!-- Text Content -->
<div class="order-2 lg:order-1 animate-on-scroll slide-up">
<!-- Metadata -->
<div class="flex flex-wrap items-center gap-3 text-[10px] font-mono text-slate-500 uppercase tracking-widest mb-5">
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-brand-accent rounded-full animate-pulse"></div>
<span class="text-brand-accent font-bold">SYS.ARTICLE</span>
</div>
<span class="h-px w-4 bg-white/20"></span>
<FormattedDate date={pubDate} />
<span class="h-px w-4 bg-white/20"></span>
<span>{readTime}</span>
</div>
{category && (
<div class="mb-4">
<span class="px-3 py-1.5 text-[10px] font-mono font-bold uppercase tracking-widest bg-brand-accent/10 border border-brand-accent/30 text-brand-accent">
{category}
</span>
</div>
)}
<h1 class="text-2xl md:text-3xl lg:text-4xl font-bold text-white uppercase leading-[0.95] tracking-tighter mb-4">
{title}
</h1>
<p class="text-sm lg:text-base text-slate-400 leading-relaxed font-light mb-5 border-l border-white/10 pl-4">
{description}
</p>
<!-- Tags -->
{tags && tags.length > 0 && (
<div class="flex flex-wrap gap-2">
{tags.map((tag) => (
<span class="px-2 py-1 text-[9px] font-mono uppercase border border-white/10 text-slate-500 hover:border-white/20 transition-colors">
{tag}
</span>
))}
</div>
)}
</div>
<!-- Hero Image -->
{heroImage && (
<div class="order-1 lg:order-2 animate-on-scroll fade-in">
<div class="relative aspect-[4/3] overflow-hidden border border-white/10 bg-white/[0.02]">
<Image
src={heroImage}
alt=""
width={600}
height={450}
class="w-full h-full object-cover"
/>
<!-- Subtle overlay -->
<div class="absolute inset-0 bg-brand-dark/10"></div>
<div class="absolute inset-0 grid-overlay opacity-20 pointer-events-none"></div>
</div>
</div>
)}
</div>
</header>
<!-- Main Content -->
<div class="prose-custom animate-on-scroll slide-up stagger-1">
<slot />
</div>
<!-- Author Footer -->
<footer class="mt-24 pt-10 border-t border-white/10 animate-on-scroll fade-in">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div>
<p class="text-[10px] font-mono text-slate-500 uppercase tracking-widest mb-2">
/// END TRANSMISSION
</p>
<p class="text-slate-400 text-sm">
Published <FormattedDate date={pubDate} />
{updatedDate && (
<span class="text-slate-500"> · Last updated <FormattedDate date={updatedDate} /></span>
)}
</p>
</div>
<!-- Share Links -->
<div class="flex items-center gap-4">
<span class="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Share</span>
<div class="flex items-center gap-2">
<a
href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(Astro.url.href)}`}
target="_blank"
rel="noopener noreferrer"
class="w-10 h-10 flex items-center justify-center border border-white/10 text-slate-400 hover:border-brand-accent hover:text-brand-accent transition-all duration-300"
aria-label="Share on Twitter"
>
<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="round" stroke-linejoin="round">
<path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"/>
</svg>
</a>
<a
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(Astro.url.href)}&title=${encodeURIComponent(title)}`}
target="_blank"
rel="noopener noreferrer"
class="w-10 h-10 flex items-center justify-center border border-white/10 text-slate-400 hover:border-brand-accent hover:text-brand-accent transition-all duration-300"
aria-label="Share on LinkedIn"
>
<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="round" stroke-linejoin="round">
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/>
<rect width="4" height="12" x="2" y="9"/>
<circle cx="4" cy="4" r="2"/>
</svg>
</a>
<button
type="button"
onclick="navigator.clipboard.writeText(window.location.href)"
class="w-10 h-10 flex items-center justify-center border border-white/10 text-slate-400 hover:border-brand-accent hover:text-brand-accent transition-all duration-300"
aria-label="Copy link"
>
<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="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
</button>
</div>
</div>
</div>
</footer>
<!-- Post Navigation -->
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
<!-- Related Posts -->
<RelatedPosts posts={relatedPosts} />
<!-- Back to Blog -->
<div class="mt-20 pt-10 border-t border-white/10">
<a href="/blog" class="inline-flex items-center gap-3 text-xs font-semibold uppercase tracking-widest text-slate-500 hover:text-white transition-colors duration-300 group">
<span class="w-8 h-[1px] bg-slate-600 group-hover:bg-brand-accent group-hover:w-12 transition-all duration-300"></span>
Back to All Posts
</a>
</div>
</div>
<h1 class="text-3xl md:text-5xl lg:text-6xl font-bold text-white uppercase leading-tight tracking-tight mb-6">{title}</h1>
<p class="text-lg lg:text-xl text-slate-400 leading-relaxed font-light">{description}</p>
<!-- Table of Contents Sidebar (Desktop) -->
<aside class="hidden lg:block lg:col-span-2 lg:col-start-11">
<div class="sticky top-24 mt-32">
<TableOfContents headings={headings} />
</div>
</aside>
</div>
</div>
<div class="prose-custom animate-on-scroll slide-up stagger-1">
<slot />
</div>
<!-- Bottom Navigation -->
<div class="mt-20 pt-10 border-t border-white/10">
<a href="/blog" class="inline-flex items-center gap-3 text-xs font-semibold uppercase tracking-widest text-slate-500 hover:text-white transition-colors duration-300 group">
<span class="w-8 h-[1px] bg-slate-600 group-hover:bg-brand-accent group-hover:w-12 transition-all duration-300"></span>
Back to All Posts
</a>
</div>
</article>
</BaseLayout>

View File

@ -4,17 +4,99 @@ import BlogPost from '../../layouts/BlogPost.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
// Sort posts by date (newest first)
const sortedPosts = posts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
return sortedPosts.map((post, index) => {
// Get previous and next posts
const prevPost = index < sortedPosts.length - 1 ? sortedPosts[index + 1] : undefined;
const nextPost = index > 0 ? sortedPosts[index - 1] : undefined;
// Find related posts (same category or shared tags)
const relatedPosts = sortedPosts
.filter((p) => p.id !== post.id)
.filter((p) => {
// Match by category
if (post.data.category && p.data.category === post.data.category) {
return true;
}
// Match by shared tags
if (post.data.tags && p.data.tags) {
const sharedTags = post.data.tags.filter((tag) => p.data.tags?.includes(tag));
return sharedTags.length > 0;
}
return false;
})
.slice(0, 3);
return {
params: { slug: post.id },
props: {
post,
prevPost: prevPost
? {
title: prevPost.data.title,
href: `/blog/${prevPost.id}/`,
heroImage: prevPost.data.heroImage,
}
: undefined,
nextPost: nextPost
? {
title: nextPost.data.title,
href: `/blog/${nextPost.id}/`,
heroImage: nextPost.data.heroImage,
}
: undefined,
relatedPosts: relatedPosts.map((p) => ({
title: p.data.title,
description: p.data.description,
pubDate: p.data.pubDate,
heroImage: p.data.heroImage,
category: p.data.category,
tags: p.data.tags,
href: `/blog/${p.id}/`,
})),
},
};
});
}
type Props = CollectionEntry<'blog'>;
const post = Astro.props;
const { Content } = await render(post);
interface Props {
post: CollectionEntry<'blog'>;
prevPost?: {
title: string;
href: string;
heroImage?: any;
};
nextPost?: {
title: string;
href: string;
heroImage?: any;
};
relatedPosts: Array<{
title: string;
description: string;
pubDate: Date;
heroImage?: any;
category?: string;
tags?: string[];
href: string;
}>;
}
const { post, prevPost, nextPost, relatedPosts } = Astro.props;
const { Content, headings } = await render(post);
---
<BlogPost {...post.data}>
<BlogPost
{...post.data}
headings={headings}
prevPost={prevPost}
nextPost={nextPost}
relatedPosts={relatedPosts}
>
<Content />
</BlogPost>

View File

@ -318,6 +318,7 @@ a {
.prose-custom {
color: #94A3B8;
line-height: 1.8;
font-size: 1.0625rem;
}
.prose-custom h2 {
@ -326,10 +327,20 @@ a {
font-weight: 700;
text-transform: uppercase;
letter-spacing: -0.025em;
margin-top: 3rem;
margin-top: 3.5rem;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
scroll-margin-top: 6rem;
}
.prose-custom h2::before {
content: "//";
color: var(--color-brand-accent);
margin-right: 0.5rem;
font-family: var(--font-mono);
font-size: 0.9em;
}
.prose-custom h3 {
@ -338,8 +349,9 @@ a {
font-weight: 600;
text-transform: uppercase;
letter-spacing: -0.015em;
margin-top: 2rem;
margin-top: 2.5rem;
margin-bottom: 1rem;
scroll-margin-top: 6rem;
}
.prose-custom h4 {
@ -348,6 +360,7 @@ a {
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
scroll-margin-top: 6rem;
}
.prose-custom p {
@ -358,11 +371,12 @@ a {
color: var(--color-brand-accent);
text-decoration: none;
transition: color 0.3s ease;
border-bottom: 1px solid transparent;
}
.prose-custom a:hover {
color: #ffffff;
text-decoration: underline;
border-bottom-color: var(--color-brand-accent);
}
.prose-custom strong {
@ -383,7 +397,7 @@ a {
.prose-custom ul li {
position: relative;
padding-left: 1.5rem;
padding-left: 1.75rem;
margin-bottom: 0.75rem;
}
@ -392,69 +406,223 @@ a {
position: absolute;
left: 0;
color: var(--color-brand-accent);
font-size: 0.85em;
}
.prose-custom ol {
list-style: decimal;
padding-left: 1.5rem;
list-style: none;
padding-left: 0;
margin-bottom: 1.5rem;
counter-reset: ol-counter;
}
.prose-custom ol li {
margin-bottom: 0.75rem;
padding-left: 0.5rem;
padding-left: 2.5rem;
position: relative;
counter-increment: ol-counter;
}
.prose-custom ol li::marker {
.prose-custom ol li::before {
content: counter(ol-counter, decimal-leading-zero);
position: absolute;
left: 0;
color: var(--color-brand-accent);
font-weight: 600;
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 700;
width: 1.75rem;
}
/* Enhanced Blockquotes - Terminal/Industrial Style */
.prose-custom blockquote {
position: relative;
border-left: 3px solid var(--color-brand-accent);
padding-left: 1.5rem;
margin: 2rem 0;
background: linear-gradient(135deg, rgba(255, 77, 0, 0.05), rgba(21, 25, 33, 0.8));
padding: 1.5rem 1.5rem 1.5rem 2rem;
margin: 2.5rem 0;
font-style: italic;
color: #CBD5E1;
border-right: 1px solid rgba(255, 255, 255, 0.05);
border-top: 1px solid rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.prose-custom blockquote::before {
content: "///";
position: absolute;
top: -0.75rem;
left: 1rem;
background: var(--color-brand-dark);
padding: 0 0.5rem;
font-family: var(--font-mono);
font-size: 0.625rem;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--color-brand-accent);
font-style: normal;
}
.prose-custom blockquote p {
margin-bottom: 0;
}
.prose-custom blockquote p:last-child {
margin-bottom: 0;
}
/* Enhanced Code - Inline */
.prose-custom code {
color: var(--color-brand-accent);
background-color: rgba(255, 77, 0, 0.1);
padding: 0.2rem 0.4rem;
border-radius: 2px;
padding: 0.2rem 0.5rem;
border-radius: 0;
font-family: var(--font-mono);
font-size: 0.9em;
font-size: 0.85em;
border: 1px solid rgba(255, 77, 0, 0.2);
}
/* Enhanced Code Blocks - Terminal Style */
.prose-custom pre {
position: relative;
background-color: var(--color-brand-panel);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 1.5rem;
margin: 2rem 0;
overflow-x: auto;
padding: 0;
margin: 2.5rem 0;
overflow: hidden;
}
.prose-custom pre::before {
content: "TERMINAL";
display: block;
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding: 0.75rem 1rem;
font-family: var(--font-mono);
font-size: 0.625rem;
font-weight: 700;
letter-spacing: 0.15em;
color: #64748B;
text-transform: uppercase;
}
.prose-custom pre code {
display: block;
background: none;
padding: 0;
padding: 1.5rem;
color: #CBD5E1;
border: none;
overflow-x: auto;
}
/* Enhanced Horizontal Rules - Section Dividers */
.prose-custom hr {
border: none;
height: 1px;
background: linear-gradient(
to right,
transparent,
rgba(255, 255, 255, 0.2) 20%,
rgba(255, 255, 255, 0.2) 80%,
transparent
);
margin: 3rem 0;
height: auto;
margin: 4rem 0;
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.prose-custom hr::before {
content: "";
flex: 1;
height: 1px;
background: linear-gradient(to right, transparent, rgba(255, 77, 0, 0.3));
}
.prose-custom hr::after {
content: "";
flex: 1;
height: 1px;
background: linear-gradient(to left, transparent, rgba(255, 77, 0, 0.3));
}
/* Enhanced Images */
.prose-custom img {
border: 1px solid rgba(255, 255, 255, 0.1);
margin: 2rem 0;
margin: 2.5rem 0;
transition: border-color 0.3s ease;
}
.prose-custom img:hover {
border-color: rgba(255, 77, 0, 0.3);
}
/* Image Captions (for figures) */
.prose-custom figure {
margin: 2.5rem 0;
}
.prose-custom figure img {
margin: 0;
}
.prose-custom figcaption {
font-family: var(--font-mono);
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #64748B;
margin-top: 0.75rem;
padding-left: 0.5rem;
border-left: 2px solid var(--color-brand-accent);
}
/* Video containers */
.prose-custom .video-container {
margin: 2.5rem 0;
position: relative;
}
.prose-custom .video-container video {
width: 100%;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.prose-custom .video-container p {
font-family: var(--font-mono);
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #64748B;
margin-top: 0.75rem;
margin-bottom: 0;
}
/* Tables */
.prose-custom table {
width: 100%;
margin: 2.5rem 0;
border-collapse: collapse;
font-size: 0.9375rem;
}
.prose-custom thead {
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.prose-custom th {
font-family: var(--font-mono);
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #64748B;
padding: 1rem;
text-align: left;
}
.prose-custom td {
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
color: #94A3B8;
}
.prose-custom tr:hover td {
background: rgba(255, 255, 255, 0.02);
}