feat: refactor blog pages to match landing CSS aesthetic
- BlogListPage: removed Tailwind/lucide-react, added featured post card, 2-col grid for remaining posts, formatDate helper, eyebrow + Lora titles - BlogDetailPage: matches DocDetailPage (skeleton loader, not-found state, tags + Lora h1 + date/read-time meta, docs-prose body, CTA box) - Added blog-specific CSS to landing.css (.blog-page, .blog-featured, .blog-grid, .blog-card); reuses .docs-back-link, .docs-prose, .docs-cta-box Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,36 +1,78 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { ArrowLeft, Calendar } from 'lucide-react';
|
||||
import SEOHead from '../components/seo/SEOHead';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { loadContent, type ContentItem } from '../lib/content';
|
||||
|
||||
function estimateReadTime(html: string): number {
|
||||
const text = html.replace(/<[^>]+>/g, '');
|
||||
const words = text.split(/\s+/).filter(Boolean).length;
|
||||
return Math.max(2, Math.round(words / 200));
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string, lang: string): string {
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return dateStr;
|
||||
return d.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export default function BlogDetailPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { language } = useLanguage();
|
||||
const [content, setContent] = useState<ContentItem | null>(null);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setContent(null);
|
||||
setNotFound(false);
|
||||
if (slug) {
|
||||
loadContent('blog', language, slug)
|
||||
.then(setContent)
|
||||
.catch(() => setContent(null));
|
||||
.catch(() => setNotFound(true));
|
||||
}
|
||||
}, [slug, language]);
|
||||
|
||||
if (!content) {
|
||||
const zh = language === 'zh';
|
||||
|
||||
if (notFound) {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto py-20 px-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-4 bg-cream-200 rounded w-24" />
|
||||
<div className="h-8 bg-cream-200 rounded w-3/4" />
|
||||
<div className="h-4 bg-cream-200 rounded w-full" />
|
||||
<div className="h-4 bg-cream-200 rounded w-5/6" />
|
||||
<div className="docs-detail">
|
||||
<Link to="/blog" className="docs-back-link">
|
||||
<svg viewBox="0 0 24 24"><path d="M15 18l-6-6 6-6" /></svg>
|
||||
{zh ? '所有文章' : 'All posts'}
|
||||
</Link>
|
||||
<div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-muted)' }}>
|
||||
<p style={{ fontSize: '18px', marginBottom: '8px' }}>
|
||||
{zh ? '文章未找到' : 'Post not found'}
|
||||
</p>
|
||||
<Link to="/blog" style={{ color: 'var(--primary)', fontSize: '14px' }}>
|
||||
{zh ? '返回博客 →' : 'Back to blog →'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return (
|
||||
<div className="docs-skeleton-wrap">
|
||||
<div className="skeleton-line" style={{ width: '80px', height: '14px', marginBottom: '44px' }} />
|
||||
<div className="skeleton-line" style={{ width: '60%', height: '44px', marginBottom: '16px' }} />
|
||||
<div className="skeleton-line" style={{ width: '40%', height: '16px', marginBottom: '48px' }} />
|
||||
<div className="skeleton-line" style={{ width: '100%', height: '16px' }} />
|
||||
<div className="skeleton-line" style={{ width: '92%', height: '16px' }} />
|
||||
<div className="skeleton-line" style={{ width: '96%', height: '16px' }} />
|
||||
<div className="skeleton-line" style={{ width: '80%', height: '16px' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const readTime = estimateReadTime(content.html);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEOHead
|
||||
@@ -40,46 +82,54 @@ export default function BlogDetailPage() {
|
||||
type="article"
|
||||
publishedTime={content.meta.date}
|
||||
/>
|
||||
<div className="max-w-3xl mx-auto py-16 lg:py-20 px-6">
|
||||
{/* Back link */}
|
||||
<Link
|
||||
to="/blog"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-ink-muted hover:text-ink transition-colors mb-8"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
{language === 'en' ? 'All posts' : '所有文章'}
|
||||
|
||||
<div className="docs-detail">
|
||||
{/* Back */}
|
||||
<Link to="/blog" className="docs-back-link">
|
||||
<svg viewBox="0 0 24 24"><path d="M15 18l-6-6 6-6" /></svg>
|
||||
{zh ? '所有文章' : 'All posts'}
|
||||
</Link>
|
||||
|
||||
{/* Article header */}
|
||||
<header className="mb-10">
|
||||
<div className="flex items-center gap-2 text-sm text-ink-muted mb-4">
|
||||
<Calendar size={14} />
|
||||
<time>{content.meta.date}</time>
|
||||
</div>
|
||||
{content.meta.tags && content.meta.tags.length > 0 && (
|
||||
<div className="flex gap-2 mb-4">
|
||||
{content.meta.tags.map((tag: string) => (
|
||||
<span key={tag} className="text-xs bg-coral-50 text-coral-600 px-2.5 py-1 rounded-full font-medium">
|
||||
{tag}
|
||||
</span>
|
||||
<div className="docs-article-header">
|
||||
{content.meta.tags.length > 0 && (
|
||||
<div className="docs-article-tags">
|
||||
{content.meta.tags.map(tag => (
|
||||
<span key={tag} className="docs-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<h1 className="docs-article-h1">{content.meta.title}</h1>
|
||||
<div className="docs-meta-row">
|
||||
<span>{formatDate(content.meta.date, language)}</span>
|
||||
<span className="docs-meta-sep">·</span>
|
||||
<span>{readTime} {zh ? '分钟阅读' : 'min read'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article body */}
|
||||
<article
|
||||
className="prose prose-lg prose-warm max-w-none
|
||||
prose-headings:font-display prose-headings:tracking-tight
|
||||
prose-h1:text-3xl prose-h1:font-bold
|
||||
prose-h2:text-2xl prose-h2:font-semibold prose-h2:mt-10
|
||||
prose-a:text-coral-600 prose-a:no-underline hover:prose-a:underline
|
||||
prose-code:text-coral-600 prose-code:bg-coral-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-sm
|
||||
prose-pre:bg-ink prose-pre:text-cream-100 prose-pre:rounded-xl
|
||||
prose-img:rounded-xl
|
||||
prose-blockquote:border-coral-300 prose-blockquote:bg-coral-50/30 prose-blockquote:rounded-r-xl prose-blockquote:py-1"
|
||||
<div
|
||||
className="docs-prose"
|
||||
dangerouslySetInnerHTML={{ __html: content.html }}
|
||||
/>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="docs-cta-box">
|
||||
<div className="docs-cta-title">
|
||||
{zh ? '准备好试试了吗?' : 'Ready to try it yourself?'}
|
||||
</div>
|
||||
<p className="docs-cta-desc">
|
||||
{zh
|
||||
? '上传一张公式图片,秒级获得 LaTeX 输出——无需注册。'
|
||||
: 'Upload a formula image and get LaTeX output in under a second — no sign-up needed.'}
|
||||
</p>
|
||||
<Link to="/app" className="btn btn-primary" style={{ display: 'inline-flex' }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
|
||||
<path d="M5 3l14 9-14 9V3z" />
|
||||
</svg>
|
||||
{zh ? '免费试用 TexPixel' : 'Try TexPixel Free'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowRight, Calendar } from 'lucide-react';
|
||||
import SEOHead from '../components/seo/SEOHead';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { loadManifest, type ContentMeta } from '../lib/content';
|
||||
|
||||
function formatDate(dateStr: string, lang: string): string {
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return dateStr;
|
||||
return d.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export default function BlogListPage() {
|
||||
const { language } = useLanguage();
|
||||
const [posts, setPosts] = useState<ContentMeta[]>([]);
|
||||
@@ -15,80 +24,71 @@ export default function BlogListPage() {
|
||||
});
|
||||
}, [language]);
|
||||
|
||||
const zh = language === 'zh';
|
||||
const featured = posts[0];
|
||||
const rest = posts.slice(1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEOHead title="Blog" description="TexPixel blog — updates, tutorials, and insights" path="/blog" />
|
||||
<SEOHead
|
||||
title={zh ? 'TexPixel 博客' : 'TexPixel Blog'}
|
||||
description={zh ? '关于公式识别和 LaTeX 的更新、教程与见解。' : 'Updates, tutorials, and insights on formula recognition and LaTeX.'}
|
||||
path="/blog"
|
||||
/>
|
||||
|
||||
<div className="max-w-5xl mx-auto py-16 lg:py-20 px-6">
|
||||
<div className="blog-page">
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<h1 className="font-display text-4xl lg:text-5xl font-bold text-ink tracking-tight mb-4">
|
||||
{language === 'en' ? 'Blog' : '博客'}
|
||||
<div className="blog-page-header reveal">
|
||||
<div className="eyebrow">{zh ? '博客' : 'Blog'}</div>
|
||||
<h1 className="blog-page-title">
|
||||
{zh ? '最新文章' : 'Latest Posts'}
|
||||
</h1>
|
||||
<p className="text-ink-secondary text-lg max-w-xl">
|
||||
{language === 'en'
|
||||
? 'Updates, tutorials, and insights on formula recognition and LaTeX.'
|
||||
: '关于公式识别和 LaTeX 的更新、教程和见解。'}
|
||||
<p className="blog-page-subtitle">
|
||||
{zh
|
||||
? '关于公式识别和 LaTeX 的更新、教程与见解。'
|
||||
: 'Updates, tutorials, and insights on formula recognition and LaTeX.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Featured post */}
|
||||
{featured && (
|
||||
<Link
|
||||
to={`/blog/${featured.slug}`}
|
||||
className="card block p-8 mb-8 group hover:border-coral-200"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-xs text-ink-muted mb-3">
|
||||
<Calendar size={13} />
|
||||
<time>{featured.date}</time>
|
||||
<Link to={`/blog/${featured.slug}`} className="blog-featured reveal">
|
||||
<div className="blog-featured-eyebrow">
|
||||
{zh ? '精选文章' : 'Featured'}
|
||||
</div>
|
||||
<h2 className="font-display text-2xl lg:text-3xl font-bold text-ink mb-3 group-hover:text-coral-600 transition-colors">
|
||||
{featured.title}
|
||||
</h2>
|
||||
<p className="text-ink-secondary leading-relaxed mb-4 max-w-2xl">
|
||||
{featured.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
{featured.tags.map(tag => (
|
||||
<span key={tag} className="text-xs bg-coral-50 text-coral-600 px-2.5 py-1 rounded-full font-medium">
|
||||
{tag}
|
||||
</span>
|
||||
<div className="blog-featured-title">{featured.title}</div>
|
||||
<div className="blog-featured-desc">{featured.description}</div>
|
||||
<div className="blog-featured-footer">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
|
||||
{featured.tags.slice(0, 3).map(tag => (
|
||||
<span key={tag} className="docs-tag">{tag}</span>
|
||||
))}
|
||||
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
|
||||
{formatDate(featured.date, language)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="inline-flex items-center gap-1 text-sm font-medium text-coral-500 group-hover:gap-2 transition-all">
|
||||
{language === 'en' ? 'Read more' : '阅读全文'}
|
||||
<ArrowRight size={14} />
|
||||
<span className="blog-featured-read">
|
||||
{zh ? '阅读全文' : 'Read more'}
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
|
||||
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Rest of posts */}
|
||||
{/* Post grid */}
|
||||
{rest.length > 0 && (
|
||||
<div className="grid md:grid-cols-2 gap-5">
|
||||
{rest.map((post) => (
|
||||
<Link
|
||||
key={post.slug}
|
||||
to={`/blog/${post.slug}`}
|
||||
className="card p-6 group hover:border-coral-200"
|
||||
>
|
||||
<div className="text-xs text-ink-muted mb-2">
|
||||
<time>{post.date}</time>
|
||||
</div>
|
||||
<h2 className="font-display text-lg font-semibold text-ink mb-2 group-hover:text-coral-600 transition-colors">
|
||||
{post.title}
|
||||
</h2>
|
||||
<p className="text-ink-secondary text-sm leading-relaxed mb-3">{post.description}</p>
|
||||
<div className="blog-grid">
|
||||
{rest.map(post => (
|
||||
<Link key={post.slug} to={`/blog/${post.slug}`} className="blog-card reveal">
|
||||
<div className="blog-card-date">{formatDate(post.date, language)}</div>
|
||||
<div className="blog-card-title">{post.title}</div>
|
||||
<div className="blog-card-desc">{post.description}</div>
|
||||
{post.tags.length > 0 && (
|
||||
<div className="flex gap-2">
|
||||
{post.tags.map(tag => (
|
||||
<span key={tag} className="text-[11px] bg-cream-200 text-ink-secondary px-2 py-0.5 rounded-full">
|
||||
{tag}
|
||||
</span>
|
||||
<div className="blog-card-footer">
|
||||
{post.tags.slice(0, 2).map(tag => (
|
||||
<span key={tag} className="docs-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -99,10 +99,8 @@ export default function BlogListPage() {
|
||||
|
||||
{/* Empty state */}
|
||||
{posts.length === 0 && (
|
||||
<div className="card p-12 text-center">
|
||||
<p className="text-ink-muted text-sm">
|
||||
{language === 'en' ? 'No posts yet. Check back soon!' : '暂无文章,敬请期待!'}
|
||||
</p>
|
||||
<div className="blog-empty">
|
||||
{zh ? '暂无文章,敬请期待。' : 'No posts yet. Check back soon.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
}
|
||||
|
||||
.marketing-page {
|
||||
position: relative;
|
||||
background: var(--bg);
|
||||
color: var(--text-strong);
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
@@ -39,7 +40,7 @@
|
||||
/* ── GRID BACKGROUND ── */
|
||||
.marketing-page::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--border) 1px, transparent 1px),
|
||||
@@ -51,27 +52,33 @@
|
||||
}
|
||||
|
||||
/* ── GLOW BLOBS ── */
|
||||
/* position: fixed keeps blobs in-viewport at all times.
|
||||
will-change + translateZ promotes each blob to its own GPU compositor
|
||||
layer — the browser composites them independently so main-thread scroll
|
||||
repaints do NOT include these elements. */
|
||||
.glow-blob {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
.glow-blob-1 {
|
||||
width: 480px; height: 380px;
|
||||
background: rgba(200,98,42,0.09);
|
||||
top: -80px; right: 80px;
|
||||
width: 520px; height: 400px;
|
||||
background: radial-gradient(circle, rgba(200,98,42,0.13) 0%, transparent 70%);
|
||||
top: -100px; right: 60px;
|
||||
}
|
||||
.glow-blob-2 {
|
||||
width: 360px; height: 320px;
|
||||
background: rgba(140,201,190,0.10);
|
||||
bottom: 20%; left: -80px;
|
||||
width: 400px; height: 360px;
|
||||
background: radial-gradient(circle, rgba(140,201,190,0.13) 0%, transparent 70%);
|
||||
bottom: 25%; left: -100px;
|
||||
}
|
||||
.glow-blob-3 {
|
||||
width: 320px; height: 280px;
|
||||
background: rgba(243,201,106,0.08);
|
||||
top: 55%; right: -60px;
|
||||
width: 360px; height: 300px;
|
||||
background: radial-gradient(circle, rgba(243,201,106,0.11) 0%, transparent 70%);
|
||||
top: 50%; right: -80px;
|
||||
}
|
||||
|
||||
/* ── LAYOUT ── */
|
||||
@@ -1970,3 +1977,190 @@
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════
|
||||
BLOG PAGES
|
||||
════════════════════════════════════════ */
|
||||
|
||||
/* ── Blog list page ── */
|
||||
.blog-page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 64px 24px 80px;
|
||||
}
|
||||
|
||||
.blog-page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.blog-page-title {
|
||||
font-family: 'Lora', serif;
|
||||
font-size: clamp(2rem, 4vw, 2.75rem);
|
||||
font-weight: 700;
|
||||
color: var(--text-strong);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.blog-page-subtitle {
|
||||
font-size: 1.0625rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Featured post ── */
|
||||
.blog-featured {
|
||||
display: block;
|
||||
background: var(--card);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--r-l);
|
||||
padding: 36px 40px;
|
||||
margin-bottom: 32px;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.18s, border-color 0.18s, transform 0.18s;
|
||||
}
|
||||
|
||||
.blog-featured::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(200,98,42,0.04) 0%, transparent 60%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blog-featured:hover {
|
||||
border-color: var(--warm-wash);
|
||||
box-shadow: var(--shadow-float);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.blog-featured-eyebrow {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--primary);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.blog-featured-title {
|
||||
font-family: 'Lora', serif;
|
||||
font-size: clamp(1.375rem, 2.5vw, 1.875rem);
|
||||
font-weight: 700;
|
||||
color: var(--text-strong);
|
||||
line-height: 1.3;
|
||||
margin-bottom: 12px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.blog-featured:hover .blog-featured-title {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.blog-featured-desc {
|
||||
font-size: 1rem;
|
||||
color: var(--text-body);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 20px;
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.blog-featured-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.blog-featured-read {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: gap 0.15s;
|
||||
}
|
||||
|
||||
.blog-featured:hover .blog-featured-read {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ── Blog grid ── */
|
||||
.blog-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.blog-card {
|
||||
display: block;
|
||||
background: var(--card);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--r-m);
|
||||
padding: 24px 28px;
|
||||
text-decoration: none;
|
||||
transition: box-shadow 0.18s, border-color 0.18s, transform 0.18s;
|
||||
}
|
||||
|
||||
.blog-card:hover {
|
||||
border-color: var(--warm-wash);
|
||||
box-shadow: var(--shadow-soft);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.blog-card-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.blog-card-title {
|
||||
font-family: 'Lora', serif;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-strong);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.blog-card:hover .blog-card-title {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.blog-card-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-body);
|
||||
line-height: 1.65;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.blog-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.blog-empty {
|
||||
text-align: center;
|
||||
padding: 64px 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.blog-grid { grid-template-columns: 1fr; }
|
||||
.blog-featured { padding: 24px; }
|
||||
.blog-page { padding: 40px 16px 64px; }
|
||||
}
|
||||
|
||||
/* ── Blog detail page (reuses .docs-detail, .docs-back-link,
|
||||
.docs-prose, .docs-cta-box, .docs-skeleton-wrap) ── */
|
||||
|
||||
Reference in New Issue
Block a user