This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user