feat(landing): Astro-Landingpage für Cardecky

Neue statische Astro 5-App in apps/landing/ (Port 4380).
Sektionen: Nav, Hero, Kartentypen-Grid (6 Typen), How-it-works,
Features, mana-e.V.-Pitch, CTA, Footer.

Stack: Astro 5 + Tailwind 3, kein MDX (overkill für MVP), keine
externen Abhängigkeiten. Forest-grüne Farbpalette passend zum App-
Theme, Serif-Headings im mana-e.V.-Stil.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-11 18:40:51 +02:00
parent 3669a86599
commit 8a56d0dcff
17 changed files with 518 additions and 0 deletions

1
.gitignore vendored
View file

@ -66,3 +66,4 @@ ssh-key-command.txt
# Volumes for local docker-compose # Volumes for local docker-compose
.volumes/ .volumes/
.local/ .local/
apps/landing/.astro

View file

@ -0,0 +1,12 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://cardecky.mana.how',
integrations: [
tailwind({ applyBaseStyles: false }),
sitemap(),
],
output: 'static',
});

21
apps/landing/package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "@cards/landing",
"type": "module",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "astro dev --port 4380",
"build": "astro build",
"preview": "astro preview --port 4380",
"astro": "astro"
},
"dependencies": {
"astro": "^5.8.0"
},
"devDependencies": {
"@astrojs/tailwind": "^6.0.2",
"@astrojs/sitemap": "^3.4.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3"
}
}

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect x="2" y="6" width="18" height="14" rx="3" stroke="#16a34a" stroke-width="2.5"/>
<rect x="12" y="12" width="18" height="14" rx="3" fill="white" stroke="#16a34a" stroke-width="2.5"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View file

@ -0,0 +1,35 @@
---
const APP_URL = 'https://cardecky.mana.how';
---
<section class="border-b border-rule bg-leaf py-20 sm:py-24">
<div class="mx-auto max-w-content px-6 text-center">
<h2 class="font-serif text-display text-white">
Bereit, das Lernen neu zu denken?
</h2>
<p class="mt-4 text-lg text-white/80">
Cardecky ist kostenlos. Keine Kreditkarte, keine versteckten Kosten.
</p>
<div class="mt-10 flex flex-wrap justify-center gap-4">
<a
href={APP_URL}
class="inline-flex items-center gap-2 rounded-lg bg-white px-6 py-3 font-medium text-leaf transition-colors hover:bg-white/90"
>
Jetzt starten
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
<a
href="https://git.mana.how/mana/cards"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 rounded-lg border border-white/30 px-6 py-3 font-medium text-white transition-colors hover:border-white/60 hover:bg-white/10"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>
</svg>
Quellcode ansehen
</a>
</div>
</div>
</section>

View file

