Add blog and redesign blog/roadmap pages

- Add blog collection (content config, prose styles, introducing-fabro post)
- Add Blog link to homepage nav and footer
- Make Layout description prop dynamic for per-page meta/OG tags
- Extract shared Nav, Footer, PageScripts components from duplicated markup
- Blog index: compact header, featured card for latest post, row list for older posts
- Blog post: clean reading surface (no grid/noise overlay), reading time, better header
- Roadmap: improved card contrast with tinted backgrounds, conditional section rendering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-16 18:26:10 -04:00
parent d8155a305c
commit 38ec5d2846
11 changed files with 466 additions and 186 deletions

View file

@ -0,0 +1,34 @@
<footer class="border-t border-navy-800/60 py-16">
<div class="mx-auto max-w-6xl px-6">
<div class="grid gap-10 sm:grid-cols-3">
<div>
<img src="/logotype.svg" alt="Fabro" class="h-6 mb-4" />
<p class="text-sm text-ice-300/60">The dark software factory for expert engineers.</p>
<p class="mt-2 text-xs text-ice-300/40">Open source, MIT-licensed</p>
</div>
<div>
<h3 class="font-display text-sm font-semibold uppercase tracking-wider text-ice-300/70 mb-4">Product</h3>
<ul class="space-y-2 text-sm">
<li><a href="https://docs.fabro.sh/getting-started/quick-start" class="text-ice-300 hover:text-ice-50 transition-colors">Quick Start</a></li>
<li><a href="https://docs.fabro.sh" class="text-ice-300 hover:text-ice-50 transition-colors">Docs</a></li>
<li><a href="/blog" class="text-ice-300 hover:text-ice-50 transition-colors">Blog</a></li>
<li><a href="/roadmap" class="text-ice-300 hover:text-ice-50 transition-colors">Roadmap</a></li>
<li><a href="https://docs.fabro.sh/changelog" class="text-ice-300 hover:text-ice-50 transition-colors">Changelog</a></li>
<li><a href="https://github.com/fabro-sh/fabro/releases" class="text-ice-300 hover:text-ice-50 transition-colors">Releases</a></li>
</ul>
</div>
<div>
<h3 class="font-display text-sm font-semibold uppercase tracking-wider text-ice-300/70 mb-4">Community</h3>
<ul class="space-y-2 text-sm">
<li><a href="/discord" class="text-ice-300 hover:text-ice-50 transition-colors">Discord</a></li>
<li><a href="https://github.com/fabro-sh/fabro" class="text-ice-300 hover:text-ice-50 transition-colors">GitHub</a></li>
<li><a href="https://github.com/fabro-sh/fabro/issues" class="text-ice-300 hover:text-ice-50 transition-colors">Issues</a></li>
<li><a href="https://github.com/fabro-sh/fabro/discussions" class="text-ice-300 hover:text-ice-50 transition-colors">Discussions</a></li>
</ul>
</div>
</div>
<div class="mt-10 border-t border-navy-800/40 pt-6 text-center text-xs text-ice-300/40">
&copy; 2025 Qlty Software Inc. All rights reserved.
</div>
</div>
</footer>

View file

