Imporved the site

This commit is contained in:
Andreas Dahm
2026-08-26 11:26:00 +02:00
parent 888fb6289d
commit db2bc7ab97
10 changed files with 78 additions and 44 deletions

View File

@@ -1,7 +1,7 @@
export class AssetsConstants { export class AssetsConstants {
static readonly ME = '/assets/me.webp'; static readonly ME = '/assets/me.webp';
static readonly LOGO = '/assets/favicon.ico'; static readonly LOGO = '/assets/logos/logo-wolf.png';
static readonly FLAG_DE = '/assets/flags/de.svg'; static readonly FLAG_DE = '/assets/flags/de.svg';
static readonly FLAG_EN = '/assets/flags/gb.svg'; static readonly FLAG_EN = '/assets/flags/gb.svg';

View File

@@ -1,6 +1,6 @@
<mat-toolbar class="flex! items-center! p-[clamp(0.5rem,1vw,1rem)]! backdrop-blur-sm! backdrop-saturate-[1.1]! bg-white/80! dark:bg-[#313131]/80! border-b! border-black/8!" color="primary" (keydown)="onKeydown($event)"> <mat-toolbar class="flex! items-center! p-[clamp(0.5rem,1vw,1rem)]! backdrop-blur-sm! backdrop-saturate-[1.1]! bg-white/80! dark:bg-[#313131]/80! border-b! border-black/8!" color="primary" (keydown)="onKeydown($event)">
<a class="flex items-center gap-[clamp(0.4rem,1vw,0.6rem)] text-inherit no-underline" routerLink="/"> <a class="flex items-center gap-[clamp(0.4rem,1vw,0.6rem)] text-inherit no-underline" routerLink="/">
<img class="w-[clamp(36px,10vw,48px)] h-[clamp(36px,10vw,48px)] rounded-full" src="{{AssetsConstants.LOGO}}" alt="" aria-hidden="true" draggable="false" <img class="w-[clamp(36px,10vw,48px)] h-auto rounded-full" src="{{AssetsConstants.LOGO}}" width="117" height="128" alt="" aria-hidden="true" draggable="false"
oncontextmenu="return false;"> oncontextmenu="return false;">
<span class="font-semibold tracking-[0.2px] text-[clamp(1rem,3vw,1.2rem)]">{{ 'APP.TITLE' | translate }}</span> <span class="font-semibold tracking-[0.2px] text-[clamp(1rem,3vw,1.2rem)]">{{ 'APP.TITLE' | translate }}</span>
</a> </a>

View File

@@ -40,4 +40,15 @@ describe('LanguageService', () => {
expect(translateSpy.use).toHaveBeenCalledWith('de'); expect(translateSpy.use).toHaveBeenCalledWith('de');
expect(localStorage.getItem(LocalStoreConstants.LANGUAGE_KEY)).toBe('de'); expect(localStorage.getItem(LocalStoreConstants.LANGUAGE_KEY)).toBe('de');
}); });
it('marks the document with the active language so screen readers switch voice', () => {
localStorage.setItem(LocalStoreConstants.LANGUAGE_KEY, 'en');
const service = createService();
expect(document.documentElement.lang).toBe('en');
service.use('de');
expect(document.documentElement.lang).toBe('de');
});
}); });

View File

