Radio show website: Full Astro build with 194 episodes imported

Complete website for The Computer Guru Show (radio.azcomputerguru.com):
- Astro 6.0.4 static site with React islands
- 194 episodes imported from gurushow.com RSS feed
- Dark/light mode HSL design system
- Persistent audio player with session persistence
- Episode archive with search and season filtering
- Home page with animated hero, stats, latest episodes
- All pages: About, Subscribe, Community, Live, Contact, Blog, 404
- Podcast RSS feed with iTunes namespace
- Session log updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 20:44:42 -07:00
parent 24ab7a69b6
commit 1b45921493
236 changed files with 16513 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
---
---
<section class="section about-preview fade-in">
<div class="container">
<div class="about-grid">
<div class="about-text">
<span class="badge">About the Show</span>
<h2 class="about-title">Meet Your Host</h2>
<p class="about-lead">
Mike Swanson has been breaking down technology for everyday people since 2014.
As a Tucson-based tech professional and broadcaster, he brings decades of
hands-on experience to every episode.
</p>
<blockquote class="about-quote">
"Technology should empower you, not intimidate you. That is what this show is all about."
</blockquote>
<p class="about-body">
From cybersecurity and privacy to the latest gadgets and internet culture,
The Computer Guru Show covers it all -- with a healthy dose of humor, real-world
advice, and honest opinions. No corporate sponsors telling us what to say.
Just straight talk about tech.
</p>
<a href="/about" class="btn btn--primary">
Learn More
<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">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</a>
</div>
<div class="about-visual">
<div class="about-image-placeholder">
<div class="about-image-inner">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
<span class="about-image-text">The Computer Guru</span>
</div>
</div>
<div class="about-visual-accent"></div>
</div>
</div>
</div>
</section>
<style>
.about-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-12);
align-items: center;
}
.about-text {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.about-title {
font-size: var(--text-3xl);
font-weight: 800;
}
.about-lead {
font-size: var(--text-lg);
color: var(--color-text-secondary);
line-height: 1.7;
}
.about-quote {
font-size: var(--text-lg);
font-style: italic;
color: var(--color-accent);
padding-left: var(--space-6);
border-left: 3px solid var(--color-accent);
margin-block: var(--space-2);
line-height: 1.6;
}
.about-body {
font-size: var(--text-base);
color: var(--color-text-secondary);
line-height: 1.7;
}
.about-text .btn {
align-self: flex-start;
margin-top: var(--space-4);
}
/* Visual / placeholder area */
.about-visual {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.about-image-placeholder {
width: 100%;
aspect-ratio: 4 / 5;
max-width: 400px;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
overflow: hidden;
}
.about-image-placeholder::before {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(135deg, hsl(200 85% 55% / 0.05), transparent 50%),
linear-gradient(315deg, hsl(270 60% 50% / 0.03), transparent 50%);
}
.about-image-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
color: var(--color-text-muted);
position: relative;
z-index: 1;
}
.about-image-text {
font-size: var(--text-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.about-visual-accent {
position: absolute;
width: 120%;
height: 120%;
top: -10%;
left: -10%;
border: 1px solid hsl(200 85% 55% / 0.06);
border-radius: var(--radius-lg);
transform: rotate(3deg);
z-index: 0;
}
@media (max-width: 768px) {
.about-grid {
grid-template-columns: 1fr;
gap: var(--space-8);
}
.about-visual {
order: -1;
}
.about-image-placeholder {
max-width: 280px;
margin-inline: auto;
aspect-ratio: 1;
}
}
</style>

View File

@@ -0,0 +1,185 @@
---
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog');
const publishedPosts = allPosts
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
.slice(0, 3);
const isSingle = publishedPosts.length === 1;
---
{publishedPosts.length > 0 && (
<section class="section blog-highlights fade-in">
<div class="container">
<div class="section-header">
<h2 class="section__title">From the Blog</h2>
<a href="/blog" class="section-header__link">
All Posts
<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">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
</div>
<div class:list={['blog-grid', { 'blog-grid--single': isSingle }]}>
{publishedPosts.map((post) => {
const formattedDate = new Date(post.data.pubDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
return (
<article class="blog-card card">
<div class="blog-card__meta">
<time datetime={post.data.pubDate.toISOString()}>{formattedDate}</time>
{post.data.tags.length > 0 && (
<div class="blog-card__tags">
{post.data.tags.slice(0, 2).map((tag: string) => (
<span class="blog-card__tag">{tag}</span>
))}
</div>
)}
</div>
<a href={`/blog/${post.id}`} class="blog-card__title-link">
<h3 class="blog-card__title">{post.data.title}</h3>
</a>
<p class="blog-card__description">{post.data.description}</p>
<div class="blog-card__footer">
<span class="blog-card__author">By {post.data.author}</span>
<a href={`/blog/${post.id}`} class="blog-card__read-more">
Read More
<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">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</a>
</div>
</article>
);
})}
</div>
</div>
</section>
)}
<style>
.section-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-4);
flex-wrap: wrap;
}
.section-header__link {
display: inline-flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-accent);
transition: gap var(--transition-fast);
}
.section-header__link:hover {
gap: var(--space-2);
}
.blog-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--space-6);
}
.blog-grid--single {
max-width: 600px;
margin-inline: auto;
}
.blog-card {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.blog-card__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
flex-wrap: wrap;
}
.blog-card__meta time {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.blog-card__tags {
display: flex;
gap: var(--space-1);
}
.blog-card__tag {
font-size: 0.65rem;
padding: 2px var(--space-2);
border-radius: 9999px;
background: var(--color-accent-glow);
color: var(--color-accent);
}
.blog-card__title-link {
color: inherit;
text-decoration: none;
}
.blog-card__title-link:hover {
color: var(--color-accent);
}
.blog-card__title {
font-size: var(--text-xl);
font-weight: 700;
line-height: 1.3;
}
.blog-card__description {
font-size: var(--text-sm);
color: var(--color-text-secondary);
line-height: 1.6;
}
.blog-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
padding-top: var(--space-3);
border-top: 1px solid var(--color-border);
}
.blog-card__author {
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.blog-card__read-more {
display: inline-flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-accent);
transition: gap var(--transition-fast);
}
.blog-card__read-more:hover {
gap: var(--space-2);
}
</style>