@ -0,0 +1,66 @@
---
interface Props {
currentPage?: "blog" | "roadmap";
}
const { currentPage } = Astro.props;
const linkClass = (page?: string) =>
page === currentPage
? "text-ice-50"
: "text-ice-100 hover:text-ice-50";
---
<nav class="fixed top-0 z-50 w-full border-b border-navy-800/60 bg-navy-950/70 backdrop-blur-xl">
<div class="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<div class="flex items-center gap-8">
<a href="/" class="flex items-center">
<img src="/logotype.svg" alt="Fabro" class="h-7" />
</a>
<a href="https://docs.fabro.sh" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass()}`}>Docs</a>
<a href="/blog" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass("blog")}`}>Blog</a>
<a href="https://docs.fabro.sh/changelog" class={`hidden md:inline-flex text-sm font-medium transition-colors ${linkClass()}`}>Changelog</a>
</div>
<div class="flex items-center gap-5">
<a href="/discord" class="hidden md:block text-ice-300 hover:text-ice-50 transition-colors" aria-label="Discord">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
<a href="https://github.com/fabro-sh/fabro" class="hidden md:block text-ice-300 hover:text-ice-50 transition-colors" aria-label="GitHub">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
</a>
<a
href="https://docs.fabro.sh/getting-started/quick-start"
class="hidden md:inline-flex rounded-lg bg-teal-700 px-4 py-2 text-sm font-semibold text-white transition-all hover:bg-teal-500 hover:shadow-[0_0_20px_-4px_rgba(53,127,158,0.5)]"
>
Get started
</a>
<button id="mobile-menu-btn" class="md:hidden text-ice-300 hover:text-ice-50 transition-colors" aria-label="Open menu">
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
</div>
</div>
<div id="mobile-menu" class="md:hidden hidden border-t border-navy-800/60 bg-navy-950/95 backdrop-blur-xl">
<div class="px-6 py-5 space-y-4">
<a href="https://docs.fabro.sh" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Docs</a>
<a href="/blog" class={`block text-sm font-medium transition-colors ${linkClass("blog")}`}>Blog</a>
<a href="https://docs.fabro.sh/changelog" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Changelog</a>
<hr class="border-navy-800/60" />
<div class="flex items-center gap-5">
<a href="/discord" class="text-ice-300 hover:text-ice-50 transition-colors" aria-label="Discord">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
<a href="https://github.com/fabro-sh/fabro" class="text-ice-300 hover:text-ice-50 transition-colors" aria-label="GitHub">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
</a>
</div>
<a
href="https://docs.fabro.sh/getting-started/quick-start"
class="block w-full text-center rounded-lg bg-teal-700 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-teal-500"
>
Get started
</a>
</div>
</div>
</nav>

View file

@ -0,0 +1,27 @@
<script>
// Mobile menu toggle
const mobileMenuBtn = document.getElementById("mobile-menu-btn")!;
const mobileMenu = document.getElementById("mobile-menu")!;
mobileMenuBtn.addEventListener("click", () => {
const isOpen = !mobileMenu.classList.contains("hidden");
mobileMenu.classList.toggle("hidden");
mobileMenuBtn.setAttribute("aria-label", isOpen ? "Open menu" : "Close menu");
mobileMenuBtn.innerHTML = isOpen
? '<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>'
: '<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>';
});
// Scroll reveal
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("revealed");
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.15, rootMargin: "0px 0px -40px 0px" }
);
document.querySelectorAll(".reveal, .reveal-left, .reveal-right, .reveal-scale").forEach((el) => observer.observe(el));
</script>

View file

@ -12,4 +12,14 @@ const roadmap = defineCollection({
}),
});
export const collections = { roadmap };
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
author: z.string(),
}),
});
export const collections = { roadmap, blog };

View file

@ -0,0 +1,22 @@
---
title: "Introducing Fabro"
description: "Fabro is an open-source workflow orchestration platform that lets expert engineers define their process as a graph and let AI agents execute it."
date: 2026-03-16
author: "Fabro Team"
---
Most AI coding tools give you a chat window and hope for the best. Fabro takes a different approach: you define your engineering process as a workflow graph, and Fabro executes it — with verification gates, human checkpoints, and full observability at every step.
## Why workflows?
A chat-based agent is a single loop: prompt, act, repeat. That works for small tasks, but it falls apart when you need structure — when the plan should be approved before implementation begins, when tests must pass before the PR is opened, when a second model should cross-review the first.
Fabro workflows are DOT graphs. Each node is a stage with a specific role: planning, coding, reviewing, testing. Edges define the flow. Human-in-the-loop gates let you intervene where it matters and step back where it doesn't.
## Multi-model by design
Not every stage needs a frontier model. Fabro's CSS-like model stylesheets let you assign models by stage ID, class, or shape — so you can use fast models for triage and frontier models for the work that matters. Swap providers without changing your workflow.
## What's next
We're just getting started. Check the [roadmap](/roadmap) to see what we're building, and join us on [Discord](/discord) to shape what comes next.

View file

