This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { site } from '../site.config';
|
||||
---
|
||||
|
||||
<BaseLayout title="About" description={`About ${site.title} and its editorial approach.`}>
|
||||
<section class="shell about-head">
|
||||
<p class="eyebrow">About the publication</p>
|
||||
<h1 class="display">Friendly on the surface. Inspectable underneath.</h1>
|
||||
</section>
|
||||
<section class="shell about-grid">
|
||||
<aside>
|
||||
<p class="portrait" aria-label="Portrait placeholder">ANA</p>
|
||||
<p><strong>{site.author.name}</strong><br />Author and maintainer</p>
|
||||
</aside>
|
||||
<div class="prose">
|
||||
<p><em>Trust Issues</em> is an independent publication about who and what we trust online, where authority actually sits, and what happens when systems meet human lives.</p>
|
||||
<p>It is written by Ana. That authorship is not hidden behind an institutional voice: judgement has a source, and the person making it should be visible.</p>
|
||||
<h2>Cheerful suspicion</h2>
|
||||
<p>When something is described as verified, trustless, self-sovereign, privacy-preserving, safe, or autonomous, the useful next question is not whether to believe or ridicule it. It is: what does that claim mean here, who controls the exception, and where did the trust move?</p>
|
||||
<h2>How maintenance works</h2>
|
||||
<p>Articles carry publication and review dates, named authors and maintainers, and an honest status. Git provides a public provenance record. It does not certify that the reporting is true; corrections, sourcing, fair representation, and editorial independence remain human responsibilities.</p>
|
||||
<h2>Contact</h2>
|
||||
{site.contactUrl ? <p><a href={site.contactUrl}>Contact Ana privately</a>.</p> : <p>A dedicated private contact and source-safety route will be added before public launch. Until then, use an existing trusted channel rather than a public repository issue for anything sensitive.</p>}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.about-head { padding: clamp(4rem, 9vw, 8rem) 0; }
|
||||
.about-head h1 { max-width: 14ch; }
|
||||
.about-grid { display: grid; grid-template-columns: minmax(180px, .5fr) 1.5fr; gap: clamp(3rem, 10vw, 10rem); align-items: start; }
|
||||
aside { position: sticky; top: 2rem; font-size: .8rem; }
|
||||
.portrait { display: grid; aspect-ratio: 4 / 5; margin: 0 0 1rem; place-items: center; background: var(--blue); color: var(--paper); font-family: 'Newsreader', Georgia, serif; font-size: 3rem; }
|
||||
@media (max-width: 700px) { .about-grid { grid-template-columns: 1fr; } aside { position: static; max-width: 220px; } }
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { site } from '../site.config';
|
||||
---
|
||||
|
||||
<BaseLayout title="Book" description="Ana's book: argument, sample, and purchase information.">
|
||||
<section class="shell book-page">
|
||||
<div class="cover" aria-hidden="true">
|
||||
{site.book.cover ? <img src={site.book.cover} alt="" /> : <span>THE<br />BOOK</span>}
|
||||
</div>
|
||||
<div>
|
||||
<p class="eyebrow">The book</p>
|
||||
<h1 class="display">{site.book.enabled ? site.book.title : 'The book belongs at the centre of the work.'}</h1>
|
||||
<p class="lede">{site.book.enabled ? site.book.description : 'This surface is ready for the actual cover, title, argument, sample chapter, endorsements, and purchase route. Those details are intentionally not fabricated in the scaffold.'}</p>
|
||||
{site.book.enabled && (
|
||||
<div class="actions">
|
||||
{site.book.sampleUrl && <a class="button" href={site.book.sampleUrl}>Read a sample</a>}
|
||||
{site.book.buyUrl && <a class="button primary" href={site.book.buyUrl}>Buy the book</a>}
|
||||
</div>
|
||||
)}
|
||||
<div class="book-anatomy">
|
||||
<div><span>01</span><p>Why I wrote it</p></div>
|
||||
<div><span>02</span><p>Read a real chapter</p></div>
|
||||
<div><span>03</span><p>Choose a purchase route</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.book-page { display: grid; grid-template-columns: minmax(250px, .65fr) 1.35fr; gap: clamp(3rem, 9vw, 9rem); align-items: center; padding: clamp(4rem, 9vw, 8rem) 0; }
|
||||
.cover { display: grid; aspect-ratio: 3 / 4; place-items: center; border: 1px solid #7690ad; background: var(--blue); box-shadow: 22px 22px 0 #d8d1c4; color: var(--paper); font-family: 'Newsreader', Georgia, serif; font-size: clamp(2.5rem, 6vw, 5rem); line-height: .85; text-align: center; }
|
||||
.cover img { width: 100%; height: 100%; object-fit: cover; }
|
||||
h1 { max-width: 13ch; }
|
||||
.lede { max-width: 58ch; color: var(--muted); font-size: 1.1rem; }
|
||||
.actions { display: flex; gap: .7rem; margin: 2rem 0; }
|
||||
.book-anatomy { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 3rem; border-top: 1px solid var(--rule); }
|
||||
.book-anatomy div { padding: 1rem; border-right: 1px solid var(--rule); }
|
||||
.book-anatomy div:last-child { border-right: 0; }
|
||||
.book-anatomy span { color: var(--amend); font-family: 'IBM Plex Mono', monospace; font-size: .68rem; }
|
||||
.book-anatomy p { font-family: 'Newsreader', Georgia, serif; font-size: 1.15rem; }
|
||||
@media (max-width: 750px) { .book-page { grid-template-columns: 1fr; } .cover { max-width: 320px; } .book-anatomy { grid-template-columns: 1fr; } .book-anatomy div { border-right: 0; border-bottom: 1px solid var(--rule); } }
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
import { getCollection, render, type CollectionEntry } from 'astro:content';
|
||||
import ArticleLayout from '../../layouts/ArticleLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = await getCollection('briefings', ({ data }) => !data.draft);
|
||||
return entries.map((entry) => ({ params: { id: entry.id }, props: { entry } }));
|
||||
}
|
||||
|
||||
interface Props { entry: CollectionEntry<'briefings'> }
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
---
|
||||
|
||||
<ArticleLayout
|
||||
title={entry.data.title}
|
||||
description={entry.data.description}
|
||||
kind={`This Week in Trust · Issue ${String(entry.data.issue).padStart(3, '0')}`}
|
||||
author={entry.data.author}
|
||||
maintainers={entry.data.maintainers}
|
||||
published={entry.data.published}
|
||||
reviewed={entry.data.reviewed}
|
||||
status={entry.data.status}
|
||||
sourcePath={`src/content/briefings/${entry.id}.md`}
|
||||
>
|
||||
<Content />
|
||||
</ArticleLayout>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { formatDate, newestFirst } from '../../lib/content';
|
||||
|
||||
const entries = newestFirst(await getCollection('briefings', ({ data }) => !data.draft));
|
||||
---
|
||||
|
||||
<BaseLayout title="This Week in Trust" description="The compact weekly briefing and historical record.">
|
||||
<section class="shell archive-head">
|
||||
<p class="eyebrow">The weekly ritual</p>
|
||||
<h1 class="display">This Week<br />in Trust</h1>
|
||||
<p>Three failures, two new systems, one government overreach, and one surprisingly good idea. Compact enough to read; consistent enough to become a record.</p>
|
||||
</section>
|
||||
<section class="shell issue-list">
|
||||
{entries.length > 0 ? entries.map((entry) => (
|
||||
<article>
|
||||
<span>Issue {String(entry.data.issue).padStart(3, '0')}</span>
|
||||
<h2><a href={`/briefing/${entry.id}`}>{entry.data.title}</a></h2>
|
||||
<p>{entry.data.description}</p>
|
||||
<time datetime={entry.data.published.toISOString()}>{formatDate(entry.data.published)}</time>
|
||||
</article>
|
||||
)) : <p class="empty-state">The first weekly briefing is being prepared.</p>}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.archive-head { padding: clamp(4rem, 9vw, 8rem) 0; }
|
||||
.archive-head > p:last-child { max-width: 58ch; color: var(--muted); }
|
||||
.issue-list article { display: grid; grid-template-columns: 8rem 1fr 1fr 8rem; gap: 1.5rem; align-items: baseline; padding: 1.5rem 0; border-top: 1px solid var(--rule); }
|
||||
.issue-list span, .issue-list time { color: var(--muted); font-family: 'IBM Plex Mono', monospace; font-size: .68rem; }
|
||||
h2 { margin: 0; font-family: 'Newsreader', Georgia, serif; font-size: 2rem; line-height: 1; }
|
||||
h2 a { text-decoration: none; }
|
||||
.issue-list p { margin: 0; color: var(--muted); font-size: .9rem; }
|
||||
@media (max-width: 760px) { .issue-list article { grid-template-columns: 1fr auto; } h2, .issue-list p { grid-column: 1 / -1; } }
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
import { getCollection, render, type CollectionEntry } from 'astro:content';
|
||||
import ArticleLayout from '../../layouts/ArticleLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = await getCollection('guides', ({ data }) => !data.draft);
|
||||
return entries.map((entry) => ({ params: { id: entry.id }, props: { entry } }));
|
||||
}
|
||||
|
||||
interface Props { entry: CollectionEntry<'guides'> }
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
---
|
||||
|
||||
<ArticleLayout
|
||||
title={entry.data.title}
|
||||
description={entry.data.description}
|
||||
kind="Field guide"
|
||||
author={entry.data.author}
|
||||
maintainers={entry.data.maintainers}
|
||||
published={entry.data.firstPublished}
|
||||
reviewed={entry.data.reviewed}
|
||||
status={entry.data.status}
|
||||
sourcePath={`src/content/guides/${entry.id}.md`}
|
||||
>
|
||||
<Content />
|
||||
</ArticleLayout>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { formatDate, guidesByReview } from '../../lib/content';
|
||||
|
||||
const entries = guidesByReview(await getCollection('guides', ({ data }) => !data.draft));
|
||||
---
|
||||
|
||||
<BaseLayout title="Field guides" description="Maintained explanations of systems that mediate identity, authority, and trust.">
|
||||
<section class="shell archive-head">
|
||||
<p class="eyebrow">Documentation written by journalists</p>
|
||||
<h1 class="display">Field guides</h1>
|
||||
<p>The page a smart newcomer needs after reading three contradictory accounts: what the system is, who must trust whom, what it knows, and what happens when access is lost.</p>
|
||||
</section>
|
||||
<section class="shell guide-list">
|
||||
{entries.length > 0 ? entries.map((entry, index) => (
|
||||
<article>
|
||||
<span>0{index + 1}</span>
|
||||
<div>
|
||||
<h2><a href={`/guides/${entry.id}`}>{entry.data.title}</a></h2>
|
||||
<p>{entry.data.description}</p>
|
||||
</div>
|
||||
<small>Last reviewed<br />{formatDate(entry.data.reviewed)}</small>
|
||||
</article>
|
||||
)) : <p class="empty-state">The first guides are in editorial review. Staleness will be visible once they publish.</p>}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.archive-head { padding: clamp(4rem, 9vw, 8rem) 0; }
|
||||
.archive-head > p:last-child { max-width: 62ch; color: var(--muted); }
|
||||
.guide-list article { display: grid; grid-template-columns: 5rem 1fr 10rem; gap: 2rem; padding: 2rem 0; border-top: 1px solid var(--rule); }
|
||||
.guide-list > article > span { color: var(--amend); font-family: 'IBM Plex Mono', monospace; font-size: .72rem; }
|
||||
h2 { margin: 0; font-family: 'Newsreader', Georgia, serif; font-size: clamp(2rem, 5vw, 3.5rem); line-height: 1; }
|
||||
h2 a { text-decoration: none; }
|
||||
p, small { color: var(--muted); }
|
||||
small { font-family: 'IBM Plex Mono', monospace; font-size: .66rem; text-transform: uppercase; }
|
||||
@media (max-width: 650px) { .guide-list article { grid-template-columns: 2rem 1fr; } small { grid-column: 2; } }
|
||||
</style>
|
||||
@@ -0,0 +1,407 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import RecentMaintenance from '../components/RecentMaintenance.astro';
|
||||
import StoryCard from '../components/StoryCard.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { formatDate, guidesByReview, newestFirst } from '../lib/content';
|
||||
import { site } from '../site.config';
|
||||
|
||||
const stories = newestFirst(await getCollection('stories', ({ data }) => !data.draft));
|
||||
const briefings = newestFirst(await getCollection('briefings', ({ data }) => !data.draft));
|
||||
const guides = guidesByReview(await getCollection('guides', ({ data }) => !data.draft));
|
||||
const episodes = newestFirst(await getCollection('episodes', ({ data }) => !data.draft));
|
||||
|
||||
const featured = stories.find(({ data }) => data.featured) ?? stories[0];
|
||||
const latestBriefing = briefings[0];
|
||||
const latestEpisode = episodes[0];
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<section class="hero shell">
|
||||
<p class="eyebrow">{site.author.note}</p>
|
||||
<div class="hero-grid">
|
||||
<h1 class="display">{site.proposition}</h1>
|
||||
<div class="hero-note">
|
||||
<p>{site.description}</p>
|
||||
<p>
|
||||
We follow claims of verification, safety, privacy, and autonomy to the
|
||||
people who hold the keys—and to the people who live with the consequences.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
{featured && <a class="button primary" href={`/stories/${featured.id}`}>Read the current story</a>}
|
||||
<a class="button" href="/podcast">Listen to the podcast</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shell current">
|
||||
<div class="section-rule">
|
||||
<h2 class="section-title">Current issue</h2>
|
||||
<a href="/stories">All stories →</a>
|
||||
</div>
|
||||
<div class="current-grid">
|
||||
<div>
|
||||
{featured ? <StoryCard entry={featured} featured /> : <p class="empty-state">The first story is being prepared.</p>}
|
||||
</div>
|
||||
<aside class="briefing-card">
|
||||
<p class="eyebrow">This week in trust</p>
|
||||
{latestBriefing ? (
|
||||
<>
|
||||
<p class="issue">Issue {String(latestBriefing.data.issue).padStart(3, '0')}</p>
|
||||
<h3><a href={`/briefing/${latestBriefing.id}`}>{latestBriefing.data.title}</a></h3>
|
||||
<p>{latestBriefing.data.description}</p>
|
||||
<div class="briefing-foot">
|
||||
<time datetime={latestBriefing.data.published.toISOString()}>{formatDate(latestBriefing.data.published)}</time>
|
||||
<a href={`/briefing/${latestBriefing.id}`}>Read issue →</a>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>The weekly briefing begins here: three failures, two new systems, one overreach, and one surprisingly good idea.</p>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shell listening">
|
||||
<div class="section-rule">
|
||||
<h2 class="section-title">Listen</h2>
|
||||
<a href="/podcast">Podcast archive →</a>
|
||||
</div>
|
||||
<div class="listen-grid">
|
||||
<div class="sound-mark" aria-hidden="true">
|
||||
<span></span><span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
<div>
|
||||
{latestEpisode ? (
|
||||
<>
|
||||
<p class="eyebrow">Episode {latestEpisode.data.episode}</p>
|
||||
<h3><a href={`/podcast/${latestEpisode.id}`}>{latestEpisode.data.title}</a></h3>
|
||||
<p>{latestEpisode.data.description}</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p class="eyebrow">Podcast</p>
|
||||
<h3>Who gets to act in our name?</h3>
|
||||
<p>Conversations about authority, recovery, delegation, and the human lives hidden inside technical systems. Episodes and full transcripts will live here.</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shell guides">
|
||||
<div class="section-rule">
|
||||
<h2 class="section-title">Field guides</h2>
|
||||
<a href="/guides">All guides →</a>
|
||||
</div>
|
||||
{guides.length > 0 ? (
|
||||
<div class="guide-grid">
|
||||
{guides.slice(0, 3).map((guide, index) => (
|
||||
<article>
|
||||
<span class="guide-number">0{index + 1}</span>
|
||||
<h3><a href={`/guides/${guide.id}`}>{guide.data.title}</a></h3>
|
||||
<p>{guide.data.description}</p>
|
||||
<small>Reviewed {formatDate(guide.data.reviewed)}</small>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="guide-grid planned">
|
||||
{['Passkeys', 'Government wallets', 'Agent authority'].map((title, index) => (
|
||||
<article>
|
||||
<span class="guide-number">0{index + 1}</span>
|
||||
<h3>{title}</h3>
|
||||
<p>In preparation. Published only when the review date means something.</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section class="shell book-strip">
|
||||
<div class="book-object" aria-hidden="true"><span>THE<br />BOOK</span></div>
|
||||
<div>
|
||||
<p class="eyebrow">The book</p>
|
||||
<h2>A permanent home, not a checkout widget.</h2>
|
||||
<p>The book page is ready for its real title, cover, sample chapter, argument, and purchase route—without inventing details before they are supplied.</p>
|
||||
<a class="button" href="/book">Visit the book page</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="shell maintenance-wrap">
|
||||
<RecentMaintenance />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding-top: clamp(4rem, 9vw, 8rem);
|
||||
padding-bottom: clamp(5rem, 10vw, 9rem);
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.65fr) minmax(260px, 0.65fr);
|
||||
gap: clamp(2rem, 6vw, 6rem);
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.hero-note {
|
||||
padding-left: 1.5rem;
|
||||
border-left: 1px solid var(--ink);
|
||||
}
|
||||
|
||||
.hero-note p:first-child {
|
||||
font-family: 'Newsreader', Georgia, serif;
|
||||
font-size: 1.45rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hero-note p:not(:first-child) {
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.current-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.8fr) minmax(280px, 0.7fr);
|
||||
}
|
||||
|
||||
.briefing-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
border-left: 1px solid var(--rule);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
background: var(--paper-raised);
|
||||
}
|
||||
|
||||
.briefing-card .issue {
|
||||
margin: 0;
|
||||
color: var(--amend);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.briefing-card h3,
|
||||
.listen-grid h3,
|
||||
.guide-grid h3,
|
||||
.book-strip h2 {
|
||||
margin: 0.35rem 0;
|
||||
font-family: 'Newsreader', Georgia, serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.briefing-card h3 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.briefing-card h3 a,
|
||||
.listen-grid h3 a,
|
||||
.guide-grid h3 a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.briefing-card > p:not(.eyebrow, .issue) {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.briefing-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-top: auto;
|
||||
padding-top: 2rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.listening,
|
||||
.guides,
|
||||
.book-strip,
|
||||
.maintenance-wrap {
|
||||
margin-top: clamp(5rem, 10vw, 9rem);
|
||||
}
|
||||
|
||||
.listen-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.4fr;
|
||||
gap: clamp(2rem, 7vw, 7rem);
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.sound-mark {
|
||||
display: flex;
|
||||
height: 180px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
.sound-mark span {
|
||||
width: 3px;
|
||||
height: 28%;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.sound-mark span:nth-child(2),
|
||||
.sound-mark span:nth-child(4) {
|
||||
height: 60%;
|
||||
}
|
||||
|
||||
.sound-mark span:nth-child(3) {
|
||||
height: 88%;
|
||||
background: var(--amend);
|
||||
}
|
||||
|
||||
.listen-grid h3 {
|
||||
max-width: 18ch;
|
||||
font-size: clamp(2rem, 5vw, 4.2rem);
|
||||
}
|
||||
|
||||
.listen-grid p:not(.eyebrow) {
|
||||
max-width: 55ch;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.guide-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.guide-grid article {
|
||||
min-height: 240px;
|
||||
padding: 1.5rem;
|
||||
border-right: 1px solid var(--rule);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.guide-grid article:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.guide-number {
|
||||
color: var(--amend);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.guide-grid h3 {
|
||||
margin-top: 3rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.guide-grid p,
|
||||
.guide-grid small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.guide-grid small {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.planned article {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.book-strip {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(200px, 0.6fr) 1.4fr;
|
||||
gap: clamp(2rem, 8vw, 8rem);
|
||||
align-items: center;
|
||||
padding: clamp(2rem, 5vw, 5rem);
|
||||
background: var(--blue);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.book-object {
|
||||
display: grid;
|
||||
max-width: 260px;
|
||||
aspect-ratio: 3 / 4;
|
||||
place-items: center;
|
||||
border: 1px solid #7690ad;
|
||||
box-shadow: 18px 18px 0 #0e2c51;
|
||||
background: #102f55;
|
||||
font-family: 'Newsreader', Georgia, serif;
|
||||
font-size: 2.5rem;
|
||||
line-height: 0.9;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.book-strip h2 {
|
||||
max-width: 15ch;
|
||||
font-size: clamp(2.6rem, 6vw, 5.2rem);
|
||||
}
|
||||
|
||||
.book-strip p:not(.eyebrow) {
|
||||
max-width: 58ch;
|
||||
color: #c5d1df;
|
||||
}
|
||||
|
||||
.book-strip .eyebrow {
|
||||
color: #9fb8d4;
|
||||
}
|
||||
|
||||
.book-strip .button {
|
||||
border-color: var(--paper);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.book-strip .button:hover {
|
||||
background: var(--paper);
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
@media (max-width: 850px) {
|
||||
.hero-grid,
|
||||
.current-grid,
|
||||
.listen-grid,
|
||||
.book-strip {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-note {
|
||||
padding-top: 1.5rem;
|
||||
padding-left: 0;
|
||||
border-top: 1px solid var(--ink);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.briefing-card {
|
||||
min-height: 310px;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.guide-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.guide-grid article {
|
||||
min-height: auto;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.guide-grid h3 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.book-strip {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import { getCollection, render, type CollectionEntry } from 'astro:content';
|
||||
import ArticleLayout from '../../layouts/ArticleLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = await getCollection('episodes', ({ data }) => !data.draft);
|
||||
return entries.map((entry) => ({ params: { id: entry.id }, props: { entry } }));
|
||||
}
|
||||
|
||||
interface Props { entry: CollectionEntry<'episodes'> }
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
---
|
||||
|
||||
<ArticleLayout
|
||||
title={entry.data.title}
|
||||
description={entry.data.description}
|
||||
kind={`Podcast · Episode ${entry.data.episode}`}
|
||||
author={entry.data.author}
|
||||
maintainers={entry.data.maintainers}
|
||||
published={entry.data.published}
|
||||
reviewed={entry.data.reviewed}
|
||||
status={entry.data.status}
|
||||
sourcePath={`src/content/episodes/${entry.id}.md`}
|
||||
>
|
||||
{entry.data.audioUrl && <audio controls preload="metadata" src={entry.data.audioUrl}>Your browser does not support embedded audio.</audio>}
|
||||
<Content />
|
||||
</ArticleLayout>
|
||||
|
||||
<style>
|
||||
audio { width: 100%; margin-bottom: 2.5rem; }
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { formatDate, newestFirst } from '../../lib/content';
|
||||
|
||||
const entries = newestFirst(await getCollection('episodes', ({ data }) => !data.draft));
|
||||
---
|
||||
|
||||
<BaseLayout title="Podcast" description="Conversations about authority, delegation, recovery, and responsibility.">
|
||||
<section class="podcast-hero">
|
||||
<div class="shell podcast-grid">
|
||||
<div>
|
||||
<p class="eyebrow">The podcast</p>
|
||||
<h1 class="display">Who gets to act in our name?</h1>
|
||||
</div>
|
||||
<p>Conversations pairing people who rarely share a microphone: system designers and the people who meet those systems at their most consequential edges. Full transcripts live beside every episode.</p>
|
||||
</div>
|
||||
<div class="wave shell" aria-hidden="true">
|
||||
{Array.from({ length: 21 }).map((_, index) => <i style={`--n:${(index * 37) % 100}`} />)}
|
||||
</div>
|
||||
</section>
|
||||
<section class="shell episode-list">
|
||||
<div class="section-rule">
|
||||
<h2 class="section-title">Episodes</h2>
|
||||
<a href="/rss.xml">RSS →</a>
|
||||
</div>
|
||||
{entries.length > 0 ? entries.map((entry) => (
|
||||
<article>
|
||||
<span>{String(entry.data.episode).padStart(2, '0')}</span>
|
||||
<div>
|
||||
<h3><a href={`/podcast/${entry.id}`}>{entry.data.title}</a></h3>
|
||||
<p>{entry.data.description}</p>
|
||||
</div>
|
||||
<time datetime={entry.data.published.toISOString()}>{formatDate(entry.data.published)}</time>
|
||||
</article>
|
||||
)) : <p class="empty-state">The archive is ready for real episodes and transcripts. No synthetic placeholder episode has been published.</p>}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.podcast-hero { padding: clamp(4rem, 9vw, 8rem) 0 0; background: var(--ink); color: var(--paper); }
|
||||
.podcast-grid { display: grid; grid-template-columns: 1.5fr .7fr; gap: clamp(2rem, 7vw, 7rem); align-items: end; }
|
||||
.podcast-grid .eyebrow { color: #9fb8d4; }
|
||||
.podcast-grid > p { color: #bcb8af; }
|
||||
.wave { display: flex; height: 210px; align-items: center; justify-content: space-between; margin-top: 4rem; overflow: hidden; }
|
||||
.wave i { display: block; width: 2px; height: calc(20% + var(--n) * .75%); background: color-mix(in srgb, var(--paper) calc(25% + var(--n) * .5%), var(--amend)); }
|
||||
.episode-list { margin-top: 6rem; }
|
||||
.episode-list article { display: grid; grid-template-columns: 4rem 1fr 8rem; gap: 2rem; padding: 2rem 0; border-bottom: 1px solid var(--rule); }
|
||||
.episode-list article > span, time { color: var(--muted); font-family: 'IBM Plex Mono', monospace; font-size: .7rem; }
|
||||
h3 { margin: 0; font-family: 'Newsreader', Georgia, serif; font-size: 2.3rem; line-height: 1; }
|
||||
h3 a { text-decoration: none; }
|
||||
.episode-list p { color: var(--muted); }
|
||||
@media (max-width: 700px) { .podcast-grid { grid-template-columns: 1fr; } .episode-list article { grid-template-columns: 2rem 1fr; } time { grid-column: 2; } }
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { site } from '../site.config';
|
||||
|
||||
export async function GET(context: { site?: URL }) {
|
||||
const [stories, briefings, episodes] = await Promise.all([
|
||||
getCollection('stories', ({ data }) => !data.draft),
|
||||
getCollection('briefings', ({ data }) => !data.draft),
|
||||
getCollection('episodes', ({ data }) => !data.draft),
|
||||
]);
|
||||
|
||||
const items = [
|
||||
...stories.map((entry) => ({
|
||||
title: entry.data.title,
|
||||
description: entry.data.description,
|
||||
pubDate: entry.data.published,
|
||||
link: `/stories/${entry.id}`,
|
||||
})),
|
||||
...briefings.map((entry) => ({
|
||||
title: entry.data.title,
|
||||
description: entry.data.description,
|
||||
pubDate: entry.data.published,
|
||||
link: `/briefing/${entry.id}`,
|
||||
})),
|
||||
...episodes.map((entry) => ({
|
||||
title: entry.data.title,
|
||||
description: entry.data.description,
|
||||
pubDate: entry.data.published,
|
||||
link: `/podcast/${entry.id}`,
|
||||
})),
|
||||
].sort((a, b) => b.pubDate.valueOf() - a.pubDate.valueOf());
|
||||
|
||||
return rss({
|
||||
title: `${site.title} ${site.qualifier}`,
|
||||
description: site.description,
|
||||
site: context.site ?? new URL('https://example.com'),
|
||||
items,
|
||||
customData: '<language>en-gb</language>',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
import { getCollection, render, type CollectionEntry } from 'astro:content';
|
||||
import ArticleLayout from '../../layouts/ArticleLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = await getCollection('stories', ({ data }) => !data.draft);
|
||||
return entries.map((entry) => ({ params: { id: entry.id }, props: { entry } }));
|
||||
}
|
||||
|
||||
interface Props { entry: CollectionEntry<'stories'> }
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
---
|
||||
|
||||
<ArticleLayout
|
||||
title={entry.data.title}
|
||||
description={entry.data.description}
|
||||
kind="Story"
|
||||
author={entry.data.author}
|
||||
maintainers={entry.data.maintainers}
|
||||
published={entry.data.published}
|
||||
reviewed={entry.data.reviewed}
|
||||
status={entry.data.status}
|
||||
sourcePath={`src/content/stories/${entry.id}.md`}
|
||||
trustPattern={entry.data.trustPattern}
|
||||
>
|
||||
<Content />
|
||||
</ArticleLayout>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import StoryCard from '../../components/StoryCard.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { newestFirst } from '../../lib/content';
|
||||
|
||||
const stories = newestFirst(await getCollection('stories', ({ data }) => !data.draft));
|
||||
---
|
||||
|
||||
<BaseLayout title="Stories" description="Reported and argued stories about trust, identity, and authority.">
|
||||
<section class="shell archive-head">
|
||||
<p class="eyebrow">Original reporting and essays</p>
|
||||
<h1 class="display">Stories</h1>
|
||||
<p>People first. Machinery explained. Claims followed to the point where somebody must decide, recover, consent, or bear the loss.</p>
|
||||
</section>
|
||||
<section class="shell archive-list">
|
||||
{stories.length > 0 ? stories.map((entry) => <StoryCard entry={entry} />) : <p class="empty-state">The first story is being prepared.</p>}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.archive-head { padding: clamp(4rem, 9vw, 8rem) 0; }
|
||||
.archive-head > p:last-child { max-width: 58ch; color: var(--muted); font-size: 1.05rem; }
|
||||
.archive-list { display: grid; gap: 2rem; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user