View File

@@ -0,0 +1,219 @@
---
import { getCollection } from 'astro:content';
const allEpisodes = await getCollection('episodes');
const classicEpisodes = allEpisodes.filter((ep) => ep.data.classic);
// If no episodes marked as classic, pick a curated selection:
// the first episode of each season for variety
const fallbackEpisodes = (() => {
const seasonFirsts = new Map<number, typeof allEpisodes[0]>();
for (const ep of allEpisodes) {
const s = ep.data.season;
if (s === 0) continue;
if (!seasonFirsts.has(s) || ep.data.episode < seasonFirsts.get(s)!.data.episode) {
seasonFirsts.set(s, ep);
}
}
return Array.from(seasonFirsts.values())
.sort((a, b) => b.data.season - a.data.season)
.slice(0, 5);
})();
const displayEpisodes = classicEpisodes.length > 0 ? classicEpisodes : fallbackEpisodes;
const hasClassics = classicEpisodes.length > 0;
---
{displayEpisodes.length > 0 && (
<section class="section classics fade-in">
<div class="container">
<div class="section-header">
<h2 class="section__title">
{hasClassics ? 'Fan Favorites' : 'From the Archive'}
</h2>
<a href="/episodes" class="section-header__link">
Browse All Episodes
<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">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
</div>
<div class="classics-scroll">
<div class="classics-track">
{displayEpisodes.map((episode) => {
const { title, season, episode: episodeNum, pubDate, duration } = episode.data;
const seasonLabel = season === 0 ? 'Special' : `S${season}`;
const episodeLabel = `${seasonLabel} E${String(episodeNum).padStart(2, '0')}`;
const formattedDate = new Date(pubDate).toLocaleDateString('en-US', {
month: 'short',
year: 'numeric',
});
const description = episode.body
? episode.body.replace(/\\?\*\\?\*\\?\*/g, '').replace(/\\\[.*?\\\]/g, '').replace(/\[.*?\]/g, '').trim()
: '';
return (
<a href={`/episodes/${episode.id}`} class="classic-card">
{hasClassics && <span class="classic-card__badge">Classic</span>}
{!hasClassics && <span class="classic-card__badge classic-card__badge--season">Season {season}</span>}
<div class="classic-card__meta">
<span class="badge">{episodeLabel}</span>
<span class="classic-card__duration">{duration}</span>
</div>
<h3 class="classic-card__title">{title}</h3>
{description && (
<p class="classic-card__desc">{description.slice(0, 100)}{description.length > 100 ? '...' : ''}</p>
)}
<time class="classic-card__date" datetime={pubDate.toISOString()}>{formattedDate}</time>
</a>
);
})}
</div>
</div>
</div>
</section>
)}
<style>
.section-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-4);
flex-wrap: wrap;
}
.section-header__link {
display: inline-flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-accent);
transition: gap var(--transition-fast);
}
.section-header__link:hover {
gap: var(--space-2);
}
.classics-scroll {
overflow-x: auto;
margin-inline: calc(-1 * var(--space-6));
padding-inline: var(--space-6);
scrollbar-width: thin;
scrollbar-color: var(--color-border) transparent;
-webkit-overflow-scrolling: touch;
}
.classics-scroll::-webkit-scrollbar {
height: 6px;
}
.classics-scroll::-webkit-scrollbar-track {
background: transparent;
}
.classics-scroll::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
.classics-track {
display: flex;
gap: var(--space-6);
padding-bottom: var(--space-4);
}
.classic-card {
flex: 0 0 280px;
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-6);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
text-decoration: none;
color: inherit;
transition:
transform var(--transition-base),
box-shadow var(--transition-base),
border-color var(--transition-base);
}
.classic-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--color-accent);
color: inherit;
}
.classic-card__badge {
align-self: flex-start;
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 2px var(--space-2);
border-radius: var(--radius-sm);
background: hsl(40 90% 55% / 0.15);
color: var(--color-warning);
border: 1px solid hsl(40 90% 55% / 0.25);
}
.classic-card__badge--season {
background: var(--color-accent-glow);
color: var(--color-accent);
border-color: hsl(200 85% 55% / 0.25);
}
.classic-card__meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: var(--space-1);
}
.classic-card__duration {
font-size: var(--text-xs);
color: var(--color-text-muted);
font-family: var(--font-mono);
}
.classic-card__title {
font-size: var(--text-base);
font-weight: 600;
line-height: 1.3;
color: var(--color-text-primary);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.classic-card__desc {
font-size: var(--text-xs);
color: var(--color-text-secondary);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.classic-card__date {
font-size: var(--text-xs);
color: var(--color-text-muted);
margin-top: auto;
}
@media (max-width: 480px) {
.classic-card {
flex: 0 0 240px;
}
}
</style>

View File

@@ -0,0 +1,295 @@
---
---
<section class="hero">
<div class="hero-bg-effects">
<div class="hero-glow hero-glow--primary"></div>
<div class="hero-glow hero-glow--secondary"></div>
<div class="hero-ring"></div>
</div>
<div class="hero-content container">
<span class="badge hero-badge">Returning 2026</span>
<h1 class="hero-title">
<span class="hero-title__line hero-title__line--1">The Computer</span>
<span class="hero-title__line hero-title__line--2">Guru Show</span>
</h1>
<p class="hero-tagline">Technology: Fun and Simple</p>
<p class="hero-description">
Your source for making sense of the tech world without the jargon.
Hosted by Mike Swanson from Tucson, Arizona -- cutting through the noise
so you can enjoy technology the way it was meant to be.
</p>
<div class="hero-actions">
<a href="/episodes" class="btn btn--primary btn--lg">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polygon points="10 8 16 12 10 16 10 8" fill="currentColor" stroke="none"></polygon>
</svg>
Browse Episodes
</a>
<a href="/subscribe" class="btn btn--ghost btn--lg">Subscribe</a>
</div>
</div>
<div class="hero-scroll-hint" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="7 13 12 18 17 13"></polyline>
<polyline points="7 6 12 11 17 6"></polyline>
</svg>
</div>
</section>
<style>
.hero {
min-height: calc(100vh - 64px);
display: flex;
align-items: center;
position: relative;
overflow: hidden;
background:
linear-gradient(135deg, hsl(220 30% 6% / 0.95), hsl(200 40% 8% / 0.9)),
radial-gradient(ellipse at 20% 50%, hsl(200 85% 55% / 0.12), transparent 50%),
radial-gradient(ellipse at 80% 20%, hsl(270 60% 50% / 0.08), transparent 50%);
}
[data-theme="light"] .hero {
background:
linear-gradient(135deg, hsl(220 20% 95% / 0.95), hsl(200 30% 92% / 0.9)),
radial-gradient(ellipse at 20% 50%, hsl(200 85% 55% / 0.08), transparent 50%),
radial-gradient(ellipse at 80% 20%, hsl(270 60% 50% / 0.05), transparent 50%);
}
/* Background effects */
.hero-bg-effects {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
}
.hero-glow {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
}
.hero-glow--primary {
width: 600px;
height: 600px;
top: -200px;
right: -100px;
background: radial-gradient(circle, hsl(200 85% 55% / 0.15), transparent 70%);
animation: glowDrift 8s ease-in-out infinite alternate;
}
.hero-glow--secondary {
width: 400px;
height: 400px;
bottom: -100px;
left: -50px;
background: radial-gradient(circle, hsl(270 60% 50% / 0.1), transparent 70%);
animation: glowDrift 10s ease-in-out infinite alternate-reverse;
}
.hero-ring {
position: absolute;
width: 500px;
height: 500px;
top: 50%;
right: 5%;
transform: translate(0, -50%);
border: 1px solid hsl(200 85% 55% / 0.08);
border-radius: 50%;
animation: ringPulse 4s ease-in-out infinite;
}
.hero-ring::before {
content: '';
position: absolute;
inset: 40px;
border: 1px solid hsl(200 85% 55% / 0.06);
border-radius: 50%;
animation: ringPulse 4s ease-in-out infinite 0.5s;
}
.hero-ring::after {
content: '';
position: absolute;
inset: 80px;
border: 1px solid hsl(200 85% 55% / 0.04);
border-radius: 50%;
animation: ringPulse 4s ease-in-out infinite 1s;
}
[data-theme="light"] .hero-glow--primary {
background: radial-gradient(circle, hsl(200 85% 55% / 0.08), transparent 70%);
}
[data-theme="light"] .hero-glow--secondary {
background: radial-gradient(circle, hsl(270 60% 50% / 0.06), transparent 70%);
}
[data-theme="light"] .hero-ring {
border-color: hsl(200 85% 55% / 0.12);
}
[data-theme="light"] .hero-ring::before {
border-color: hsl(200 85% 55% / 0.08);
}
[data-theme="light"] .hero-ring::after {
border-color: hsl(200 85% 55% / 0.05);
}
/* Content */
.hero-content {
position: relative;
z-index: 1;
max-width: 720px;
}
.hero-badge {
animation: fadeSlideIn 0.6s ease-out both;
}
.hero-title {
margin-top: var(--space-6);
display: flex;
flex-direction: column;
gap: 0;
}
.hero-title__line {
display: block;
font-size: clamp(2.5rem, 6vw, 5rem);
font-weight: 800;
line-height: 1.05;
background: linear-gradient(135deg, var(--color-text-primary) 30%, var(--color-accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-title__line--1 {
animation: fadeSlideIn 0.6s ease-out 0.15s both;
}
.hero-title__line--2 {
animation: fadeSlideIn 0.6s ease-out 0.3s both;
background: linear-gradient(135deg, var(--color-accent) 0%, hsl(220 70% 65%) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-tagline {
font-size: var(--text-xl);
color: var(--color-accent);
font-weight: 600;
margin-top: var(--space-4);
animation: fadeSlideIn 0.6s ease-out 0.45s both;
letter-spacing: 0.02em;
}
.hero-description {
font-size: var(--text-lg);
color: var(--color-text-secondary);
margin-top: var(--space-4);
line-height: 1.7;
max-width: 560px;
animation: fadeSlideIn 0.6s ease-out 0.6s both;
}
.hero-actions {
display: flex;
gap: var(--space-4);
margin-top: var(--space-8);
flex-wrap: wrap;
animation: fadeSlideIn 0.6s ease-out 0.75s both;
}
.btn--lg {
padding: var(--space-4) var(--space-8);
font-size: var(--text-base);
}
/* Scroll hint */
.hero-scroll-hint {
position: absolute;
bottom: var(--space-8);
left: 50%;
transform: translateX(-50%);
color: var(--color-text-muted);
animation: bounceDown 2s ease-in-out infinite;
z-index: 1;
}
/* Keyframes */
@keyframes fadeSlideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes glowDrift {
from {
transform: translate(0, 0) scale(1);
}
to {
transform: translate(30px, -20px) scale(1.1);
}
}
@keyframes ringPulse {
0%, 100% {
opacity: 0.3;
transform: translate(0, -50%) scale(1);
}
50% {
opacity: 0.6;
transform: translate(0, -50%) scale(1.05);
}
}
@keyframes bounceDown {
0%, 100% {
transform: translateX(-50%) translateY(0);
opacity: 0.4;
}
50% {
transform: translateX(-50%) translateY(8px);
opacity: 0.8;
}
}
@media (max-width: 768px) {
.hero {
min-height: auto;
padding-block: var(--space-16) var(--space-24);
}
.hero-ring {
width: 300px;
height: 300px;
right: -80px;
opacity: 0.5;
}
.hero-scroll-hint {
display: none;
}
}
@media (max-width: 480px) {
.hero-title__line {
font-size: clamp(2rem, 10vw, 3rem);
}
}
</style>

View File

@@ -0,0 +1,69 @@
---
import { getCollection } from 'astro:content';
import EpisodeCard from '../episodes/EpisodeCard.astro';
const allEpisodes = await getCollection('episodes');
const latestEpisodes = allEpisodes
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
.slice(0, 6);
---
<section class="section latest-episodes fade-in">
<div class="container">
<div class="section-header">
<h2 class="section__title">Latest Episodes</h2>
<a href="/episodes" class="section-header__link">
View All
<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">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
</div>
{latestEpisodes.length > 0 ? (
<div class="grid grid--3">
{latestEpisodes.map((episode) => (
<EpisodeCard episode={episode} />
))}
</div>
) : (
<div class="empty-state">
<p>New episodes coming soon. Stay tuned for the return of The Computer Guru Show.</p>
</div>
)}
</div>
</section>
<style>
.section-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-4);
flex-wrap: wrap;
}
.section-header__link {
display: inline-flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-accent);
transition: gap var(--transition-fast);
}
.section-header__link:hover {
gap: var(--space-2);
}
.empty-state {
text-align: center;
padding: var(--space-16) var(--space-8);
color: var(--color-text-secondary);
font-size: var(--text-lg);
background: var(--color-bg-secondary);
border: 1px dashed var(--color-border);
border-radius: var(--radius-md);
}
</style>

View File

@@ -0,0 +1,129 @@
---
import { getCollection } from 'astro:content';
const allEpisodes = await getCollection('episodes');
const totalEpisodes = allEpisodes.length;
const seasons = new Set(allEpisodes.map((ep) => ep.data.season).filter((s) => s > 0));
const totalSeasons = seasons.size;
const stats = [
{ value: `${totalEpisodes}+`, label: 'Episodes', sublabel: 'and counting' },
{ value: String(totalSeasons), label: 'Seasons', sublabel: 'on the air' },
{ value: '2014', label: 'Since', sublabel: 'year one' },
{ value: 'Tucson', label: 'Arizona', sublabel: 'home base' },
];
---
<section class="section stats fade-in">
<div class="container">
<div class="stats-grid">
{stats.map((stat) => (
<div class="stat-card">
<span class="stat-card__value">{stat.value}</span>
<span class="stat-card__label">{stat.label}</span>
<span class="stat-card__sublabel">{stat.sublabel}</span>
</div>
))}
</div>
</div>
</section>
<style>
.stats {
position: relative;
}
.stats::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 50% 50%, hsl(200 85% 55% / 0.04), transparent 70%);
pointer-events: none;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-6);
}
.stat-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: var(--space-8) var(--space-6);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
position: relative;
overflow: hidden;
transition:
transform var(--transition-base),
box-shadow var(--transition-base),
border-color var(--transition-base);
}
.stat-card::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2px;
background: var(--color-accent);
transition: width var(--transition-base);
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow:
var(--shadow-md),
0 0 30px hsl(200 85% 55% / 0.08);
border-color: hsl(200 85% 55% / 0.3);
}
.stat-card:hover::after {
width: 60%;
}
.stat-card__value {
font-size: clamp(var(--text-2xl), 4vw, var(--text-4xl));
font-weight: 800;
line-height: 1;
background: linear-gradient(135deg, var(--color-text-primary), var(--color-accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-card__label {
font-size: var(--text-lg);
font-weight: 600;
color: var(--color-text-primary);
margin-top: var(--space-2);
}
.stat-card__sublabel {
font-size: var(--text-xs);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-top: var(--space-1);
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 400px) {
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,153 @@
---
import { platforms } from '../../data/platforms';
const platformIcons: Record<string, string> = {
apple: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
spotify: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>`,
google: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.372 0 0 5.373 0 12s5.372 12 12 12c6.627 0 12-5.373 12-12S18.627 0 12 0zm-.5 4.5h1v3.258l2.82-1.628.5.866-2.82 1.628 2.82 1.628-.5.866L12.5 9.49V12.5h-1V9.49l-2.82 1.628-.5-.866 2.82-1.628-2.82-1.628.5-.866L11.5 7.758V4.5z"/></svg>`,
overcast: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3"/><line x1="12" y1="5" x2="12" y2="7" stroke="currentColor" stroke-width="2"/><line x1="12" y1="17" x2="12" y2="19" stroke="currentColor" stroke-width="2"/><line x1="5" y1="12" x2="7" y2="12" stroke="currentColor" stroke-width="2"/><line x1="17" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2"/></svg>`,
pocketcasts: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.372 0 0 5.372 0 12s5.372 12 12 12 12-5.372 12-12S18.628 0 12 0zm0 3.6c4.636 0 8.4 3.764 8.4 8.4h-2.4c0-3.312-2.688-6-6-6s-6 2.688-6 6 2.688 6 6 6v2.4c-4.636 0-8.4-3.764-8.4-8.4S7.364 3.6 12 3.6zm0 4.8a3.6 3.6 0 110 7.2 3.6 3.6 0 010-7.2z"/></svg>`,
rss: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1" fill="currentColor"></circle></svg>`,
};
---
<section class="subscribe-cta fade-in">
<div class="subscribe-cta__bg"></div>
<div class="container subscribe-cta__content">
<h2 class="subscribe-cta__title">Never Miss an Episode</h2>
<p class="subscribe-cta__subtitle">
Subscribe on your favorite platform and get notified when new episodes drop.
</p>
<div class="subscribe-cta__platforms">
{platforms.map((platform) => (
<a
href={platform.url}
class="subscribe-cta__platform"
target={platform.url.startsWith('http') ? '_blank' : undefined}
rel={platform.url.startsWith('http') ? 'noopener noreferrer' : undefined}
aria-label={`Subscribe on ${platform.name}`}
>
<span class="subscribe-cta__icon" set:html={platformIcons[platform.icon] || platformIcons.rss} />
<span class="subscribe-cta__platform-name">{platform.name}</span>
</a>
))}
</div>
</div>
</section>
<style>
.subscribe-cta {
position: relative;
overflow: hidden;
padding-block: var(--space-16);
}
.subscribe-cta__bg {
position: absolute;
inset: 0;
background:
linear-gradient(135deg, hsl(200 85% 55% / 0.08), hsl(270 60% 50% / 0.05)),
linear-gradient(to bottom, var(--color-bg-secondary), var(--color-bg-primary));
z-index: 0;
}
.subscribe-cta__bg::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(to right, transparent, var(--color-accent), transparent);
opacity: 0.3;
}
.subscribe-cta__bg::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(to right, transparent, var(--color-accent), transparent);
opacity: 0.3;
}
.subscribe-cta__content {
position: relative;
z-index: 1;
text-align: center;
}
.subscribe-cta__title {
font-size: var(--text-3xl);
font-weight: 800;
background: linear-gradient(135deg, var(--color-text-primary), var(--color-accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subscribe-cta__subtitle {
font-size: var(--text-lg);
color: var(--color-text-secondary);
margin-top: var(--space-3);
max-width: 500px;
margin-inline: auto;
}
.subscribe-cta__platforms {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--space-4);
margin-top: var(--space-8);
}
.subscribe-cta__platform {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-6);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--color-text-primary);
font-size: var(--text-sm);
font-weight: 500;
text-decoration: none;
transition:
transform var(--transition-fast),
border-color var(--transition-fast),
box-shadow var(--transition-fast),
color var(--transition-fast);
}
.subscribe-cta__platform:hover {
transform: translateY(-2px);
border-color: var(--color-accent);
box-shadow: var(--shadow-glow);
color: var(--color-accent);
}
.subscribe-cta__icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
}
@media (max-width: 640px) {
.subscribe-cta__platforms {
flex-direction: column;
align-items: center;
}
.subscribe-cta__platform {
width: 100%;
max-width: 280px;
justify-content: center;
}
}
</style>