@ -3,9 +3,10 @@ import "../styles/global.css";
interface Props {
title: string;
description?: string;
}
const { title } = Astro.props;
const { title, description = "Fabro is the open source dark software factory for expert engineers. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters." } = Astro.props;
---
<!doctype html>
@ -13,7 +14,7 @@ const { title } = Astro.props;
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Fabro is the open source dark software factory for expert engineers. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters." />
<meta name="description" content={description} />
<meta name="application-name" content="Fabro" />
<meta name="apple-mobile-web-app-title" content="Fabro" />
<link rel="canonical" href={`https://fabro.sh${Astro.url.pathname}`} />
@ -21,7 +22,7 @@ const { title } = Astro.props;
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content="Fabro is the open source dark software factory for expert engineers. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters." />
<meta property="og:description" content={description} />
<meta property="og:image" content="https://fabro.sh/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
@ -31,7 +32,7 @@ const { title } = Astro.props;
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content="Fabro is the open source dark software factory for expert engineers. Define your process as a workflow graph, let AI agents execute it, and intervene only where it matters." />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content="https://fabro.sh/og-image.png" />
<meta name="twitter:image:width" content="1200" />
<meta name="twitter:image:height" content="630" />

View file

@ -0,0 +1,64 @@
---
import Layout from "../../layouts/Layout.astro";
import Nav from "../../components/Nav.astro";
import Footer from "../../components/Footer.astro";
import PageScripts from "../../components/PageScripts.astro";
import { getCollection, render } from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
const formatDate = (date: Date) =>
date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
const wordCount = post.body?.split(/\s+/).length ?? 0;
const readingTime = Math.max(1, Math.round(wordCount / 250));
---
<Layout title={`${post.data.title} — Fabro`} description={post.data.description}>
<Nav currentPage="blog" />
<main>
<article class="relative pt-24 sm:pt-28 pb-24">
<div class="relative mx-auto max-w-3xl px-6">
<!-- Back link -->
<a href="/blog" class="reveal inline-flex items-center gap-2 text-sm text-ice-300/60 hover:text-teal-300 transition-colors mb-10 group">
<svg class="h-4 w-4 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<polyline points="15,18 9,12 15,6" />
</svg>
Blog
</a>
<!-- Header -->
<header class="mb-10 border-b border-navy-800/40 pb-8">
<h1 class="reveal reveal-d1 font-display text-3xl font-bold tracking-tight text-ice-50 sm:text-4xl leading-[1.15]">
{post.data.title}
</h1>
<div class="reveal reveal-d2 mt-4 flex items-center gap-3 text-sm text-ice-300/50">
<time class="font-mono">{formatDate(post.data.date)}</time>
<span>&middot;</span>
<span>{post.data.author}</span>
<span>&middot;</span>
<span>{readingTime} min read</span>
</div>
</header>
<!-- Content -->
<div class="reveal reveal-d3 prose">
<Content />
</div>
</div>
</article>
</main>
<Footer />
<PageScripts />
</Layout>

View file

