From 68e93a1d4dda4fd125c5551d15a224986e102fba Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jun 2026 16:56:03 +0200 Subject: [PATCH] Add new font dependencies and refactor landing page to use components for improved structure --- package.json | 10 + pnpm-lock.yaml | 82 +++++ src/components/SwissLanding.astro | 156 ++++++++++ src/data/landing-content.ts | 75 +++++ src/data/legal.ts | 43 +++ src/layouts/BaseLayout.astro | 75 +++++ src/layouts/LegalLayout.astro | 49 +++ src/pages/datenschutz.astro | 209 +++++++++++++ src/pages/impressum.astro | 137 ++++++++ src/pages/index.astro | 116 +------ src/styles/site.css | 497 ++++++++++++++++++++++++++++++ 11 files changed, 1349 insertions(+), 100 deletions(-) create mode 100644 src/components/SwissLanding.astro create mode 100644 src/data/landing-content.ts create mode 100644 src/data/legal.ts create mode 100644 src/layouts/BaseLayout.astro create mode 100644 src/layouts/LegalLayout.astro create mode 100644 src/pages/datenschutz.astro create mode 100644 src/pages/impressum.astro create mode 100644 src/styles/site.css diff --git a/package.json b/package.json index ac2d4ac..6e97fa3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,16 @@ }, "dependencies": { "@fontsource-variable/geist": "^5.2.8", + "@fontsource/archivo": "^5.2.8", + "@fontsource/dm-sans": "^5.2.8", + "@fontsource/fraunces": "^5.2.9", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/ibm-plex-sans": "^5.2.8", + "@fontsource/instrument-serif": "^5.2.8", + "@fontsource/literata": "^5.2.8", + "@fontsource/newsreader": "^5.2.10", + "@fontsource/source-sans-3": "^5.2.9", + "@fontsource/syne": "^5.2.7", "@tailwindcss/vite": "^4.2.3", "astro": "^6.1.8", "class-variance-authority": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec01788..af571cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,36 @@ importers: '@fontsource-variable/geist': specifier: ^5.2.8 version: 5.2.8 + '@fontsource/archivo': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/dm-sans': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/fraunces': + specifier: ^5.2.9 + version: 5.2.9 + '@fontsource/ibm-plex-mono': + specifier: ^5.2.7 + version: 5.2.7 + '@fontsource/ibm-plex-sans': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/instrument-serif': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/literata': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/newsreader': + specifier: ^5.2.10 + version: 5.2.10 + '@fontsource/source-sans-3': + specifier: ^5.2.9 + version: 5.2.9 + '@fontsource/syne': + specifier: ^5.2.7 + version: 5.2.7 '@tailwindcss/vite': specifier: ^4.2.3 version: 4.2.3(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)) @@ -387,6 +417,36 @@ packages: '@fontsource-variable/geist@5.2.8': resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==} + '@fontsource/archivo@5.2.8': + resolution: {integrity: sha512-Fc4yuMt85MmcYooUVpYHAJRJTymPv2+J9HsMP2f5mxR/0pIWgE2V5XfgNTavPpklX2rJFprdT36iE6Rr8ds9MA==} + + '@fontsource/dm-sans@5.2.8': + resolution: {integrity: sha512-tlovG42m9ESG28WiHpLq3F5umAlm64rv0RkqTbYowRn70e9OlRr5a3yTJhrhrY+k5lftR/OFJjPzOLQzk8EfCA==} + + '@fontsource/fraunces@5.2.9': + resolution: {integrity: sha512-XDzuddBtoC7BZgZdBn6b7hsFZY2+V1hgN7yca5fBTKuHjb/lOd45a0Ji8dTUgFhPoL7RdGupo+bC2BFSt6UH8Q==} + + '@fontsource/ibm-plex-mono@5.2.7': + resolution: {integrity: sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==} + + '@fontsource/ibm-plex-sans@5.2.8': + resolution: {integrity: sha512-eztSXjDhPhcpxNIiGTgMebdLP9qS4rWkysuE1V7c+DjOR0qiezaiDaTwQE7bTnG5HxAY/8M43XKDvs3cYq6ZYQ==} + + '@fontsource/instrument-serif@5.2.8': + resolution: {integrity: sha512-s+bkz+syj2rO00Rmq9g0P+PwuLig33DR1xDR8pTWmovH1pUjwnncrFk++q9mmOex8fUQ7oW80gPpPDaw7V1MMw==} + + '@fontsource/literata@5.2.8': + resolution: {integrity: sha512-Rj3eEQeu7+yBw9ZjXxcRiKDDrhbhpCyb0b79VJVCjDF1fdgJ8gDr9C1SRFLwfi+VaHEkhJDnRGm25fq1Yk+wNg==} + + '@fontsource/newsreader@5.2.10': + resolution: {integrity: sha512-TFaYzoFhDqarUyV2yYjgZZEwT4bpaj6sGBnXSnFknQ/QB8/9LzfY6IO9+inHOX4zzPp87Z7/KuG1OI5gr91Q3A==} + + '@fontsource/source-sans-3@5.2.9': + resolution: {integrity: sha512-u3ymIq4rfmCCyB9MEw/sFR5lPVJ1yTNXmIMbUz+9kVCFIHvNtfzXOEBuvkg3Tk0zhmioPeJ28ZK5smZ7TurezQ==} + + '@fontsource/syne@5.2.7': + resolution: {integrity: sha512-OgWgLPSbieiSfdFXgMs0l/rspIFzVoDZxw8wKj15n39epRqO5+Uv36c0MXNDcw2GuK/TIBgpC9G8VAgs+bVm7A==} + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -1657,6 +1717,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -3388,6 +3449,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -4082,6 +4144,26 @@ snapshots: '@fontsource-variable/geist@5.2.8': {} + '@fontsource/archivo@5.2.8': {} + + '@fontsource/dm-sans@5.2.8': {} + + '@fontsource/fraunces@5.2.9': {} + + '@fontsource/ibm-plex-mono@5.2.7': {} + + '@fontsource/ibm-plex-sans@5.2.8': {} + + '@fontsource/instrument-serif@5.2.8': {} + + '@fontsource/literata@5.2.8': {} + + '@fontsource/newsreader@5.2.10': {} + + '@fontsource/source-sans-3@5.2.9': {} + + '@fontsource/syne@5.2.7': {} + '@hono/node-server@1.19.14(hono@4.12.14)': dependencies: hono: 4.12.14 diff --git a/src/components/SwissLanding.astro b/src/components/SwissLanding.astro new file mode 100644 index 0000000..884862e --- /dev/null +++ b/src/components/SwissLanding.astro @@ -0,0 +1,156 @@ +--- +import { + site, + services, + stack, + projects, + process, +} from "../data/landing-content"; + +function stackSlug(name: string) { + return name.toLowerCase().replace(/\s+/g, "-").replace(/\//g, ""); +} +--- + +
+
+
+

+ + matthias-meister +

+
+ +
+
+
+ +

{site.name}

+

{site.tagline}

+ + + contact + +
+

+ + {site.role} + +

+
+ +
+ +
+
+ { + services.map((service) => ( +
+

{service.title}

+

{service.description}

+
+ )) + } +
+
+
+ +
+ +
+ + + + + + + + + { + stack.map((item) => ( + + + + + )) + } + +
modulescope
{stackSlug(item.name)}{item.category.toLowerCase()}
+
+
+ +
+ +
+
+ { + projects.map((project) => ( +
+

{project.title}

+

{project.description}

+
    + {project.tags.map((tag) => ( +
  • [{tag}]
  • + ))} +
+
+ )) + } +
+
+
+ +
+ +
+
+ { + process.map((step) => ( +
+

{step.step}

+

{step.description}

+
+ )) + } +
+
+
+ +
+ +
+

+ + mailto: {site.email} +

+
+
+ + +
+
+
diff --git a/src/data/landing-content.ts b/src/data/landing-content.ts new file mode 100644 index 0000000..f5cfc0f --- /dev/null +++ b/src/data/landing-content.ts @@ -0,0 +1,75 @@ +export const site = { + name: "Matthias Meister", + role: "iOS & Fullstack Web Developer", + tagline: + "Native Apps und moderne Webprodukte — von der Idee bis zum Launch, durchdacht und wartbar.", + email: "support@matthiasmeister.dev", +} as const; + +export const services = [ + { + title: "Native iOS Apps", + description: + "SwiftUI-Apps mit klarer Architektur, sauberer State-Verwaltung und App-Store-tauglicher Qualität.", + }, + { + title: "Web Apps", + description: + "Schnelle, zugängliche Frontends mit Astro oder React — performant, responsiv und wartbar.", + }, + { + title: "Fullstack", + description: + "APIs, Datenbanken und Deployment aus einer Hand. Convex, Supabase oder klassische Backends.", + }, +] as const; + +export const stack = [ + { name: "Swift / SwiftUI", category: "Mobile" }, + { name: "Astro", category: "Web" }, + { name: "React", category: "Web" }, + { name: "TypeScript", category: "Language" }, + { name: "Convex", category: "Backend" }, + { name: "Supabase", category: "Backend" }, + { name: "Tailwind CSS", category: "Styling" }, + { name: "Git / CI", category: "Ops" }, +] as const; + +export const projects = [ + { + title: "HabitFlow", + description: + "Minimalistische Gewohnheits-App mit Widgets, HealthKit-Integration und Offline-First-Sync.", + tags: ["SwiftUI", "WidgetKit", "CloudKit"], + }, + { + title: "Studio Dashboard", + description: + "Realtime-Projektboard für ein kleines Studio — Kanban, Zeiterfassung und Kundenportal in einem.", + tags: ["Astro", "Convex", "TypeScript"], + }, + { + title: "Field Notes", + description: + "Ortsbezogene Notizen-App für Außendienst — Kamera, GPS und strukturierte Formulare.", + tags: ["SwiftUI", "Core Location", "Supabase"], + }, +] as const; + +export const process = [ + { + step: "Discovery", + description: + "Ziele, Nutzer und technische Rahmenbedingungen klären — bevor Code geschrieben wird.", + }, + { + step: "Build", + description: + "Iterativ entwickeln mit regelmäßigem Feedback. Fokus auf Qualität statt Feature-Inflation.", + }, + { + step: "Launch", + description: + "App Store, Deployment, Monitoring und Übergabe — damit das Produkt auch danach läuft.", + }, +] as const; diff --git a/src/data/legal.ts b/src/data/legal.ts new file mode 100644 index 0000000..95e5cfa --- /dev/null +++ b/src/data/legal.ts @@ -0,0 +1,43 @@ +/** + * Pflichtangaben für Impressum und Datenschutz. + * Bitte Straße, PLZ, Ort und ggf. USt-IdNr. vor dem Live-Gang eintragen. + */ +export const legal = { + operatorName: "Matthias Meister", + businessForm: "Matthias Meister Softwareentwicklung", + street: "Karl-Marx-Str. 22", + postalCode: "08451", + city: "Crimmitschau", + country: "Deutschland", + email: "support@matthiasmeister.dev", + /** Optional — leer lassen, wenn keine Telefonnummer angegeben werden soll */ + phone: "", + /** USt-IdNr. gemäß § 27a UStG — leer lassen bei Kleinunternehmer */ + vatId: "DE460155697", + /** § 19 UStG — keine Umsatzsteuer ausweisen */ + smallBusinessPursuantTo19UStG: false, + siteUrl: "https://matthiasmeister.dev", + /** Hosting-Anbieter der statischen Website */ + hosting: { + name: "Hetzner Online GmbH", + address: "Industriestr. 20, 91217 Neumarkt", + privacyUrl: "https://www.hetzner.com/de/privacy-policy-notice", + }, + analytics: { + name: "Rybbit", + description: "selbst gehostete Webanalyse", + operator: "Matthias Meister", + endpoint: "https://rybbit.matthias.lol", + }, + /** Stand der Datenschutzerklärung (ISO-Datum) */ + privacyLastUpdated: "2025-06-24", +} as const; + +export function formatPostalAddress(): string { + const lines = [ + legal.street, + `${legal.postalCode} ${legal.city}`, + legal.country, + ]; + return lines.join("\n"); +} diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..4ecb693 --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,75 @@ +--- +interface Props { + title: string; + description?: string; +} + +const { + title, + description = "iOS & Fullstack Web Development — Matthias Meister", +} = Astro.props; +--- + + + + + + + + + + + {title} + + + + + + + + + diff --git a/src/layouts/LegalLayout.astro b/src/layouts/LegalLayout.astro new file mode 100644 index 0000000..82e2c0d --- /dev/null +++ b/src/layouts/LegalLayout.astro @@ -0,0 +1,49 @@ +--- +import BaseLayout from "./BaseLayout.astro"; +import { site } from "../data/landing-content"; +import "@fontsource/syne/700.css"; +import "@fontsource/ibm-plex-sans/400.css"; +import "@fontsource/ibm-plex-sans/600.css"; +import "@fontsource/ibm-plex-mono/400.css"; +import "@fontsource/ibm-plex-mono/500.css"; +import "../styles/site.css"; + +interface Props { + title: string; + description: string; +} + +const { title, description } = Astro.props; +--- + + +
+ +
+
diff --git a/src/pages/datenschutz.astro b/src/pages/datenschutz.astro new file mode 100644 index 0000000..0c2e149 --- /dev/null +++ b/src/pages/datenschutz.astro @@ -0,0 +1,209 @@ +--- +import LegalLayout from "../layouts/LegalLayout.astro"; +import { legal } from "../data/legal"; +--- + + +

Datenschutzerklärung

+ + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/pages/impressum.astro b/src/pages/impressum.astro new file mode 100644 index 0000000..780fd30 --- /dev/null +++ b/src/pages/impressum.astro @@ -0,0 +1,137 @@ +--- +import LegalLayout from "../layouts/LegalLayout.astro"; +import { legal } from "../data/legal"; +--- + + +

Impressum

+ + + + + + { + legal.vatId ? ( + + ) : legal.smallBusinessPursuantTo19UStG ? ( + + ) : null + } + + + + + + + + + + +
diff --git a/src/pages/index.astro b/src/pages/index.astro index 339d5b9..bf55d03 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,102 +1,18 @@ --- +import BaseLayout from "../layouts/BaseLayout.astro"; +import SwissLanding from "../components/SwissLanding.astro"; +import { site } from "../data/landing-content"; +import "@fontsource/syne/700.css"; +import "@fontsource/ibm-plex-sans/400.css"; +import "@fontsource/ibm-plex-sans/600.css"; +import "@fontsource/ibm-plex-mono/400.css"; +import "@fontsource/ibm-plex-mono/500.css"; +import "../styles/site.css"; +--- ---- - - - - - - - - - Website im Aufbau - - - - -
-

Website in Arbeit

-

Ich überarbeite diese Website gerade, da ich den Tech Stack gewechselt habe..

-

- Bei Fragen oder Anliegen erreichen Sie mich unter - support@matthiasmeister.dev. -

-
- - + + + diff --git a/src/styles/site.css b/src/styles/site.css new file mode 100644 index 0000000..766fad4 --- /dev/null +++ b/src/styles/site.css @@ -0,0 +1,497 @@ +.v2 { + --v2-bg: oklch(0.975 0.014 145); + --v2-ink: oklch(0.26 0.04 155); + --v2-muted: oklch(0.48 0.04 155); + --v2-accent: oklch(0.26 0.04 155); + --v2-rule: oklch(0.89 0.022 145); + --v2-term: oklch(0.4 0.1 155); + --v2-term-dim: oklch(0.52 0.06 155); + --v2-display: "Syne", system-ui, sans-serif; + --v2-body: "IBM Plex Sans", system-ui, sans-serif; + --v2-mono: "IBM Plex Mono", ui-monospace, monospace; + + background: var(--v2-bg); + color: var(--v2-ink); + font-family: var(--v2-body); + line-height: 1.6; +} + +.v2__shell { + max-width: 76rem; + margin: 0 auto; + padding: clamp(1.25rem, 3vw, 2.5rem); +} + +.v2__header { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--v2-rule); + margin-bottom: clamp(2.5rem, 6vw, 4rem); +} + +.v2__site-mark { + margin: 0; + font-family: var(--v2-mono); + font-size: 0.8125rem; + font-weight: 500; + letter-spacing: 0.02em; + color: var(--v2-term); +} + +.v2__prompt { + color: var(--v2-term); + font-weight: 500; +} + +.v2__comment { + color: var(--v2-term-dim); + font-weight: 400; +} + +.v2__grid { + display: grid; + grid-template-columns: repeat(12, 1fr); + gap: 1rem clamp(1rem, 2vw, 1.5rem); + background-image: + linear-gradient(to right, oklch(0.84 0.025 145 / 0.55) 1px, transparent 1px), + linear-gradient(to bottom, oklch(0.84 0.025 145 / 0.2) 1px, transparent 1px); + background-size: + calc(100% / 12) 100%, + 100% 2.5rem; + background-position: 0 0; +} + +.v2__hero { + grid-column: 1 / -1; + display: grid; + grid-template-columns: subgrid; + padding-bottom: clamp(2rem, 5vw, 3.5rem); + border-bottom: 1px solid var(--v2-rule); + margin-bottom: clamp(2rem, 5vw, 3.5rem); +} + +.v2__hero-main { + grid-column: 1 / 9; +} + +.v2__hero-prompt { + margin: 0 0 0.75rem; + font-family: var(--v2-mono); + font-size: 0.8125rem; + color: var(--v2-term); +} + +.v2__hero-meta { + grid-column: 9 / -1; + align-self: end; + font-family: var(--v2-mono); + font-size: 0.8125rem; + font-weight: 500; + letter-spacing: 0.04em; + color: var(--v2-term-dim); +} + +.v2__cursor { + display: inline-block; + width: 0.5em; + height: 1.05em; + margin-left: 1px; + vertical-align: text-bottom; + background: var(--v2-term); + animation: v2-cursor-blink 1.15s step-end infinite; +} + +@keyframes v2-cursor-blink { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0; + } +} + +@media (max-width: 767px) { + .v2__grid { + background-image: none; + } + + .v2__hero-main, + .v2__hero-meta { + grid-column: 1 / -1; + } +} + +.v2__title { + margin: 0 0 1rem; + font-family: var(--v2-display); + font-size: clamp(2.5rem, 6vw, 4.25rem); + font-weight: 700; + line-height: 1.02; + letter-spacing: -0.03em; +} + +.v2__tagline { + margin: 0 0 1.75rem; + max-width: 42ch; + font-size: 1.0625rem; + color: var(--v2-muted); + text-wrap: pretty; +} + +.v2__service p, +.v2__project p, +.v2__step p { + text-wrap: pretty; +} + +.v2__cta { + display: inline-block; + padding: 0.75rem 1.5rem; + font-family: var(--v2-mono); + font-size: 0.8125rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: lowercase; + text-decoration: none; + color: var(--v2-ink); + background: transparent; + box-shadow: inset 0 0 0 2px var(--v2-ink); + transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease; +} + +.v2__cta-prefix { + color: var(--v2-term); +} + +.v2__cta:hover { + color: var(--v2-bg); + background: var(--v2-term); + box-shadow: inset 0 0 0 2px var(--v2-term); +} + +.v2__cta:focus-visible { + outline: 2px solid var(--v2-term); + outline-offset: 3px; +} + +.v2__section { + grid-column: 1 / -1; + display: grid; + grid-template-columns: subgrid; + padding-block: clamp(2rem, 4vw, 3rem); + border-bottom: 1px solid var(--v2-rule); +} + +.v2__section-label { + grid-column: 1 / 4; + margin: 0; + font-family: var(--v2-mono); + font-size: clamp(0.875rem, 1.8vw, 1.0625rem); + font-weight: 500; + letter-spacing: 0.02em; + color: var(--v2-ink); +} + +.v2__label-num { + color: var(--v2-term); +} + +.v2__section-body { + grid-column: 4 / -1; +} + +@media (max-width: 767px) { + .v2__section-label, + .v2__section-body { + grid-column: 1 / -1; + } + + .v2__section-label { + margin-bottom: 1.25rem; + } +} + +.v2__services { + display: grid; + gap: 2rem; +} + +@media (min-width: 640px) { + .v2__services { + grid-template-columns: repeat(3, 1fr); + } +} + +.v2__service h3 { + margin: 0 0 0.5rem; + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.02em; +} + +.v2__service p { + margin: 0; + font-size: 0.9375rem; + color: var(--v2-muted); +} + +.v2__stack-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9375rem; +} + +.v2__stack-table th, +.v2__stack-table td { + padding: 0.65rem 0; + text-align: left; + border-bottom: 1px solid var(--v2-rule); + transition: background 0.15s ease; +} + +.v2__stack-table th { + font-family: var(--v2-mono); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: lowercase; + color: var(--v2-term-dim); +} + +.v2__stack-table .v2__mono { + font-family: var(--v2-mono); + color: var(--v2-ink); + font-size: 0.875rem; +} + +.v2__stack-table tbody td:last-child { + font-family: var(--v2-mono); + font-size: 0.8125rem; + color: var(--v2-term-dim); + text-transform: lowercase; +} + +.v2__stack-table tbody tr:hover td { + background: oklch(0.94 0.02 145); +} + +.v2__projects { + display: grid; + gap: 1.5rem; +} + +.v2__project { + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--v2-rule); +} + +.v2__project:last-child { + padding-bottom: 0; + border-bottom: none; +} + +.v2__project h3 { + margin: 0 0 0.35rem; + font-size: 1.0625rem; + font-weight: 600; +} + +.v2__project p { + margin: 0 0 0.75rem; + font-size: 0.9375rem; + color: var(--v2-muted); +} + +.v2__tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0; + padding: 0; + list-style: none; +} + +.v2__tags li { + padding: 0.15rem 0.45rem; + font-family: var(--v2-mono); + font-size: 0.625rem; + letter-spacing: 0.04em; + color: var(--v2-term); + border: 1px solid oklch(0.4 0.1 155 / 0.25); + background: oklch(0.94 0.02 145 / 0.6); +} + +.v2__process { + display: grid; + gap: 1.5rem; +} + +@media (min-width: 640px) { + .v2__process { + grid-template-columns: repeat(3, 1fr); + } +} + +.v2__step h3 { + margin: 0 0 0.35rem; + font-size: 0.9375rem; + font-weight: 600; +} + +.v2__step p { + margin: 0; + font-size: 0.875rem; + color: var(--v2-muted); +} + +.v2__contact-line { + margin: 0; + font-family: var(--v2-mono); + font-size: 0.875rem; + color: var(--v2-term-dim); +} + +.v2__contact-line a { + color: var(--v2-ink); + font-weight: 600; +} + +.v2__footer { + grid-column: 1 / -1; + padding-top: 2rem; + font-family: var(--v2-mono); + font-size: 0.75rem; + color: var(--v2-term-dim); +} + +.v2__footer a { + color: var(--v2-ink); +} + +@media (prefers-reduced-motion: reduce) { + .v2__cursor { + animation: none; + opacity: 0.85; + } +} + +/* Legal pages */ + +.v2__shell--legal { + max-width: 48rem; +} + +.v2__header--legal { + margin-bottom: clamp(1.5rem, 4vw, 2.5rem); +} + +.v2__home-link { + color: inherit; + text-decoration: none; +} + +.v2__home-link:hover { + color: var(--v2-term); +} + +.v2__legal-nav { + display: flex; + flex-wrap: wrap; + gap: 0.75rem 1.25rem; + font-family: var(--v2-mono); + font-size: 0.8125rem; +} + +.v2__legal-nav a { + color: var(--v2-term-dim); + text-decoration: none; +} + +.v2__legal-nav a:hover { + color: var(--v2-ink); +} + +.v2__legal { + padding-bottom: clamp(2rem, 5vw, 3rem); +} + +.v2__legal-title { + margin: 0 0 0.5rem; + font-family: var(--v2-display); + font-size: clamp(2rem, 4vw, 2.75rem); + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.02em; +} + +.v2__legal-meta { + margin: 0 0 2rem; + font-family: var(--v2-mono); + font-size: 0.8125rem; + color: var(--v2-term-dim); +} + +.v2__legal-section { + padding-block: 1.25rem; + border-bottom: 1px solid var(--v2-rule); +} + +.v2__legal-section:last-child { + border-bottom: none; +} + +.v2__legal-section h2 { + margin: 0 0 0.75rem; + font-family: var(--v2-mono); + font-size: 0.9375rem; + font-weight: 600; + letter-spacing: 0.02em; +} + +.v2__legal-section p, +.v2__legal-section li { + margin: 0 0 0.75rem; + font-size: 0.9375rem; + color: var(--v2-muted); + text-wrap: pretty; +} + +.v2__legal-section p:last-child, +.v2__legal-section li:last-child { + margin-bottom: 0; +} + +.v2__legal-section ul { + margin: 0 0 0.75rem; + padding-left: 1.25rem; +} + +.v2__legal-section a { + color: var(--v2-ink); + font-weight: 500; +} + +.v2__legal-section strong { + color: var(--v2-ink); + font-weight: 600; +} + +.v2__footer--legal { + border-top: 1px solid var(--v2-rule); + padding-top: 1.5rem; +} + +.v2__footer a { + color: var(--v2-ink); + text-decoration: none; +} + +.v2__footer a:hover { + color: var(--v2-term); +}