This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
SITE_URL=https://your-domain.example
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Challenge a passage
|
||||||
|
about: Supply evidence or context about a specific published passage
|
||||||
|
title: "Challenge: "
|
||||||
|
labels:
|
||||||
|
- editorial-challenge
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Do not include private correspondence, personal data, confidential documents, or source identities. Use the private contact route for sensitive material. -->
|
||||||
|
|
||||||
|
## Published item and exact passage
|
||||||
|
|
||||||
|
Link to the item and quote only the passage being challenged.
|
||||||
|
|
||||||
|
## Type
|
||||||
|
|
||||||
|
- [ ] Source needed
|
||||||
|
- [ ] Source does not support the claim
|
||||||
|
- [ ] Material context missing
|
||||||
|
- [ ] Potential conflict of interest
|
||||||
|
- [ ] Outdated
|
||||||
|
- [ ] Subject disputes the account
|
||||||
|
- [ ] Technical description may be incorrect
|
||||||
|
|
||||||
|
## Objection
|
||||||
|
|
||||||
|
Explain what is wrong, incomplete, or liable to mislead.
|
||||||
|
|
||||||
|
## Supporting basis
|
||||||
|
|
||||||
|
Link primary sources where possible. First-hand testimony is welcome; describe what you directly know and what you are inferring.
|
||||||
|
|
||||||
|
## Publication and safety
|
||||||
|
|
||||||
|
State whether your name and submission may be quoted publicly. If disclosure could create risk, stop and use the private contact route instead.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: validate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Validate content and build
|
||||||
|
run: npm run validate
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
The publication is authored first and collaborative second. Contributions should improve a specific piece of work rather than turn the repository into a general forum.
|
||||||
|
|
||||||
|
Useful contributions include:
|
||||||
|
|
||||||
|
- a primary source that materially supports or challenges a passage;
|
||||||
|
- a technical correction with a reproducible explanation;
|
||||||
|
- missing jurisdictional or historical context;
|
||||||
|
- first-hand experience, with clear limits on what may be published;
|
||||||
|
- an accessibility, rendering, or archive defect.
|
||||||
|
|
||||||
|
For editorial challenges, use the challenge template when public issues are enabled. Quote the exact passage and explain the objection. Do not publish personal data, confidential documents, private correspondence, or source identities in an issue.
|
||||||
|
|
||||||
|
For code changes, keep the site static, accessible, inspectable, and inexpensive to operate. Run `npm run validate` before opening a pull request.
|
||||||
|
|
||||||
|
General ideological arguments, promotional submissions, and vendor-comparison pitches are out of scope.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Editorial commitments
|
||||||
|
|
||||||
|
This document is public because the publication should be judged against promises a reader can inspect. It is a working policy, not a claim of infallibility.
|
||||||
|
|
||||||
|
## Voice
|
||||||
|
|
||||||
|
We ask one recurring question with cheerful suspicion: when a system claims to be verified, trustless, sovereign, private, safe, or autonomous, what does that mean in practice and where did the trust move?
|
||||||
|
|
||||||
|
Stories begin with people and consequences. Technical machinery is explained in plain English. Jargon is defined when it is first used. Outrage is reserved for what the evidence supports.
|
||||||
|
|
||||||
|
## Authorship and maintenance
|
||||||
|
|
||||||
|
An author and a maintainer are different roles. Every published item identifies its author. Maintained material also names the person responsible for review.
|
||||||
|
|
||||||
|
Git records provenance and revision history. It does not certify truth, fairness, completeness, or independence. Those remain editorial responsibilities.
|
||||||
|
|
||||||
|
## Corrections and challenges
|
||||||
|
|
||||||
|
Corrections are attached to the relevant passage, dated, and described plainly. Substantive changes are not silently folded into a story.
|
||||||
|
|
||||||
|
Public challenges should identify the passage, objection, and supporting basis. First-hand testimony can be evidence. Sensitive material must use a private channel rather than a public issue.
|
||||||
|
|
||||||
|
An article-level `disputed` status is used only when the dispute materially changes how the whole article should be read. A challenge to one passage does not automatically label an entire story disputed.
|
||||||
|
|
||||||
|
## Sources and safety
|
||||||
|
|
||||||
|
This public repository never contains confidential source identities, unpublished interview notes, private contact information, embargoed drafts, or documents whose publication could endanger a source. Public source links belong in the relevant article; protected reporting material belongs elsewhere.
|
||||||
|
|
||||||
|
## Conflicts and commercial boundaries
|
||||||
|
|
||||||
|
Relevant financial, professional, and personal conflicts are disclosed. Sponsorship never buys inclusion, favourable treatment, access to corrections, or influence over review status.
|
||||||
|
|
||||||
|
## Review dates
|
||||||
|
|
||||||
|
A `last reviewed` date means a named maintainer genuinely checked the material on that date. It is not updated mechanically to make an archive appear fresh.
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# Licence
|
||||||
|
|
||||||
|
## Site code
|
||||||
|
|
||||||
|
Copyright (c) 2026 Ana
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of the software and associated documentation files in this repository, excluding editorial content, to deal in the software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software, subject to inclusion of this copyright notice and permission notice.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
## Editorial content
|
||||||
|
|
||||||
|
Unless an individual file states otherwise, articles, briefings, guides, podcast transcripts, original illustrations, and photographs are copyright Ana. All rights reserved.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Trust Issues
|
||||||
|
|
||||||
|
The public, content-first foundation for Ana's independent publication about power, identity, and the systems acting in our name.
|
||||||
|
|
||||||
|
`Trust Issues` is a working title. The masthead, domain, podcast title, and book details remain deliberately configurable; the archive and URLs are structured so the publication can be renamed without rewriting its editorial model.
|
||||||
|
|
||||||
|
## What exists now
|
||||||
|
|
||||||
|
- An authored, responsive homepage rather than a product dashboard.
|
||||||
|
- Typed Markdown collections for stories, weekly briefings, field guides, and podcast episodes.
|
||||||
|
- Article metadata for author, maintainer, publication date, review date, status, and trust pattern.
|
||||||
|
- Story, briefing, guide, podcast, book, and about surfaces.
|
||||||
|
- RSS, accessible HTML, print styles, and graceful no-JavaScript operation.
|
||||||
|
- A quiet recent-maintenance ledger derived from Git at build time.
|
||||||
|
- Editorial, contribution, source-safety, and challenge policies.
|
||||||
|
- Gitea Actions validation on every push and pull request.
|
||||||
|
|
||||||
|
## Start locally
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate the same way CI does:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run validate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Where to edit
|
||||||
|
|
||||||
|
- Publication identity and external links: `src/site.config.ts`
|
||||||
|
- Stories: `src/content/stories/`
|
||||||
|
- Weekly briefing: `src/content/briefings/`
|
||||||
|
- Field guides: `src/content/guides/`
|
||||||
|
- Podcast episodes and transcripts: `src/content/episodes/`
|
||||||
|
- Editorial promises: `EDITORIAL.md`
|
||||||
|
- Roadmap and explicit non-goals: `ROADMAP.md`
|
||||||
|
|
||||||
|
Draft entries use `draft: true` and are excluded from public routes, the homepage, and RSS.
|
||||||
|
|
||||||
|
## Publishing model
|
||||||
|
|
||||||
|
The generated site is static. Set `SITE_URL` in the deployment environment and run `npm run build`; deploy `dist/` to Cloudflare Pages or any static host.
|
||||||
|
|
||||||
|
Published content belongs in this public repository. Reporting notes, unpublished interviews, confidential sources, contact information, and embargoed drafts do not.
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Site code is MIT licensed. Editorial content is copyright Ana unless an individual page states otherwise. See `LICENSE.md`.
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
The roadmap protects the editorial heartbeat from product sprawl.
|
||||||
|
|
||||||
|
## Now: authored foundation
|
||||||
|
|
||||||
|
- Establish the masthead, author identity, domain, and book details.
|
||||||
|
- Publish the weekly briefing and a sustainable original-story cadence.
|
||||||
|
- Bring the existing podcast archive and transcripts home.
|
||||||
|
- Launch three genuinely useful field guides.
|
||||||
|
- Publish a clear correction log and recent-maintenance ledger.
|
||||||
|
- Build an editorial buffer before promising a public weekly rhythm.
|
||||||
|
|
||||||
|
## Next: useful archive
|
||||||
|
|
||||||
|
- Add annotated links only when the weekly workflow can sustain them.
|
||||||
|
- Render passage-level Git history without turning articles into issue trackers.
|
||||||
|
- Introduce confidential correction intake and measured public challenge handling.
|
||||||
|
- Add durable topic and relationship navigation after real reporting reveals the useful shapes.
|
||||||
|
|
||||||
|
## Later: participation
|
||||||
|
|
||||||
|
- Contextual contributor signals, never global scores.
|
||||||
|
- Small events or correspondence formats before a community platform.
|
||||||
|
- Professional briefing only after the free editorial rhythm is demonstrably sustainable.
|
||||||
|
|
||||||
|
## Explicit non-goals
|
||||||
|
|
||||||
|
- No accounts, comments, likes, follower counts, global karma, or vendor marketplace.
|
||||||
|
- No database until a proven reader need requires one.
|
||||||
|
- No public reporting notebook or confidential-source material.
|
||||||
|
- No claim that Git history makes reporting true.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Security and source safety
|
||||||
|
|
||||||
|
Do not report security vulnerabilities or disclose confidential editorial material through a public issue.
|
||||||
|
|
||||||
|
Until a dedicated private contact is configured, contact Ana through an existing trusted channel. A security email and optional encrypted contact method must be added to `src/site.config.ts` before public launch.
|
||||||
|
|
||||||
|
Never place access tokens, private keys, source identities, interview recordings, private correspondence, unpublished legal review, or embargoed documents in this repository.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
site: process.env.SITE_URL ?? 'https://example.com',
|
||||||
|
output: 'static',
|
||||||
|
trailingSlash: 'never',
|
||||||
|
});
|
||||||
Generated
+5360
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "trust-issues",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"check": "astro check",
|
||||||
|
"validate": "npm run check && npm run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/rss": "4.0.19",
|
||||||
|
"astro": "7.0.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "0.9.9",
|
||||||
|
"@types/node": "26.1.1",
|
||||||
|
"typescript": "6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<rect width="64" height="64" rx="8" fill="#151515"/>
|
||||||
|
<path d="M14 17h36v30H14z" fill="none" stroke="#f5f0e6" stroke-width="4"/>
|
||||||
|
<path d="M10 40 28 22m8 20 18-18" stroke="#c34a35" stroke-width="5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 272 B |
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user