Establish the publication foundation
validate / validate (push) Failing after 7s

This commit is contained in:
Compleet
2026-07-14 11:54:31 +01:00
commit 006955bfc8
46 changed files with 7704 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
---
import { site } from '../site.config';
---
<footer>
<div class="shell footer-grid">
<div>
<p class="footer-title">{site.title}</p>
<p>{site.description}</p>
</div>
<div>
<p class="footer-label">Read independently</p>
<a href="/rss.xml">RSS feed</a><br />
<a href={site.repositoryUrl}>Public repository</a>
</div>
<div>
<p class="footer-label">Editorial record</p>
<a href={`${site.repositoryUrl}/src/branch/main/EDITORIAL.md`}>Commitments</a><br />
<a href={`${site.repositoryUrl}/commits/branch/main`}>Revision history</a>
</div>
</div>
<div class="shell colophon">
<span>© {new Date().getFullYear()} Ana</span>
<span>Static HTML · no accounts · no tracking scripts</span>
</div>
</footer>
<style>
footer {
margin-top: 7rem;
padding: 3.5rem 0 1.5rem;
border-top: 1px solid var(--ink);
background: var(--ink);
color: var(--paper);
}
.footer-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: 3rem;
}
p {
max-width: 38rem;
margin: 0.25rem 0;
color: #bcb8af;
}
.footer-title {
color: var(--paper);
font-family: 'Newsreader', Georgia, serif;
font-size: 2rem;
line-height: 1;
}
.footer-label {
margin-bottom: 0.75rem;
color: #8e8a82;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
a {
color: var(--paper);
font-size: 0.85rem;
}
a:hover {
color: #9fb8d4;
}
.colophon {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid #393937;
color: #8e8a82;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.66rem;
text-transform: uppercase;
}
@media (max-width: 700px) {
.footer-grid {
grid-template-columns: 1fr;
gap: 2rem;
}
.colophon {
align-items: flex-start;
flex-direction: column;
}
}
</style>
+156
View File
@@ -0,0 +1,156 @@
---
import { navigation, site } from '../site.config';
---
<header class="site-header">
<div class="shell header-inner">
<a class="masthead" href="/" aria-label={`${site.title} home`}>
<span class="masthead-mark" aria-hidden="true"><i></i></span>
<span class="masthead-text">
<strong>{site.title}</strong>
<small>{site.qualifier}</small>
</span>
</a>
<nav aria-label="Primary navigation">
{navigation.map((item) => <a href={item.href}>{item.label}</a>)}
<a class="rss" href="/rss.xml">RSS</a>
</nav>
</div>
</header>
<style>
.site-header {
position: relative;
z-index: 10;
border-bottom: 1px solid var(--rule);
background: color-mix(in srgb, var(--paper) 94%, transparent);
}
.header-inner {
display: flex;
min-height: 76px;
align-items: center;
justify-content: space-between;
gap: 2rem;
}
.masthead {
display: inline-flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
}
.masthead:hover {
color: var(--ink);
}
.masthead-mark {
position: relative;
display: block;
width: 30px;
height: 30px;
border: 2px solid currentColor;
}
.masthead-mark::before,
.masthead-mark::after,
.masthead-mark i {
position: absolute;
display: block;
content: '';
background: var(--paper);
}
.masthead-mark::before {
width: 18px;
height: 4px;
top: 11px;
left: -7px;
border-top: 2px solid var(--amend);
transform: rotate(-45deg);
}
.masthead-mark::after {
width: 8px;
height: 8px;
right: -4px;
bottom: -4px;
}
.masthead-text {
display: flex;
align-items: baseline;
gap: 0.45rem;
}
.masthead strong {
font-family: 'Newsreader', Georgia, serif;
font-size: 1.55rem;
letter-spacing: -0.03em;
}
.masthead small {
color: var(--muted);
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
nav {
display: flex;
align-items: center;
gap: 1.25rem;
}
nav a {
font-size: 0.8rem;
font-weight: 600;
text-decoration: none;
}
nav .rss {
padding-left: 1rem;
border-left: 1px solid var(--rule);
font-family: 'IBM Plex Mono', monospace;
color: var(--blue);
}
@media (max-width: 900px) {
.header-inner {
align-items: flex-start;
flex-direction: column;
padding-block: 1rem;
gap: 0.7rem;
}
nav {
width: 100%;
gap: 1rem;
padding-bottom: 0.1rem;
overflow-x: auto;
}
nav a {
white-space: nowrap;
}
}
@media (max-width: 600px) {
nav {
flex-wrap: wrap;
gap: 0.45rem 0.9rem;
overflow: visible;
}
nav a {
font-size: 0.7rem;
}
nav .rss {
display: none;
}
}
</style>
+109
View File
@@ -0,0 +1,109 @@
---
import { formatDate } from '../lib/content';
import { site } from '../site.config';
interface Props {
author: string;
maintainers: string[];
published: Date;
reviewed: Date;
status: 'developing' | 'disputed' | 'resolved' | 'historical';
sourcePath: string;
}
const { author, maintainers, published, reviewed, status, sourcePath } = Astro.props;
---
<dl class="metadata" aria-label="Publication and maintenance details">
<div>
<dt>By</dt>
<dd>{author}</dd>
</div>
<div>
<dt>Published</dt>
<dd><time datetime={published.toISOString()}>{formatDate(published)}</time></dd>
</div>
<div>
<dt>Last reviewed</dt>
<dd><time datetime={reviewed.toISOString()}>{formatDate(reviewed)}</time></dd>
</div>
<div>
<dt>Maintained by</dt>
<dd>{maintainers.join(', ')}</dd>
</div>
<div>
<dt>Status</dt>
<dd><span class:list={['status', `status-${status}`]}>{status}</span></dd>
</div>
<div>
<dt>Record</dt>
<dd><a href={`${site.repositoryUrl}/src/branch/main/${sourcePath}`}>Source file ↗</a></dd>
</div>
</dl>
<style>
.metadata {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 2.25rem 0 0;
padding: 1rem 0;
border-top: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
gap: 1rem 1.5rem;
}
dt {
color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
dd {
margin: 0.15rem 0 0;
font-size: 0.82rem;
font-weight: 600;
}
.status {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.status::before {
width: 0.55rem;
height: 0.55rem;
border: 1px solid currentColor;
border-radius: 50%;
content: '';
}
.status-developing::before {
background: var(--blue-soft);
color: var(--blue);
}
.status-disputed {
color: var(--amend);
}
.status-disputed::before {
background: var(--amend);
}
.status-resolved::before {
background: var(--ink);
}
.status-historical {
color: var(--muted);
}
@media (max-width: 620px) {
.metadata {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
+61
View File
@@ -0,0 +1,61 @@
---
import { recentMaintenance } from '../lib/git';
import { site } from '../site.config';
const commits = recentMaintenance();
---
<section class="maintenance" aria-labelledby="maintenance-title">
<div class="section-rule">
<h2 class="section-title" id="maintenance-title">Recently maintained</h2>
<a href={`${site.repositoryUrl}/commits/branch/main`}>Full record ↗</a>
</div>
{commits.length > 0 ? (
<ol>
{commits.map((commit) => (
<li>
<time datetime={commit.date}>{commit.date}</time>
<span>{commit.subject}</span>
<code>{commit.hash}</code>
</li>
))}
</ol>
) : (
<p class="empty-state">The maintenance ledger begins with the first published commit.</p>
)}
</section>
<style>
ol {
margin: 0;
padding: 0;
list-style: none;
}
li {
display: grid;
grid-template-columns: 8rem 1fr auto;
gap: 1rem;
padding: 0.9rem 0;
border-bottom: 1px solid var(--rule);
font-size: 0.85rem;
}
time,
code {
color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
}
@media (max-width: 600px) {
li {
grid-template-columns: 1fr auto;
}
li span {
grid-column: 1 / -1;
grid-row: 1;
}
}
</style>
+140
View File
@@ -0,0 +1,140 @@
---
import type { CollectionEntry } from 'astro:content';
import { formatDate } from '../lib/content';
interface Props {
entry: CollectionEntry<'stories'>;
featured?: boolean;
}
const { entry, featured = false } = Astro.props;
---
<article class:list={['story-card', { featured }] }>
<div class="visual" aria-hidden="true">
<span>PERSON</span>
<i></i>
<span>SYSTEM</span>
<i class="broken"></i>
<span>CONSEQUENCE</span>
</div>
<div class="story-copy">
<p class="eyebrow">Story · {formatDate(entry.data.published)}</p>
<h3><a href={`/stories/${entry.id}`}>{entry.data.title}</a></h3>
<p>{entry.data.description}</p>
<div class="card-foot">
<span>By {entry.data.author}</span>
<span>{entry.data.status}</span>
</div>
</div>
</article>
<style>
.story-card {
display: grid;
grid-template-columns: minmax(180px, 0.8fr) minmax(0, 1.2fr);
min-height: 280px;
border-bottom: 1px solid var(--rule);
}
.visual {
display: flex;
min-height: 230px;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 2rem;
background: var(--blue);
color: var(--paper);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.1em;
}
.visual i {
display: block;
width: 1px;
height: 2.8rem;
margin: 0.6rem 0;
background: #8ea7c1;
}
.visual .broken {
position: relative;
height: 3.4rem;
background: linear-gradient(to bottom, #8ea7c1 0 38%, transparent 38% 62%, #8ea7c1 62%);
}
.visual .broken::after {
position: absolute;
width: 0.8rem;
height: 2px;
top: 50%;
left: -0.38rem;
background: #ef765e;
content: '';
transform: rotate(-45deg);
}
.story-copy {
display: flex;
flex-direction: column;
padding: 2rem;
border-top: 1px solid var(--rule);
}
h3 {
max-width: 17ch;
margin: 0;
font-family: 'Newsreader', Georgia, serif;
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 600;
letter-spacing: -0.03em;
line-height: 0.98;
}
h3 a {
text-decoration: none;
}
.story-copy > p:not(.eyebrow) {
max-width: 52ch;
color: var(--muted);
}
.card-foot {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-top: auto;
padding-top: 1.25rem;
color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
@media (max-width: 650px) {
.story-card {
grid-template-columns: 1fr;
}
.visual {
min-height: 190px;
flex-direction: row;
}
.visual i {
width: 2.5rem;
height: 1px;
margin: 0 0.65rem;
}
.visual .broken {
width: 3rem;
height: 1px;
background: linear-gradient(to right, #8ea7c1 0 38%, transparent 38% 62%, #8ea7c1 62%);
}
}
</style>
+74
View File
@@ -0,0 +1,74 @@
---
interface Props {
pattern?: {
claim: string;
trusted: string;
failure: string;
};
}
const { pattern } = Astro.props;
---
{pattern && (
<details class="trust-pattern">
<summary>The trust pattern</summary>
<dl>
<div><dt>Claim</dt><dd>{pattern.claim}</dd></div>
<div><dt>Who must be trusted</dt><dd>{pattern.trusted}</dd></div>
<div><dt>What can fail</dt><dd>{pattern.failure}</dd></div>
</dl>
</details>
)}
<style>
.trust-pattern {
width: min(100%, var(--reading));
margin-top: 3.5rem;
border-top: 1px solid var(--ink);
border-bottom: 1px solid var(--ink);
}
summary {
padding: 1rem 0;
cursor: pointer;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.76rem;
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
}
dl {
display: grid;
gap: 0;
margin: 0 0 1rem;
}
dl div {
display: grid;
grid-template-columns: 10rem 1fr;
gap: 1rem;
padding: 0.85rem 0;
border-top: 1px solid var(--rule);
}
dt {
color: var(--muted);
font-size: 0.75rem;
font-weight: 600;
}
dd {
margin: 0;
font-family: 'Newsreader', Georgia, serif;
font-size: 1.08rem;
}
@media (max-width: 560px) {
dl div {
grid-template-columns: 1fr;
gap: 0.2rem;
}
}
</style>
+72
View File
@@ -0,0 +1,72 @@
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const status = z.enum(['developing', 'disputed', 'resolved', 'historical']);
const maintenance = {
author: z.string().default('Ana'),
maintainers: z.array(z.string()).default(['Ana']),
reviewed: z.coerce.date(),
status: status.default('developing'),
draft: z.boolean().default(false),
};
const stories = defineCollection({
loader: glob({ base: './src/content/stories', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
description: z.string(),
published: z.coerce.date(),
featured: z.boolean().default(false),
tags: z.array(z.string()).default([]),
image: z.string().optional(),
imageAlt: z.string().optional(),
trustPattern: z
.object({
claim: z.string(),
trusted: z.string(),
failure: z.string(),
})
.optional(),
...maintenance,
}),
});
const briefings = defineCollection({
loader: glob({ base: './src/content/briefings', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
description: z.string(),
issue: z.number().int().nonnegative(),
published: z.coerce.date(),
...maintenance,
}),
});
const guides = defineCollection({
loader: glob({ base: './src/content/guides', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
description: z.string(),
firstPublished: z.coerce.date(),
tags: z.array(z.string()).default([]),
...maintenance,
}),
});
const episodes = defineCollection({
loader: glob({ base: './src/content/episodes', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
description: z.string(),
episode: z.number().int().positive(),
published: z.coerce.date(),
duration: z.string().optional(),
audioUrl: z.url().optional(),
guests: z.array(z.string()).default([]),
...maintenance,
}),
});
export const collections = { stories, briefings, guides, episodes };
@@ -0,0 +1,25 @@
---
title: "Before the first issue"
description: "The shape of a weekly briefing designed to become a useful historical record rather than another feed."
issue: 0
published: 2026-07-14
reviewed: 2026-07-14
author: "Ana"
maintainers:
- "Ana"
status: developing
draft: false
---
This is issue zero: a description of the promise, not a simulated week of news.
Each real issue will contain:
1. **Three failures.** Incidents in which trust, identity, recovery, delegation, or authority broke in a consequential way.
2. **Two new systems.** Products, policies, protocols, or institutions that change who is being asked to trust whom.
3. **One government overreach.** Specific and evidenced, not selected merely to satisfy a political mood.
4. **One surprisingly good idea.** A design, safeguard, policy, or act of institutional competence worth preserving.
Annotations will state why an item matters. Links without judgement belong in a search engine, not in an authored briefing.
The issue will be allowed to be short. It will not be allowed to pretend that a thin week was a profound one.
+24
View File
@@ -0,0 +1,24 @@
---
title: "Episode title"
description: "A precise, human description of the conversation."
episode: 1
published: 2026-07-14
reviewed: 2026-07-14
author: "Ana"
maintainers:
- "Ana"
status: developing
draft: true
duration: "00:00"
guests: []
---
> Draft episode template. Add the audio URL in frontmatter only after the durable feed location is known.
## Listen
## What we discussed
## Transcript
## Sources and further reading
+38
View File
@@ -0,0 +1,38 @@
---
title: "Passkeys"
description: "What passkeys are, who participates in recovery, and what changes when a password disappears."
firstPublished: 2026-07-14
reviewed: 2026-07-14
author: "Ana"
maintainers:
- "Ana"
status: developing
draft: true
tags:
- identity
- authentication
---
> Draft field-guide template. Do not publish until every section has been sourced and reviewed.
## What it is
## Why people care
## How it works, without the marketing
## Who must trust whom
## What it knows about you
## What happens when you lose access
## The strongest argument for it
## The strongest argument against it
## Known incidents and disputes
## What changed recently
## Maintainers and contributors
@@ -0,0 +1,45 @@
---
title: "What Trust Issues is for"
description: "A publication about authority that remains curious about systems and loyal to the people who must live inside them."
published: 2026-07-14
reviewed: 2026-07-14
author: "Ana"
maintainers:
- "Ana"
status: developing
featured: true
draft: false
tags:
- trust
- editorial
trustPattern:
claim: "Inspectability can make a publication more worthy of trust."
trusted: "Readers must still trust the author's judgement, sourcing, and willingness to correct the record."
failure: "Visible machinery can become theatre if review dates, challenges, and corrections are not honoured."
---
Every architecture diagram conceals a governance diagram. Somebody chooses the defaults, controls recovery, interprets the exception, or bears the loss when the elegant path stops working.
This publication follows those choices.
It begins with a friendly question: when a government, company, protocol, or autonomous system says something is verified, private, safe, self-sovereign, or trustless, **what does that mean here?**
That question is not an accusation. Sometimes the system is better than what came before. Sometimes a compromise is reasonable once it is named. Sometimes the marketing claim survives contact with reality. The point is to look.
## Authored, not anonymous
This is an independent publication by Ana. The writing should never hide behind the voice of an institution that does not yet exist.
Stories identify their author and, separately, their maintainer. Substantial corrections are visible. Review dates mean a human being performed a review. The public repository provides provenance, not a certificate of truth.
## People before machinery
Identity systems are usually described from the centre: issuers, wallets, verifiers, protocols, platforms. Their character is often clearest at the edge: the replaced phone, the changed name, the undocumented worker, the coercive partner, the delegated agent that acted beyond what its operator understood.
Technical systems matter. So do the lives they reorganise.
## A useful archive
The ambition is modest to state and difficult to earn: publish consistently, explain systems plainly, correct the record honestly, and leave behind an archive that becomes more useful with age.
No badge can establish that in advance. The work has to do it.
+77
View File
@@ -0,0 +1,77 @@
---
import MaintenanceMeta from '../components/MaintenanceMeta.astro';
import TrustPattern from '../components/TrustPattern.astro';
import BaseLayout from './BaseLayout.astro';
interface Props {
title: string;
description: string;
kind: string;
author: string;
maintainers: string[];
published: Date;
reviewed: Date;
status: 'developing' | 'disputed' | 'resolved' | 'historical';
sourcePath: string;
trustPattern?: {
claim: string;
trusted: string;
failure: string;
};
}
const props = Astro.props;
---
<BaseLayout title={props.title} description={props.description}>
<article class="shell article">
<header class="article-head">
<p class="eyebrow">{props.kind}</p>
<h1>{props.title}</h1>
<p class="dek">{props.description}</p>
<MaintenanceMeta
author={props.author}
maintainers={props.maintainers}
published={props.published}
reviewed={props.reviewed}
status={props.status}
sourcePath={props.sourcePath}
/>
</header>
<div class="article-body prose">
<slot />
</div>
<TrustPattern pattern={props.trustPattern} />
</article>
</BaseLayout>
<style>
.article {
padding-top: clamp(3.5rem, 8vw, 7rem);
}
.article-head {
max-width: 980px;
}
h1 {
max-width: 18ch;
margin: 0;
font-family: 'Newsreader', Georgia, serif;
font-size: clamp(3rem, 8vw, 6.8rem);
font-weight: 500;
letter-spacing: -0.05em;
line-height: 0.92;
}
.dek {
max-width: 65ch;
margin: 1.6rem 0 0;
color: var(--muted);
font-size: clamp(1.05rem, 2vw, 1.3rem);
}
.article-body {
margin-top: clamp(3rem, 7vw, 6rem);
}
</style>
+53
View File
@@ -0,0 +1,53 @@
---
import Footer from '../components/Footer.astro';
import Header from '../components/Header.astro';
import { site } from '../site.config';
import '../styles/global.css';
interface Props {
title?: string;
description?: string;
}
const { title, description = site.description } = Astro.props;
const pageTitle = title ? `${title} — ${site.title}` : `${site.title} ${site.qualifier}`;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="description" content={description} />
<meta name="generator" content={Astro.generator} />
<meta name="theme-color" content="#f5f1e8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate" type="application/rss+xml" title={site.title} href="/rss.xml" />
<title>{pageTitle}</title>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<Header />
<main id="main">
<slot />
</main>
<Footer />
</body>
</html>
<style>
.skip-link {
position: fixed;
z-index: 100;
top: 0.75rem;
left: 0.75rem;
padding: 0.65rem 0.8rem;
background: var(--ink);
color: var(--paper);
transform: translateY(-200%);
}
.skip-link:focus {
transform: translateY(0);
}
</style>
+28
View File
@@ -0,0 +1,28 @@
import type { CollectionEntry } from 'astro:content';
type DatedEntry =
| CollectionEntry<'stories'>
| CollectionEntry<'briefings'>
| CollectionEntry<'episodes'>;
export function newestFirst<T extends DatedEntry>(entries: T[]): T[] {
return [...entries].sort(
(a, b) => b.data.published.valueOf() - a.data.published.valueOf(),
);
}
export function guidesByReview(
entries: CollectionEntry<'guides'>[],
): CollectionEntry<'guides'>[] {
return [...entries].sort(
(a, b) => b.data.reviewed.valueOf() - a.data.reviewed.valueOf(),
);
}
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric',
}).format(date);
}
+33
View File
@@ -0,0 +1,33 @@
import { execFileSync } from 'node:child_process';
export interface MaintenanceCommit {
hash: string;
date: string;
subject: string;
}
export function recentMaintenance(limit = 4): MaintenanceCommit[] {
try {
const output = execFileSync(
'git',
[
'log',
`-${limit}`,
'--date=short',
'--pretty=format:%h%x09%ad%x09%s',
],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
);
return output
.trim()
.split('\n')
.filter(Boolean)
.map((line: string) => {
const [hash, date, ...subject] = line.split('\t');
return { hash, date, subject: subject.join(' ') };
});
} catch {
return [];
}
}
+36
View File
@@ -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>
+43
View File
@@ -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>
+27
View File
@@ -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>
+36
View File
@@ -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>
+27
View File
@@ -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>
+39
View File
@@ -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>
+407
View File
@@ -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>
+32
View File
@@ -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>
+54
View File
@@ -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>
+40
View File
@@ -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>',
});
}
+28
View File
@@ -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>
+25
View File
@@ -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>
+32
View File
@@ -0,0 +1,32 @@
export const site = {
title: 'Trust Issues',
qualifier: 'by Ana',
description: 'Power, identity, and the systems acting in our name.',
proposition:
'Technology keeps asking us to trust people and systems we cannot see.',
author: {
name: 'Ana',
note: 'An independent publication by Ana.',
},
repositoryUrl: 'https://git.frrn.life/ana/trust-issues',
newsletterUrl: '',
podcastFeedUrl: '',
contactUrl: '',
book: {
enabled: false,
title: '',
description: '',
cover: '',
sampleUrl: '',
buyUrl: '',
},
} as const;
export const navigation = [
{ href: '/stories', label: 'Stories' },
{ href: '/briefing', label: 'Briefing' },
{ href: '/podcast', label: 'Podcast' },
{ href: '/guides', label: 'Guides' },
{ href: '/book', label: 'Book' },
{ href: '/about', label: 'About' },
] as const;
+213
View File
@@ -0,0 +1,213 @@
:root {
--paper: #f5f1e8;
--paper-raised: #fbf9f4;
--ink: #171716;
--muted: #66645f;
--rule: #cbc6bb;
--blue: #153d6f;
--blue-soft: #dce6ef;
--amend: #b74432;
--max: 1180px;
--reading: 720px;
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-synthesis: none;
background: var(--paper);
color: var(--ink);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-width: 320px;
background:
linear-gradient(rgba(23, 23, 22, 0.025) 1px, transparent 1px) 0 0 / 100% 32px,
var(--paper);
color: var(--ink);
line-height: 1.6;
}
a {
color: inherit;
text-underline-offset: 0.18em;
text-decoration-thickness: 1px;
}
a:hover {
color: var(--blue);
}
img {
display: block;
max-width: 100%;
}
button,
input,
textarea,
select {
font: inherit;
}
.shell {
width: min(calc(100% - 40px), var(--max));
margin-inline: auto;
}
.eyebrow {
margin: 0 0 0.75rem;
color: var(--blue);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.74rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.display {
margin: 0;
font-family: 'Newsreader', Georgia, serif;
font-size: clamp(2.8rem, 7vw, 6.5rem);
font-weight: 500;
letter-spacing: -0.045em;
line-height: 0.94;
}
.section-title {
margin: 0;
font-family: 'Newsreader', Georgia, serif;
font-size: clamp(2rem, 4vw, 3.25rem);
font-weight: 600;
letter-spacing: -0.025em;
line-height: 1;
}
.section-rule {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
padding-bottom: 0.9rem;
border-bottom: 1px solid var(--ink);
}
.section-rule a {
font-size: 0.85rem;
font-weight: 600;
text-decoration: none;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0.7rem 1.05rem;
border: 1px solid var(--ink);
border-radius: 999px;
background: transparent;
font-size: 0.88rem;
font-weight: 600;
text-decoration: none;
transition: background 150ms ease, color 150ms ease, transform 150ms ease;
}
.button:hover {
background: var(--ink);
color: var(--paper);
transform: translateY(-1px);
}
.button.primary {
background: var(--ink);
color: var(--paper);
}
.button.primary:hover {
background: var(--blue);
}
.prose {
width: min(100%, var(--reading));
font-family: 'Newsreader', Georgia, serif;
font-size: clamp(1.15rem, 2.2vw, 1.35rem);
line-height: 1.7;
}
.prose h2,
.prose h3 {
margin-top: 2.4em;
margin-bottom: 0.65em;
line-height: 1.08;
}
.prose h2 {
font-size: 1.9em;
}
.prose h3 {
font-size: 1.4em;
}
.prose blockquote {
margin: 2rem 0;
padding-left: 1.4rem;
border-left: 4px solid var(--amend);
font-style: italic;
}
.prose code {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82em;
}
.empty-state {
padding: 2rem;
border: 1px dashed var(--rule);
color: var(--muted);
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 700px) {
.shell {
width: min(calc(100% - 28px), var(--max));
}
.display {
font-size: clamp(2.8rem, 15vw, 4.8rem);
}
}
@media print {
body {
background: white;
}
header,
footer,
.no-print {
display: none !important;
}
a {
text-decoration: none;
}
}