@ -0,0 +1,82 @@
---
import Layout from "../../layouts/Layout.astro";
import Nav from "../../components/Nav.astro";
import Footer from "../../components/Footer.astro";
import PageScripts from "../../components/PageScripts.astro";
import { getCollection } from "astro:content";
const posts = (await getCollection("blog")).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
const [featured, ...older] = posts;
const formatDate = (date: Date) =>
date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
---
<Layout title="Blog — Fabro" description="News, updates, and insights from the Fabro team.">
<Nav currentPage="blog" />
<main>
<!-- Header — compact, content-forward -->
<section class="relative pt-24 sm:pt-28 pb-10">
<div class="absolute inset-0 noise-overlay"></div>
<div class="pointer-events-none absolute top-8 -left-40 h-[400px] w-[400px] rounded-full bg-teal-500/8 blur-[120px]"></div>
<div class="relative mx-auto max-w-3xl px-6">
<h1 class="reveal font-display text-4xl font-bold tracking-tight text-ice-50 sm:text-5xl leading-[1.1]">
Blog
</h1>
<p class="reveal reveal-d1 mt-3 text-lg text-ice-300">
News, updates, and insights from the Fabro team.
</p>
</div>
</section>
<!-- Featured post -->
{featured && (
<section class="relative pb-8">
<div class="mx-auto max-w-3xl px-6">
<a href={`/blog/${featured.id}`} class="reveal reveal-d2 group block">
<article class="rounded-xl border border-teal-700/30 bg-gradient-to-br from-navy-800/40 to-navy-900/20 p-8 relative overflow-hidden transition-all duration-400 hover:border-teal-700/50 hover:shadow-[0_0_40px_-8px_rgba(53,127,158,0.15)]">
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/40 to-transparent" />
<time class="text-xs font-mono text-teal-500/70">{formatDate(featured.data.date)}</time>
<h2 class="mt-3 font-display text-2xl font-bold text-ice-50 group-hover:text-teal-300 transition-colors sm:text-3xl">
{featured.data.title}
</h2>
<p class="mt-3 text-base leading-relaxed text-ice-300">{featured.data.description}</p>
<div class="mt-5 flex items-center gap-3">
<span class="text-sm text-ice-300/50">By {featured.data.author}</span>
<span class="text-ice-300/30">&middot;</span>
<span class="text-sm font-medium text-teal-500 group-hover:text-teal-300 transition-colors">
Read &rarr;
</span>
</div>
</article>
</a>
</div>
</section>
)}
<!-- Older posts -->
{older.length > 0 && (
<section class="relative pb-24">
<div class="mx-auto max-w-3xl px-6 space-y-1">
{older.map((post, i) => (
<a href={`/blog/${post.id}`} class={`reveal reveal-d${Math.min(i + 3, 5)} group flex items-baseline gap-4 py-4 border-b border-navy-800/40 transition-colors hover:border-teal-700/30`}>
<time class="shrink-0 w-28 text-xs font-mono text-ice-300/40">{formatDate(post.data.date)}</time>
<span class="font-display text-base font-semibold text-ice-50 group-hover:text-teal-300 transition-colors">{post.data.title}</span>
<span class="hidden sm:inline text-sm text-ice-300/50">&mdash; {post.data.description}</span>
</a>
))}
</div>
</section>
)}
{older.length === 0 && <div class="pb-24" />}
</main>
<Footer />
<PageScripts />
</Layout>

View file