@ -0,0 +1,74 @@
---
const types = [
{
name: 'Klassisch',
tag: 'basic',
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18"/></svg>`,
desc: 'Vorderseite und Rückseite. Der Klassiker — simpel, bewährt, effektiv für einfache Fakten.',
},
{
name: 'Multiple Choice',
tag: 'multiple-choice',
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="12" cy="12" r="9"/><path d="M12 8v4l3 3"/></svg>`,
desc: 'Vier Antwortoptionen zur Auswahl. Fehlende Distractors generiert das System automatisch aus dem Deck.',
highlight: true,
},
{
name: 'Lückentext',
tag: 'cloze',
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M4 6h16M4 12h8M4 18h12"/><rect x="13" y="10" width="7" height="4" rx="1" fill="currentColor" opacity=".15" stroke="currentColor"/></svg>`,
desc: 'Text mit {{c1::Lücken}} — jeder markierte Cluster wird als eigene Karte abgefragt.',
},
{
name: 'Bild-Okklusion',
tag: 'image-occlusion',
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor"/><path d="m21 15-5-5L5 21"/><rect x="11" y="10" width="7" height="5" rx="1" fill="currentColor" opacity=".2" stroke="currentColor"/></svg>`,
desc: 'Bereiche auf einem Bild ausblenden — ideal für Anatomie, Landkarten, Diagramme.',
},
{
name: 'Tippen',
tag: 'typing',
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="2" y="7" width="20" height="12" rx="2"/><path d="M8 11h.01M12 11h.01M16 11h.01M8 15h8"/></svg>`,
desc: 'Die Antwort eintippen statt nur aufzudecken. Fuzzy-Match verzeiht kleine Tippfehler.',
},
{
name: 'Audio',
tag: 'audio-front',
icon: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8"/></svg>`,
desc: 'Audioclip als Prompt — perfekt für Sprachlernen, Vokabeln und Aussprache-Training.',
},
] as const;
---
<section id="kartentypen" class="border-b border-rule py-20 sm:py-28">
<div class="mx-auto max-w-content px-6">
<p class="section-label">Kartentypen</p>
<h2 class="mt-3 font-serif text-display text-ink">Sechs Formate, ein System.</h2>
<p class="mt-4 max-w-prose text-muted">
Nicht jedes Wissen lässt sich gleich lernen. Cardecky hat für jeden Lerninhalt den richtigen
Kartentyp — alle im gleichen Deck, alle mit FSRS-Scheduling.
</p>
<div class="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{types.map((type) => (
<div class:list={[
'group rounded-xl border p-6 transition-all',
type.highlight
? 'border-leaf-border bg-leaf-light'
: 'border-rule bg-white hover:border-leaf-border hover:bg-leaf-light/40'
]}>
<div class:list={[
'mb-4 inline-flex rounded-lg p-2',
type.highlight ? 'bg-leaf/10 text-leaf' : 'bg-rule text-muted group-hover:bg-leaf/10 group-hover:text-leaf'
]}>
<Fragment set:html={type.icon} />
</div>
<div class="mb-1 flex items-center gap-2">
<h3 class="font-medium text-ink">{type.name}</h3>
<code class="rounded bg-rule px-1.5 py-0.5 text-xs text-muted">{type.tag}</code>
</div>
<p class="text-sm leading-relaxed text-muted">{type.desc}</p>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,57 @@
---
const features = [
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>`,
title: 'Open Source',
body: 'Der gesamte Quellcode liegt offen auf Forgejo. Keine Blackbox, kein Vendor-Lock-in.',
},
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`,
title: 'Datensouveränität',
body: 'Deine Daten liegen auf mana-Infrastruktur in Europa. Kein Google, kein AWS, keine Datenhändler.',
},
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>`,
title: 'FSRS-Algorithmus',
body: 'Free Spaced Repetition Scheduler — nachweislich effektiver als SM-2, das Herzstück von Anki.',
},
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`,
title: 'Anki-Import',
body: '.apkg-Dateien direkt importieren — Vorlagen werden automatisch auf Cardecky-Kartentypen gemappt.',
},
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>`,
title: 'Öffentliche Bibliothek',
body: 'Kuratierte Decks zu Periodensystem, Geografie, Sprachen, Philosophie und mehr — ein Klick zum Start.',
},
{
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
title: 'Föderiert im mana-Ökosystem',
body: 'Cardecky teilt Identität und Credits mit den anderen mana-Apps — ein Login, eine Community.',
},
] as const;
---
<section id="funktionen" class="border-b border-rule py-20 sm:py-28">
<div class="mx-auto max-w-content px-6">
<p class="section-label">Funktionen</p>
<h2 class="mt-3 font-serif text-display text-ink">Gebaut für ernsthaftes Lernen.</h2>
<p class="mt-4 max-w-prose text-muted">
Kein Feature-Bloat. Nur was du brauchst, um Wissen dauerhaft zu verankern.
</p>
<div class="mt-12 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{features.map((f) => (
<div class="flex gap-4">
<div class="mt-0.5 flex-shrink-0 text-leaf">
<Fragment set:html={f.icon} />
</div>
<div>
<h3 class="font-semibold text-ink">{f.title}</h3>
<p class="mt-1 text-sm leading-relaxed text-muted">{f.body}</p>
</div>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,40 @@
---
const year = new Date().getFullYear();
---
<footer class="bg-ink py-12 text-white/60">
<div class="mx-auto max-w-content px-6">
<div class="grid gap-8 sm:grid-cols-3">
<div>
<p class="font-serif text-base font-semibold text-white">Cardecky</p>
<p class="mt-2 text-sm leading-relaxed">
Spaced-Repetition-App des mana e.V.<br/>
Freie Software für freie Menschen.
</p>
</div>
<div>
<p class="text-xs font-semibold uppercase tracking-widest text-white/40">App</p>
<ul class="mt-3 space-y-2 text-sm">
<li><a href="https://cardecky.mana.how" class="hover:text-white transition-colors">Zur App</a></li>
<li><a href="https://cardecky.mana.how/decks" class="hover:text-white transition-colors">Bibliothek</a></li>
<li><a href="https://cardecky.mana.how/study" class="hover:text-white transition-colors">Lernen</a></li>
</ul>
</div>
<div>
<p class="text-xs font-semibold uppercase tracking-widest text-white/40">Verein</p>
<ul class="mt-3 space-y-2 text-sm">
<li><a href="https://mana-ev.ch" class="hover:text-white transition-colors" target="_blank" rel="noopener">mana e.V.</a></li>
<li><a href="https://mana-ev.ch#mitglied" class="hover:text-white transition-colors" target="_blank" rel="noopener">Mitglied werden</a></li>
<li><a href="https://git.mana.how/mana/cards" class="hover:text-white transition-colors" target="_blank" rel="noopener">Quellcode</a></li>
<li><a href="https://mana-ev.ch/datenschutz" class="hover:text-white transition-colors" target="_blank" rel="noopener">Datenschutz</a></li>
</ul>
</div>
</div>
<div class="mt-10 border-t border-white/10 pt-6 text-xs">
© {year} mana e.V. · Cardecky ist freie Software (AGPL-3.0) ·
<a href="https://mana-ev.ch" class="hover:text-white transition-colors">mana-ev.ch</a>
</div>
</div>
</footer>

View file

@ -0,0 +1,43 @@
---
const APP_URL = 'https://cardecky.mana.how';
---
<section class="relative overflow-hidden border-b border-rule bg-paper py-24 sm:py-32">
<!-- Subtle grid background -->
<div
class="pointer-events-none absolute inset-0 opacity-[0.03]"
style="background-image: linear-gradient(#111812 1px, transparent 1px), linear-gradient(90deg, #111812 1px, transparent 1px); background-size: 48px 48px;"
aria-hidden="true"
></div>
<div class="relative mx-auto max-w-content px-6">
<div class="max-w-prose">
<p class="section-label mb-4">Spaced Repetition · mana e.V.</p>
<h1 class="font-serif text-hero text-ink">
Lernkarten, die<br/>
<span class="text-leaf">tatsächlich wirken.</span>
</h1>
<p class="mt-6 text-lg leading-relaxed text-muted">
Cardecky nutzt den FSRS-Algorithmus — den aktuellen Stand der Wissenschaft im Spaced
Repetition — um dir jede Karte genau dann zu zeigen, wenn du sie kurz vor dem Vergessen bist.
Keine Abos, keine Tracker, keine dark patterns.
Freie Software des <a href="https://mana-ev.ch" class="underline underline-offset-2 hover:text-ink transition-colors">mana e.V.</a>
</p>
<div class="mt-10 flex flex-wrap items-center gap-4">
<a href={APP_URL} class="btn-primary">
Kostenlos starten
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
<a href="#kartentypen" class="btn-ghost">Alle Kartentypen ansehen</a>
</div>
<p class="mt-6 text-xs text-muted">
Kein Account erforderlich für den ersten Blick · DSGVO-konform · Open Source
</p>
</div>
</div>
</section>

View file

@ -0,0 +1,35 @@
---
const steps = [
{
n: '01',
title: 'Deck erstellen oder importieren',
body: 'Leg ein neues Deck an und füge Karten über das Web-Interface hinzu — oder importiere deine bestehenden Anki-Decks direkt als .apkg-Datei. Decks aus der Bibliothek mit einem Klick übernehmen.',
},
{
n: '02',
title: 'Karten anlegen',
body: 'Wähle den passenden Kartentyp und füll die Felder aus. Für Multiple-Choice-Karten zieht das System automatisch passende Distractors aus dem restlichen Deck — du gibst nur die richtige Antwort vor.',
},
{
n: '03',
title: 'Täglich kurz lernen',
body: 'Der FSRS-Algorithmus berechnet für jede Karte den optimalen Wiederholungszeitpunkt. Du bewertest dich selbst (Nochmal / Schwer / Gut / Leicht) — damit kalibriert sich das System auf deinen persönlichen Vergessenskurve.',
},
] as const;
---
<section class="border-b border-rule bg-white py-20 sm:py-28">
<div class="mx-auto max-w-content px-6">
<p class="section-label">So funktioniert's</p>
<h2 class="mt-3 font-serif text-display text-ink">In drei Schritten zur täglichen Praxis.</h2>
<div class="mt-12 grid gap-10 sm:grid-cols-3">
{steps.map((s) => (
<div>
<span class="font-serif text-4xl font-bold text-rule">{s.n}</span>
<h3 class="mt-4 font-semibold text-ink">{s.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-muted">{s.body}</p>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,46 @@
---
---
<section class="border-b border-rule bg-white py-20 sm:py-24">
<div class="mx-auto max-w-content px-6">
<div class="grid gap-12 lg:grid-cols-2 lg:items-center">
<div>
<p class="section-label">mana e.V.</p>
<h2 class="mt-3 font-serif text-display text-ink">
Software als Gemeingut.
</h2>
<p class="mt-4 leading-relaxed text-muted">
Cardecky ist ein Projekt des <strong class="text-ink">mana e.V.</strong> — einem Schweizer Verein,
der digitale Infrastruktur als Gemeingut begreift. Keine Investoren, keine Werbung,
kein Daten-Harvesting. Finanziert durch freiwillige Mitgliedsbeiträge.
</p>
<p class="mt-3 leading-relaxed text-muted">
80 % der Einnahmen fließen direkt in den Betrieb der Apps. 10 % in Langlebigkeit
und Nachhaltigkeit. 10 % in den Vereinsbetrieb. Transparenz ist kein Marketing —
wir veröffentlichen jeden Quartal einen Transparenzbericht.
</p>
<div class="mt-8 flex flex-wrap gap-4">
<a href="https://mana-ev.ch" target="_blank" rel="noopener" class="btn-primary">
mana-ev.ch besuchen
</a>
<a href="https://mana-ev.ch#mitglied" target="_blank" rel="noopener" class="btn-ghost">
Mitglied werden
</a>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
{[
{ label: 'Open Source', desc: 'Quellcode liegt offen auf git.mana.how' },
{ label: 'Kein Tracking', desc: 'Keine Werbung, keine externen Dienste' },
{ label: 'Europa', desc: 'Daten auf Infrastruktur in Europa' },
{ label: 'Transparent', desc: 'Quartalsbericht zu Finanzen & Betrieb' },
].map((item) => (
<div class="rounded-xl border border-rule bg-paper p-5">
<p class="font-semibold text-ink">{item.label}</p>
<p class="mt-1 text-sm text-muted">{item.desc}</p>
</div>
))}
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,24 @@
---
const APP_URL = 'https://cardecky.mana.how';
---
<header class="sticky top-0 z-50 border-b border-rule bg-paper/90 backdrop-blur-sm">
<div class="mx-auto flex max-w-content items-center justify-between px-6 py-3">
<a href="/" class="flex items-center gap-2 font-serif text-lg font-semibold text-ink">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="2" y="4" width="14" height="10" rx="2" stroke="currentColor" stroke-width="1.75" class="text-leaf"/>
<rect x="8" y="10" width="14" height="10" rx="2" fill="white" stroke="currentColor" stroke-width="1.75"/>
</svg>
Cardecky
</a>
<nav class="hidden items-center gap-6 text-sm text-muted sm:flex">
<a href="#kartentypen" class="hover:text-ink transition-colors">Kartentypen</a>
<a href="#funktionen" class="hover:text-ink transition-colors">Funktionen</a>
<a href="https://mana-ev.ch" target="_blank" rel="noopener" class="hover:text-ink transition-colors">mana e.V.</a>
</nav>
<a href={APP_URL} class="btn-primary text-xs py-2 px-4">
Zur App →
</a>
</div>
</header>

View file

@ -0,0 +1,37 @@
---
interface Props {
title: string;
description: string;
ogImage?: string;
}
const { title, description, ogImage = '/og.png' } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site);
---
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="canonical" href={canonical} />
<title>{title}</title>
<meta name="description" content={description} />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(ogImage, Astro.site)} />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={new URL(ogImage, Astro.site)} />
</head>
<body>
<slot />
</body>
</html>

View file

@ -0,0 +1,27 @@
---
import Layout from '../layouts/Layout.astro';
import Nav from '../components/Nav.astro';
import Hero from '../components/Hero.astro';
import CardTypes from '../components/CardTypes.astro';
import HowItWorks from '../components/HowItWorks.astro';
import Features from '../components/Features.astro';
import ManaSection from '../components/ManaSection.astro';
import CTASection from '../components/CTASection.astro';
import Footer from '../components/Footer.astro';
import '../styles/base.css';
---
<Layout
title="Cardecky — Lernkarten mit FSRS-Spaced-Repetition · mana e.V."
description="Cardecky ist eine freie Spaced-Repetition-App des mana e.V. Sechs Kartentypen, FSRS-Algorithmus, Anki-Import und eine kuratierte Bibliothek — ohne Tracking, ohne Abo."
>
<Nav />
<main>
<Hero />
<CardTypes />
<HowItWorks />
<Features />
<ManaSection />
<CTASection />
</main>
<Footer />
</Layout>

View file

@ -0,0 +1,26 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
}
body {
overflow-x: hidden;
background-color: theme('colors.paper');
color: theme('colors.ink');
}
@layer components {
.btn-primary {
@apply inline-flex items-center gap-2 rounded-lg bg-leaf px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-leaf-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-leaf focus-visible:ring-offset-2;
}
.btn-ghost {
@apply inline-flex items-center gap-2 rounded-lg border border-rule px-5 py-2.5 text-sm font-medium text-ink transition-colors hover:border-muted hover:bg-rule focus-visible:outline-none;
}
.section-label {
@apply text-xs font-semibold uppercase tracking-widest text-leaf;
}
}

View file

@ -0,0 +1,31 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
ink: '#111812',
paper: '#f9faf7',
muted: '#6b7a6e',
rule: '#e2e8e0',
leaf: '#16a34a',
'leaf-hover': '#15803d',
'leaf-light': '#f0fdf4',
'leaf-border': '#bbf7d0',
},
fontFamily: {
serif: ['Georgia', 'Cambria', '"Times New Roman"', 'serif'],
sans: ['system-ui', '-apple-system', 'sans-serif'],
},
fontSize: {
hero: ['clamp(2.25rem,5vw,3.5rem)', { lineHeight: '1.1', letterSpacing: '-0.02em' }],
display: ['clamp(1.75rem,3vw,2.5rem)', { lineHeight: '1.2', letterSpacing: '-0.015em' }],
},
maxWidth: {
content: '68rem',
prose: '44rem',
},
},
},
plugins: [],
};

View file

@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}