@@ -1,4 +1,5 @@
import { Injectable, inject, signal } from '@angular/core'; import { Injectable, inject, signal } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import {LocalStoreConstants} from '../constants/LocalStoreConstants'; import {LocalStoreConstants} from '../constants/LocalStoreConstants';
@@ -7,20 +8,32 @@ type Lang = 'de' | 'en';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class LanguageService { export class LanguageService {
private readonly translate = inject(TranslateService); private readonly translate = inject(TranslateService);
private readonly document = inject(DOCUMENT);
readonly lang = signal<Lang>(this.getInitial()); readonly lang = signal<Lang>(this.getInitial());
constructor() { constructor() {
// translate service lang and fallback are already configured via provideTranslateService in app.config // translate service lang and fallback are already configured via provideTranslateService in app.config
// just ensure the stored preference is active on startup // just ensure the stored preference is active on startup
this.translate.use(this.lang()); this.translate.use(this.lang());
this.markDocumentLanguage(this.lang());
} }
use(l: Lang) { use(l: Lang) {
this.lang.set(l); this.lang.set(l);
this.translate.use(l); this.translate.use(l);
this.markDocumentLanguage(l);
try { localStorage.setItem(LocalStoreConstants.LANGUAGE_KEY, l); } catch (e) { void e; } try { localStorage.setItem(LocalStoreConstants.LANGUAGE_KEY, l); } catch (e) { void e; }
} }
/**
* index.html ships a static lang="en". Without this the document keeps
* claiming English after a switch, so a screen reader reads the German
* content with an English voice.
*/
private markDocumentLanguage(l: Lang): void {
this.document.documentElement.lang = l;
}
private getInitial(): Lang { private getInitial(): Lang {
try { try {
const stored = localStorage.getItem(LocalStoreConstants.LANGUAGE_KEY) as Lang | null; const stored = localStorage.getItem(LocalStoreConstants.LANGUAGE_KEY) as Lang | null;

View File

@@ -29,7 +29,7 @@
"PROFILE": "Profilfoto von Andreas Dahm" "PROFILE": "Profilfoto von Andreas Dahm"
}, },
"HELLO": "Hallo, ich bin Andreas.", "HELLO": "Hallo, ich bin Andreas.",
"LEAD": "Senior Softwareentwickler und Architekt mit über 15 Jahren Erfahrung. Meine Arbeit bewegt sich zwischen zwei Bereichen, die selten zusammentreffen: Echtzeit-3D-Simulation Stoffphysik, Kollisionserkennung, GPU-Shader — und verteilte Cloud-Architektur für Unternehmensplattformen. Unterwegs habe ich ein Entwicklungsteam geleitet, die Migration eines großen Legacy-Monolithen zu Microservices geplant und ein Spiel auf Steam veröffentlicht. In letzter Zeit beschäftigt mich vor allem die Engineering-Seite der KI-gestützten Entwicklung: die Harnesses, Leitplanken und Quality Gates, die generierten Code überprüfbar machen. Privat entwickle ich Spiele und beschäftige mich mit algorithmischen Themen — vieles davon findet sich irgendwo auf dieser Seite wieder.", "LEAD": "Ich arbeite seit 2010 als Softwareentwickler. Die meiste Zeit davon in zwei ziemlich unterschiedlichen Ecken. Die eine ist Echtzeit-3D-Simulation: Stoffphysik, Kollisionserkennung, GPU-Shader. Die andere ist Cloud-Architektur für größere Unternehmensplattformen. Ein paar Jahre habe ich ein kleines Backend-Team geleitet, den Umbau eines großen Legacy-Monolithen Richtung Microservices geplant und zusammen mit meinem Bruder ein Spiel auf Steam veröffentlicht. In letzter Zeit geht viel Zeit in Tooling rund um KI-gestützte Entwicklung, vor allem darum, das Ergebnis überprüfbar zu machen und nicht nur schnell. Privat baue ich Spiele und probiere Sachen mit Algorithmen aus. Das meiste davon landet irgendwann auf dieser Seite.",
"ROLE": "Senior Software Entwickler / Full-Stack Entwickler / Softwarearchitekt", "ROLE": "Senior Software Entwickler / Full-Stack Entwickler / Softwarearchitekt",
"LOCATION": "München · Remote", "LOCATION": "München · Remote",
"VIEW_PROJECTS": "Projekte ansehen", "VIEW_PROJECTS": "Projekte ansehen",
@@ -187,12 +187,12 @@
"PROJECT": { "PROJECT": {
"CLAUDE_BOX": { "CLAUDE_BOX": {
"TITLE": "Claude-in-a-Box", "TITLE": "Claude-in-a-Box",
"DESCRIPTION": "Eine containerisierte Entwicklungsumgebung, die einen KI-Coding-Agenten zu einem überprüfbaren Teil der Toolchain macht. Statt spontaner Prompts führt die Box den Agenten über Regeln pro Stack, Architekturprofile pro Repository und Commit-Hooks, die keinen roten Build durchlassen.", "DESCRIPTION": "Eine Entwicklungsumgebung im Container, in der ein KI-Coding-Agent nach festen Regeln arbeitet statt nach spontanen Prompts. Er zieht sich die Regeln für den Stack, den er im Repository vorfindet, und ein Git-Hook stoppt den Commit, wenn Build oder Tests rot sind.",
"LINK_INTERNAL": "Projektdetails", "LINK_INTERNAL": "Projektdetails",
"HIGHLIGHTS": { "HIGHLIGHTS": {
"P1": "Deterministisches Context-Routing: Der Agent lädt die Regeln des Stacks, den er im Repository tatsächlich vorfindet.", "P1": "Der Agent lädt die Regeln für den Stack, den er im Repo vorfindet. Ein Java-Service bekommt also andere als ein Frontend.",
"P2": "Commit-Gates, die bei fehlgeschlagenem Build oder Test blockieren — generierter Code kann die Qualitätsschwelle nicht umgehen.", "P2": "Ein Commit-Hook führt Build und Tests aus und blockiert den Commit, wenn sie fehlschlagen.",
"P3": "MCP-Integrationen für Code-Intelligence und lesenden Ticket-Zugriff, dazu Subagenten, die den Diff prüfen." "P3": "MCP-Server für Codesuche und lesenden Jira-Zugriff, dazu eigene Review-Agenten, die den Diff durchgehen."
} }
}, },
"P2": { "P2": {
@@ -364,17 +364,17 @@
}, },
"CLAUDE_BOX": { "CLAUDE_BOX": {
"TITLE": "Claude-in-a-Box", "TITLE": "Claude-in-a-Box",
"SHORT_DESCRIPTION": "Eine containerisierte, regelgetriebene Umgebung für die Arbeit mit einem KI-Coding-Agenten.", "SHORT_DESCRIPTION": "Ein Container-Setup, um mit einem KI-Coding-Agenten nach festen Regeln zu arbeiten.",
"INTRODUCTION": "Mit einem KI-Coding-Agenten zu arbeiten ist leicht angefangen und schwer ehrlich gehalten. Das Ergebnis sieht plausibel aus, also sind die teuren Fehler genau die, die durch das Review kommen. Claude-in-a-Box ist meine Antwort darauf: eine Docker-basierte Entwicklungsumgebung, in der der Agent unter denselben Bedingungen arbeitet wie eine neue Kollegin oder ein neuer Kollege — nach den Architekturregeln des Repositories, in dem er sich befindet, und mit einem Build, der grün bleiben muss, bevor irgendetwas committet werden kann. Das interessante Engineering steckt nicht in den Prompts, sondern im Harness darum herum.", "INTRODUCTION": "Einen KI-Coding-Agenten dazu zu bringen, irgendetwas zu produzieren, ist einfach. Ihn dazu zu bringen, etwas zu produzieren, das man auch wirklich mergen würde, ist eine andere Sache, weil das Ergebnis auf den ersten Blick meistens ganz vernünftig aussieht. Also habe ich ein Docker-Setup gebaut, in dem der Agent wie jeder andere Mitwirkende arbeitet. Er bekommt die Architekturregeln des Repositories, in dem er gerade ist, und der Build muss grün sein, bevor überhaupt etwas committet werden kann. Die meiste Arbeit steckt im Setup rund um den Agenten und nicht in den Prompts.",
"BULLET_1": "Containerisierter Workspace mit fixierten Toolchains — über SDKMAN verwaltete JDKs und Maven, Node und ein persistentes Home-Volume, das einen Container-Rebuild überlebt.", "BULLET_1": "Container mit festen Toolchains: JDKs und Maven über SDKMAN, Node und ein Home-Volume, das einen Rebuild übersteht.",
"BULLET_2": "Context-Routing per Erkennung: Ein Repository mit pom.xml lädt die Java/Spring-Regeln, eines mit package.json die TypeScript-Regeln, und ein Repository mit eigenem Architekturprofil lädt zusätzlich dieses.", "BULLET_2": "Die Regeln werden per Erkennung geladen. Ein Repo mit pom.xml bekommt die Java- und Spring-Regeln, eines mit package.json die TypeScript-Regeln, und Repos mit eigenen Architekturnotizen bekommen diese zusätzlich.",
"BULLET_3": "Ein Commit-Hook, der den Stack selbst erkennt, das passende Build- und Test-Gate ausführt und den Commit bei Fehlschlag blockiert.", "BULLET_3": "Ein Commit-Hook ermittelt den Stack, führt den passenden Build und die Tests aus und verweigert den Commit, wenn sie fehlschlagen.",
"BULLET_4": "MCP-Server für Code-Graph-Abfragen und lesenden Ticket-Zugriff, dazu eigens gebaute Review-Subagenten, die einen fertigen Diff prüfen, bevor er als erledigt gemeldet wird.", "BULLET_4": "MCP-Server für Code-Graph-Abfragen und lesende Jira-Zugriffe, dazu Review-Agenten, die einen fertigen Diff prüfen, bevor er als erledigt gilt.",
"CHALLENGE_1": "Das Regelwerk klein genug für den Context zu halten und trotzdem verbindlich zu machen — allgemeine Ratschläge werden ignoriert, also mussten die Regeln in Stack-Skills und Repository-Profile aufgeteilt werden, die nur bei Bedarf laden.", "CHALLENGE_1": "Die Regeln kurz genug zu halten, dass sie noch etwas bringen. Lange allgemeine Richtlinien werden einfach ignoriert, also sind sie in Dateien pro Stack und pro Repo aufgeteilt, die nur bei Bedarf geladen werden.",
"CHALLENGE_2": "Das Quality Gate unumgehbar machen. Die Prüfung läuft als Hook außerhalb des Agenten, sodass ein fehlgeschlagener Build den Commit blockiert — unabhängig davon, was der Agent über seine eigene Arbeit glaubt.", "CHALLENGE_2": "Das Quality Gate so zu bauen, dass der Agent sich nicht daran vorbeireden kann. Die Prüfung läuft als Git-Hook außerhalb des Agenten, ein fehlgeschlagener Build blockiert den Commit also unabhängig davon, was der Agent über seine eigene Arbeit denkt.",
"CHALLENGE_3": "Toolchain-Isolation in einem Container ohne Root: Builds, Browser und Sprach-Runtimes mussten alle in Volumes installiert werden, die dem Entwickler-User gehören.", "CHALLENGE_3": "Die Toolchains in einem Container ohne Root installiert zu bekommen. Builds, Browser und Sprach-Runtimes mussten alle in Volumes, die dem Entwickler-User gehören.",
"LEARNING_1": "Die Qualität KI-gestützter Arbeit hängt vor allem am Harness, nicht am Prompt. Deterministische Gates schlagen sorgfältige Formulierungen jedes Mal.", "LEARNING_1": "Wie gut das Ganze funktioniert, hängt viel stärker am Setup rund um den Agenten als an der Formulierung der Prompts.",
"LEARNING_2": "Den Agenten als nicht vertrauenswürdigen Mitwirkenden zu behandeln — reviewed, gegated und nur mit dem nötigen Context versorgt — ist dieselbe Disziplin, die auch menschliches Code-Review funktionieren lässt." "LEARNING_2": "Den Output des Agenten genauso zu reviewen und zu gaten wie den Pull Request einer neuen Kollegin hat sich als die nützlichste Regel überhaupt herausgestellt."
} }
}, },
"IMPRINT": { "IMPRINT": {

View File

@@ -29,7 +29,7 @@
"PROFILE": "Profile photo of Andreas Dahm" "PROFILE": "Profile photo of Andreas Dahm"
}, },
"HELLO": "Hello, Im Andreas.", "HELLO": "Hello, Im Andreas.",
"LEAD": "Senior software developer and architect, 15+ years in the field. My work sits across two areas that rarely meet: real-time 3D simulation cloth physics, collision detection, GPU shaders — and distributed cloud architecture for enterprise platforms. Along the way I have led a development team, planned the migration of a large legacy monolith to microservices, and shipped a game on Steam. More recently I have been working on the engineering side of AI-assisted development: the harnesses, guardrails and quality gates that make generated code reviewable. Outside work I build games and dig into algorithmic problems — most of which end up somewhere on this site.", "LEAD": "I have been working as a software developer since 2010. Most of that time went into two fairly different corners. One is real-time 3D simulation: cloth physics, collision detection, GPU shaders. The other is cloud architecture for larger enterprise platforms. I led a small backend team for a few years, planned the move of a big legacy monolith towards microservices, and released a game on Steam together with my brother. Lately a lot of my time goes into tooling around AI-assisted development, mostly into making the output reviewable instead of just fast. In my free time I build games and mess around with algorithms. Most of that ends up somewhere on this site.",
"ROLE": "Senior Software Developer / Full-Stack Developer / Software Architect", "ROLE": "Senior Software Developer / Full-Stack Developer / Software Architect",
"LOCATION": "Munich · Remote", "LOCATION": "Munich · Remote",
"VIEW_PROJECTS": "View projects", "VIEW_PROJECTS": "View projects",
@@ -187,12 +187,12 @@
"PROJECT": { "PROJECT": {
"CLAUDE_BOX": { "CLAUDE_BOX": {
"TITLE": "Claude-in-a-Box", "TITLE": "Claude-in-a-Box",
"DESCRIPTION": "A containerised development environment that turns an AI coding agent into a reviewable part of the toolchain. Instead of prompting ad hoc, the box routes the agent through per-stack rules, per-repository architecture profiles and commit hooks that refuse to let a red build through.", "DESCRIPTION": "A development setup in a container where an AI coding agent works under fixed rules instead of ad hoc prompting. It picks up the rules for whatever stack it finds in the repository, and a git hook stops the commit if the build or the tests are red.",
"LINK_INTERNAL": "Project details", "LINK_INTERNAL": "Project details",
"HIGHLIGHTS": { "HIGHLIGHTS": {
"P1": "Deterministic context routing: the agent loads the rules for the stack it actually detects in the repository.", "P1": "The agent loads the rules for the stack it finds in the repo, so a Java service and a frontend get different ones.",
"P2": "Commit gates that block on a failing build or test run, so generated code cannot bypass the quality bar.", "P2": "A commit hook runs the build and the tests and blocks the commit when they fail.",
"P3": "MCP integrations for code intelligence and read-only ticket access, plus subagents that review the diff." "P3": "MCP servers for code search and read-only Jira access, plus separate review agents that go over the diff."
} }
}, },
"P2": { "P2": {
@@ -364,17 +364,17 @@
}, },
"CLAUDE_BOX": { "CLAUDE_BOX": {
"TITLE": "Claude-in-a-Box", "TITLE": "Claude-in-a-Box",
"SHORT_DESCRIPTION": "A containerised, rule-driven environment for working with an AI coding agent.", "SHORT_DESCRIPTION": "A container setup for working with an AI coding agent under fixed rules.",
"INTRODUCTION": "Working with an AI coding agent is easy to start and hard to keep honest. The output looks plausible, so the expensive failures are the ones that pass review. Claude-in-a-Box is my answer to that: a Docker-based development environment where the agent operates under the same constraints a new colleague would — the architecture rules of the repository it is in, and a build that has to stay green before anything can be committed. The interesting engineering is not in the prompts; it is in the harness around them.", "INTRODUCTION": "Getting an AI coding agent to produce something is easy. Getting it to produce something you would actually merge is a different problem, because the output usually looks fine at first glance. So I built a Docker setup where the agent works like any other contributor. It gets the architecture rules of the repository it is in, and the build has to be green before anything can be committed. Most of the work went into the setup around the agent rather than into the prompts.",
"BULLET_1": "Containerised workspace with pinned toolchains — SDKMAN-managed JDKs and Maven, Node, and a persistent home volume that survives a container rebuild.", "BULLET_1": "Container with fixed toolchains: JDKs and Maven through SDKMAN, Node, and a home volume that survives a rebuild.",
"BULLET_2": "Context routing by detection: a repository with a pom.xml loads the Java/Spring rules, one with a package.json loads the TypeScript rules, and a repository with its own architecture profile loads that too.", "BULLET_2": "Rules are loaded by detection. A repo with a pom.xml gets the Java and Spring rules, one with a package.json gets the TypeScript rules, and repos with their own architecture notes get those on top.",
"BULLET_3": "A commit hook that detects the stack itself, runs the matching build and test gate, and blocks the commit when it fails.", "BULLET_3": "A commit hook works out the stack, runs the matching build and tests, and refuses the commit if they fail.",
"BULLET_4": "MCP servers for code-graph queries and read-only ticket lookup, plus purpose-built review subagents that grill a finished diff before it is reported as done.", "BULLET_4": "MCP servers for code graph queries and read-only Jira lookups, plus review agents that check a finished diff before it counts as done.",
"CHALLENGE_1": "Keeping the rule set small enough to stay in context but specific enough to be binding general advice gets ignored, so the rules had to be split into stack skills and repository profiles that load only when relevant.", "CHALLENGE_1": "Keeping the rules short enough to stay useful. Long general guidelines just get ignored, so they are split into per-stack and per-repo files that only load when they apply.",
"CHALLENGE_2": "Making the quality gate impossible to talk around. The check runs as a hook outside the agent, so a failing build blocks the commit regardless of what the agent believes about its own work.", "CHALLENGE_2": "Making the quality gate something the agent cannot talk its way past. The check runs as a git hook outside the agent, so a failing build blocks the commit no matter what the agent believes about its own work.",
"CHALLENGE_3": "Toolchain isolation in a container that has no root: builds, browsers and language runtimes all had to be installed into volumes owned by the development user.", "CHALLENGE_3": "Getting the toolchains installed in a container that has no root. Builds, browsers and language runtimes all had to go into volumes owned by the development user.",
"LEARNING_1": "The quality of AI-assisted work is mostly a function of the harness, not the prompt. Deterministic gates beat careful wording every time.", "LEARNING_1": "How well this works depends far more on the setup around the agent than on how the prompt is worded.",
"LEARNING_2": "Treating the agent as an untrusted contributor — reviewed, gated, and given only the context it needs — turns out to be the same discipline that makes human code review work." "LEARNING_2": "Reviewing and gating the agent's output the same way you would a new colleague's pull request turned out to be the most useful rule of the lot."
} }
}, },
"IMPRINT": { "IMPRINT": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -27,9 +27,18 @@
<meta name="twitter:image" content="https://andreas-dahm.eu/assets/me.webp"> <meta name="twitter:image" content="https://andreas-dahm.eu/assets/me.webp">
<link rel="icon" type="image/x-icon" href="assets/favicon.ico"> <link rel="icon" type="image/x-icon" href="assets/favicon.ico">
<!--
The LCP image lives in a lazily loaded route chunk, so the browser cannot
discover it until Angular has booted. Preloading it here is what makes it
fetch in parallel with the bundle instead of after it.
-->
<link rel="preload" as="image" href="assets/me.webp" fetchpriority="high">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&display=swap" rel="stylesheet">
</head> </head>
<body> <body>
<app-root class="flex flex-col min-h-screen"></app-root> <app-root class="flex flex-col min-h-screen"></app-root>

View File

@@ -31,8 +31,13 @@ $dark-theme: mat.define-theme((color: (theme-type: dark, primary: mat.$cyan-pale
--card-border-inset: rgba(255, 255, 255, 0.35); --card-border-inset: rgba(255, 255, 255, 0.35);
--card-border-inset-dark: rgba(0, 0, 0, 0.35); --card-border-inset-dark: rgba(0, 0, 0, 0.35);
--link-color: #38a7ff; /*
--link-color-hover: #66bfff; Chosen to clear WCAG AA *after* compositing: several link containers carry
opacity-85/opacity-80, which blends the text toward the surface. Worst case
(footer, 80% over #f1f4f3) is 4.73:1; a plain link on a card is 8.03:1.
*/
--link-color: #0f4f8a;
--link-color-hover: #0a3a66;
} }
.dark { .dark {

View File

@@ -26,7 +26,7 @@
--container-app: var(--app-maxWidth); --container-app: var(--app-maxWidth);
--radius-card: var(--card-radius); --radius-card: var(--card-radius);
--font-sans: Inter, Roboto, Arial, sans-serif; --font-sans: Roboto, Arial, sans-serif;
--spacing-fluid-sm: clamp(0.5rem, 2vw, 1rem); --spacing-fluid-sm: clamp(0.5rem, 2vw, 1rem);
--spacing-fluid-md: clamp(1rem, 3vw, 1.5rem); --spacing-fluid-md: clamp(1rem, 3vw, 1.5rem);
@@ -88,7 +88,7 @@
body { body {
margin: 0; margin: 0;
font-family: Inter, Roboto, Arial, sans-serif; font-family: Roboto, Arial, sans-serif;
background-color: var(--app-bg); background-color: var(--app-bg);
color: var(--app-fg); color: var(--app-fg);
transition: background-color 220ms ease, color 220ms ease; transition: background-color 220ms ease, color 220ms ease;
@@ -98,10 +98,6 @@
@apply text-link no-underline font-medium hover:text-link-hover hover:underline; @apply text-link no-underline font-medium hover:text-link-hover hover:underline;
} }
.material-symbols-outlined {
font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
}
.dark body { .dark body {
background: radial-gradient(ellipse at 50% 0%, #1e2530 0%, #1a1a1a 65%); background: radial-gradient(ellipse at 50% 0%, #1e2530 0%, #1a1a1a 65%);
} }