@ -51,6 +51,7 @@ const cssExample = `<span class="text-ice-300">/* All nodes default to fast + ch
<img src="/logotype.svg" alt="Fabro" class="h-7" />
</a>
<a href="https://docs.fabro.sh" class="hidden md:inline-flex text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Docs</a>
<a href="/blog" class="hidden md:inline-flex text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Blog</a>
<!-- <a href="/roadmap" class="hidden md:inline-flex text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Roadmap</a> -->
<a href="https://docs.fabro.sh/changelog" class="hidden md:inline-flex text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Changelog</a>
</div>
@ -79,6 +80,7 @@ const cssExample = `<span class="text-ice-300">/* All nodes default to fast + ch
<div id="mobile-menu" class="md:hidden hidden border-t border-navy-800/60 bg-navy-950/95 backdrop-blur-xl">
<div class="px-6 py-5 space-y-4">
<a href="https://docs.fabro.sh" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Docs</a>
<a href="/blog" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Blog</a>
<!-- <a href="/roadmap" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Roadmap</a> -->
<a href="https://docs.fabro.sh/changelog" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Changelog</a>
<hr class="border-navy-800/60" />
@ -681,6 +683,7 @@ const cssExample = `<span class="text-ice-300">/* All nodes default to fast + ch
<ul class="space-y-2 text-sm">
<li><a href="https://docs.fabro.sh/getting-started/quick-start" class="text-ice-300 hover:text-ice-50 transition-colors">Quick Start</a></li>
<li><a href="https://docs.fabro.sh" class="text-ice-300 hover:text-ice-50 transition-colors">Docs</a></li>
<li><a href="/blog" class="text-ice-300 hover:text-ice-50 transition-colors">Blog</a></li>
<li><a href="/roadmap" class="text-ice-300 hover:text-ice-50 transition-colors">Roadmap</a></li>
<li><a href="https://docs.fabro.sh/changelog" class="text-ice-300 hover:text-ice-50 transition-colors">Changelog</a></li>
<li><a href="https://github.com/fabro-sh/fabro/releases" class="text-ice-300 hover:text-ice-50 transition-colors">Releases</a></li>

View file

@ -1,5 +1,8 @@
---
import Layout from "../layouts/Layout.astro";
import Nav from "../components/Nav.astro";
import Footer from "../components/Footer.astro";
import PageScripts from "../components/PageScripts.astro";
import { getCollection } from "astro:content";
const allItems = await getCollection("roadmap");
@ -13,108 +16,50 @@ const SHIPPED_VISIBLE = 3;
const shippedVisible = shipped.slice(0, SHIPPED_VISIBLE);
const shippedHidden = shipped.slice(SHIPPED_VISIBLE);
const sections = [
{ label: "Building", status: "building" as const, items: byStatus("building") },
{ label: "Next", status: "next" as const, items: byStatus("next") },
];
const building = byStatus("building");
const next = byStatus("next");
const statusConfig = {
building: {
dot: "bg-amber",
border: "via-amber/30",
label: "text-amber",
track: "bg-amber/20",
track: "bg-amber/30",
cardBorder: "border-amber/15",
cardBg: "bg-amber/5",
},
next: {
dot: "bg-teal-500",
border: "via-teal-500/30",
label: "text-teal-500",
track: "bg-teal-500/20",
track: "bg-teal-500/30",
cardBorder: "border-teal-500/15",
cardBg: "bg-teal-500/5",
},
};
---
<Layout title="Roadmap — Fabro">
<!-- Nav -->
<nav class="fixed top-0 z-50 w-full border-b border-navy-800/60 bg-navy-950/70 backdrop-blur-xl">
<div class="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<div class="flex items-center gap-8">
<a href="/" class="flex items-center">
<img src="/logotype.svg" alt="Fabro" class="h-7" />
</a>
<a href="https://docs.fabro.sh" class="hidden md:inline-flex text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Docs</a>
<!-- <a href="/roadmap" class="hidden md:inline-flex text-sm font-medium text-ice-50 transition-colors">Roadmap</a> -->
<a href="https://docs.fabro.sh/changelog" class="hidden md:inline-flex text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Changelog</a>
</div>
<div class="flex items-center gap-5">
<a href="/discord" class="hidden md:block text-ice-300 hover:text-ice-50 transition-colors" aria-label="Discord">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
<a href="https://github.com/fabro-sh/fabro" class="hidden md:block text-ice-300 hover:text-ice-50 transition-colors" aria-label="GitHub">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
</a>
<a
href="https://docs.fabro.sh/getting-started/quick-start"
class="hidden md:inline-flex rounded-lg bg-teal-700 px-4 py-2 text-sm font-semibold text-white transition-all hover:bg-teal-500 hover:shadow-[0_0_20px_-4px_rgba(53,127,158,0.5)]"
>
Get started
</a>
<button id="mobile-menu-btn" class="md:hidden text-ice-300 hover:text-ice-50 transition-colors" aria-label="Open menu">
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
</div>
</div>
<div id="mobile-menu" class="md:hidden hidden border-t border-navy-800/60 bg-navy-950/95 backdrop-blur-xl">
<div class="px-6 py-5 space-y-4">
<a href="https://docs.fabro.sh" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Docs</a>
<!-- <a href="/roadmap" class="block text-sm font-medium text-ice-50 transition-colors">Roadmap</a> -->
<a href="https://docs.fabro.sh/changelog" class="block text-sm font-medium text-ice-100 hover:text-ice-50 transition-colors">Changelog</a>
<hr class="border-navy-800/60" />
<div class="flex items-center gap-5">
<a href="/discord" class="text-ice-300 hover:text-ice-50 transition-colors" aria-label="Discord">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
<a href="https://github.com/fabro-sh/fabro" class="text-ice-300 hover:text-ice-50 transition-colors" aria-label="GitHub">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
</a>
</div>
<a
href="https://docs.fabro.sh/getting-started/quick-start"
class="block w-full text-center rounded-lg bg-teal-700 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-teal-500"
>
Get started
</a>
</div>
</div>
</nav>
<Nav currentPage="roadmap" />
<main>
<!-- Hero -->
<section class="relative overflow-hidden pt-28 sm:pt-36 pb-20">
<div class="absolute inset-0 grid-overlay"></div>
<!-- Header -->
<section class="relative overflow-hidden pt-24 sm:pt-28 pb-10">
<div class="absolute inset-0 noise-overlay"></div>
<div class="pointer-events-none absolute top-16 -left-40 h-[500px] w-[500px] rounded-full bg-teal-500/10 blur-[120px] animate-float"></div>
<div class="pointer-events-none absolute -top-10 -right-20 h-[300px] w-[300px] rounded-full bg-mint/8 blur-[100px] animate-float" style="animation-delay: -3s"></div>
<div class="pointer-events-none absolute top-8 -left-40 h-[400px] w-[400px] rounded-full bg-teal-500/8 blur-[120px]"></div>
<div class="relative mx-auto max-w-4xl px-6 text-center">
<div class="reveal">
<p class="mb-5 inline-block rounded-full border border-teal-700/40 bg-teal-700/10 px-4 py-1.5 text-sm font-medium text-teal-300">
Where we're headed
</p>
</div>
<h1 class="reveal reveal-d1 font-display text-5xl font-bold tracking-tight text-ice-50 sm:text-6xl leading-[1.1]">
<div class="relative mx-auto max-w-3xl px-6">
<h1 class="reveal font-display text-4xl font-bold tracking-tight text-ice-50 sm:text-5xl leading-[1.1]">
Roadmap
</h1>
<p class="reveal reveal-d2 mx-auto mt-6 max-w-2xl text-lg leading-relaxed text-ice-300">
<p class="reveal reveal-d1 mt-3 text-lg text-ice-300">
What we've shipped, what we're building, and where Fabro is going next.
</p>
</div>
</section>
<!-- Shipped — compact rows, recent 3 visible -->
<section class="relative pb-16">
<!-- Shipped — compact rows -->
<section class="relative pb-12">
<div class="mx-auto max-w-3xl px-6">
<div class="reveal flex items-center gap-4">
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
@ -181,53 +126,74 @@ const statusConfig = {
</div>
</section>
<!-- Building & Next — expanded timeline -->
<section class="relative pb-32">
<div class="mx-auto max-w-3xl px-6">
{sections.map((section, sectionIdx) => {
const config = statusConfig[section.status];
return (
<div class="relative">
<div class={`reveal flex items-center gap-4 ${sectionIdx > 0 ? "mt-16" : ""}`}>
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
{section.status === "building" && (
<div class="h-3 w-3 rounded-full bg-amber animate-pulse-glow" />
)}
{section.status === "next" && (
<svg class="h-4 w-4 text-teal-500" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<polyline points="12,6 12,12 16,14" />
</svg>
)}
<span class={`font-display text-sm font-semibold uppercase tracking-wider ${config.label}`}>
{section.label}
</span>
</div>
<div class="h-px flex-1 bg-gradient-to-r from-navy-800 to-transparent" />
</div>
<div class="relative mt-8 ml-5">
<div class={`absolute top-0 bottom-0 left-0 w-px ${config.track}`} />
<div class="space-y-0">
{section.items.map((item, itemIdx) => (
<div class={`reveal reveal-d${Math.min(itemIdx + 1, 5)} relative pl-10 pb-8 group`}>
<div class="absolute left-0 top-1.5 -translate-x-1/2 z-10">
<div class={`h-2.5 w-2.5 rounded-full ${config.dot} ring-4 ring-navy-950 transition-all group-hover:scale-125`} />
</div>
<div class="glow-card rounded-xl border border-navy-800 bg-navy-800/20 p-6 relative overflow-hidden">
<div class={`absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent ${config.border} to-transparent`} />
<h3 class="font-display text-base font-semibold text-ice-50">{item.data.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-ice-300">{item.data.description}</p>
</div>
</div>
))}
</div>
</div>
<!-- Building — only render if items exist -->
{building.length > 0 && (
<section class="relative pb-12">
<div class="mx-auto max-w-3xl px-6">
<div class="reveal flex items-center gap-4">
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
<div class="h-3 w-3 rounded-full bg-amber animate-pulse-glow" />
<span class="font-display text-sm font-semibold uppercase tracking-wider text-amber">Building</span>
</div>
);
})}
</div>
</section>
<div class="h-px flex-1 bg-gradient-to-r from-navy-800 to-transparent" />
</div>
<div class="relative mt-8 ml-5">
<div class="absolute top-0 bottom-0 left-0 w-px bg-amber/30" />
<div class="space-y-0">
{building.map((item, i) => (
<div class={`reveal reveal-d${Math.min(i + 1, 5)} relative pl-10 pb-8 group`}>
<div class="absolute left-0 top-1.5 -translate-x-1/2 z-10">
<div class="h-2.5 w-2.5 rounded-full bg-amber ring-4 ring-navy-950 transition-all group-hover:scale-125" />
</div>
<div class="rounded-xl border border-amber/15 bg-amber/5 p-6 relative overflow-hidden transition-all duration-400 hover:border-amber/25 hover:bg-amber/8">
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-amber/30 to-transparent" />
<h3 class="font-display text-base font-semibold text-ice-50">{item.data.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-ice-300">{item.data.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
)}
<!-- Next — only render if items exist -->
{next.length > 0 && (
<section class="relative pb-24">
<div class="mx-auto max-w-3xl px-6">
<div class="reveal flex items-center gap-4">
<div class="flex items-center gap-3 rounded-full border border-navy-800 bg-navy-900/80 px-5 py-2">
<svg class="h-4 w-4 text-teal-500" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<polyline points="12,6 12,12 16,14" />
</svg>
<span class="font-display text-sm font-semibold uppercase tracking-wider text-teal-500">Next</span>
</div>
<div class="h-px flex-1 bg-gradient-to-r from-navy-800 to-transparent" />
</div>
<div class="relative mt-8 ml-5">
<div class="absolute top-0 bottom-0 left-0 w-px bg-teal-500/30" />
<div class="space-y-0">
{next.map((item, i) => (
<div class={`reveal reveal-d${Math.min(i + 1, 5)} relative pl-10 pb-8 group`}>
<div class="absolute left-0 top-1.5 -translate-x-1/2 z-10">
<div class="h-2.5 w-2.5 rounded-full bg-teal-500 ring-4 ring-navy-950 transition-all group-hover:scale-125" />
</div>
<div class="rounded-xl border border-teal-500/15 bg-teal-500/5 p-6 relative overflow-hidden transition-all duration-400 hover:border-teal-500/25 hover:bg-teal-500/8">
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-teal-500/30 to-transparent" />
<h3 class="font-display text-base font-semibold text-ice-50">{item.data.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-ice-300">{item.data.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
)}
<!-- CTA -->
<section class="relative overflow-hidden border-t border-navy-800 py-24">
@ -258,68 +224,10 @@ const statusConfig = {
</section>
</main>
<!-- Footer -->
<footer class="border-t border-navy-800/60 py-16">
<div class="mx-auto max-w-6xl px-6">
<div class="grid gap-10 sm:grid-cols-3">
<div>
<img src="/logotype.svg" alt="Fabro" class="h-6 mb-4" />
<p class="text-sm text-ice-300/60">The dark software factory for expert engineers.</p>
<p class="mt-2 text-xs text-ice-300/40">Open source, MIT-licensed</p>
</div>
<div>
<h3 class="font-display text-sm font-semibold uppercase tracking-wider text-ice-300/70 mb-4">Product</h3>
<ul class="space-y-2 text-sm">
<li><a href="https://docs.fabro.sh/getting-started/quick-start" class="text-ice-300 hover:text-ice-50 transition-colors">Quick Start</a></li>
<li><a href="https://docs.fabro.sh" class="text-ice-300 hover:text-ice-50 transition-colors">Docs</a></li>
<li><a href="/roadmap" class="text-ice-300 hover:text-ice-50 transition-colors">Roadmap</a></li>
<li><a href="https://docs.fabro.sh/changelog" class="text-ice-300 hover:text-ice-50 transition-colors">Changelog</a></li>
<li><a href="https://github.com/fabro-sh/fabro/releases" class="text-ice-300 hover:text-ice-50 transition-colors">Releases</a></li>
</ul>
</div>
<div>
<h3 class="font-display text-sm font-semibold uppercase tracking-wider text-ice-300/70 mb-4">Community</h3>
<ul class="space-y-2 text-sm">
<li><a href="/discord" class="text-ice-300 hover:text-ice-50 transition-colors">Discord</a></li>
<li><a href="https://github.com/fabro-sh/fabro" class="text-ice-300 hover:text-ice-50 transition-colors">GitHub</a></li>
<li><a href="https://github.com/fabro-sh/fabro/issues" class="text-ice-300 hover:text-ice-50 transition-colors">Issues</a></li>
<li><a href="https://github.com/fabro-sh/fabro/discussions" class="text-ice-300 hover:text-ice-50 transition-colors">Discussions</a></li>
</ul>
</div>
</div>
<div class="mt-10 border-t border-navy-800/40 pt-6 text-center text-xs text-ice-300/40">
&copy; 2025 Qlty Software Inc. All rights reserved.
</div>
</div>
</footer>
<Footer />
<PageScripts />
<script>
// Mobile menu toggle
const mobileMenuBtn = document.getElementById("mobile-menu-btn")!;
const mobileMenu = document.getElementById("mobile-menu")!;
mobileMenuBtn.addEventListener("click", () => {
const isOpen = !mobileMenu.classList.contains("hidden");
mobileMenu.classList.toggle("hidden");
mobileMenuBtn.setAttribute("aria-label", isOpen ? "Open menu" : "Close menu");
mobileMenuBtn.innerHTML = isOpen
? '<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>'
: '<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>';
});
// Scroll reveal
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("revealed");
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.15, rootMargin: "0px 0px -40px 0px" }
);
document.querySelectorAll(".reveal, .reveal-left, .reveal-right, .reveal-scale").forEach((el) => observer.observe(el));
// Shipped older items toggle
const toggle = document.getElementById("shipped-toggle");
const older = document.getElementById("shipped-older");

View file

@ -210,3 +210,66 @@
.install-panel.active {
display: block;
}
/* Blog prose styles */
.prose {
color: var(--color-ice-300);
line-height: 1.75;
}
.prose h1, .prose h2, .prose h3, .prose h4 {
color: var(--color-ice-50);
font-family: var(--font-display);
font-weight: 700;
margin-top: 2em;
margin-bottom: 0.75em;
}
.prose h2 { font-size: 1.5rem; }
.prose h3 { font-size: 1.25rem; }
.prose p { margin-bottom: 1.25em; }
.prose a {
color: var(--color-teal-300);
text-decoration: underline;
text-underline-offset: 2px;
transition: color 0.2s;
}
.prose a:hover { color: var(--color-teal-500); }
.prose ul, .prose ol { padding-left: 1.5em; margin-bottom: 1.25em; }
.prose li { margin-bottom: 0.5em; }
.prose ul { list-style-type: disc; }
.prose ol { list-style-type: decimal; }
.prose blockquote {
border-left: 3px solid var(--color-teal-700);
padding-left: 1em;
color: var(--color-ice-100);
font-style: italic;
margin: 1.5em 0;
}
.prose code {
font-family: var(--font-mono);
font-size: 0.875em;
background: var(--color-navy-800);
padding: 0.15em 0.4em;
border-radius: 0.25rem;
color: var(--color-teal-300);
}
.prose pre {
background: var(--color-navy-900);
border: 1px solid var(--color-navy-800);
border-radius: 0.75rem;
padding: 1.25em;
overflow-x: auto;
margin: 1.5em 0;
}
.prose pre code {
background: none;
padding: 0;
border-radius: 0;
}
.prose img {
border-radius: 0.75rem;
margin: 1.5em 0;
}
.prose hr {
border-color: var(--color-navy-800);
margin: 2em 0;
}