feat: core layout, UI atoms, homepage + behaviour modules

- UI atoms: Chip, Button, SplitCvButton, LangSwitch, ThemeToggle, Card,
  SectionLabel, TraceMotif, FramedImage
- core: Base/Seo/Header/MobileNav/Footer/HintBar/SkipLink/ThemeScript
- homepage sections: Hero, ProofStrip, SkillsGrid, ProjectCards,
  ExperienceTimeline, AboutTeaser, ContactBand + Homepage composition (EN/NO)
- behaviour modules: theme, nav (overlay/hint/copy/header), observer (reveal/spy),
  lightbox, form — progressive enhancement, fail-silent
- SEO head component: meta, hreflang from slug map, JSON-LD, OG references
- move content data to src/data (avoid Astro reserved content-collection folder)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-04 01:20:46 +02:00
parent 2184a1892f
commit 1fce1b3cec
45 changed files with 1891 additions and 27 deletions
@@ -0,0 +1,77 @@
---
import type { Locale } from '@i18n/locales';
import { useDict } from '@i18n/t';
import { homeAnchor } from '@i18n/slugMap';
import { getExperience } from '@lib/content';
import SectionLabel from '@components/ui/SectionLabel.astro';
interface Props {
locale: Locale;
compact?: boolean; // when true (experience page), render heading differently
}
const { locale } = Astro.props;
const d = useDict(locale);
const items = getExperience(locale);
const firstAlongside = items.findIndex((i) => i.alongside);
const period = (from: number, to: number | null) => `${from}${to ?? d.home.present}`;
---
<section id={homeAnchor('experience', locale)} class="mx-auto max-w-[--content-max] px-6 pt-24" data-reveal>
<SectionLabel number="03" text={d.home.experienceLabel} />
<h2 class="mt-4 text-h2">{d.home.experienceHeading}</h2>
<div class="relative mt-10 pl-8">
<span
class="timeline-spine absolute bottom-2 left-[5px] top-2 w-px origin-top bg-line-strong"
aria-hidden="true"></span>
{
items.map((item, i) => (
<>
{i === firstAlongside && (
<p class="mb-3 mt-6 font-mono text-mono-label font-medium uppercase tracking-[0.08em] text-ink-faint">
{d.home.earlierRoles}
</p>
)}
{item.emphasis === 'featured' ? (
<div class="relative pb-10">
<span class="absolute -left-8 top-1.5 h-2.5 w-2.5 rounded-full bg-accent ring-4 ring-surface-0" aria-hidden="true" />
<h3 class="text-h4 font-semibold text-ink">
{item.role} — {item.employer}
</h3>
<p class="mt-1 font-mono text-mono-meta text-ink-faint">
{period(item.period.from, item.period.to)} · {item.location}
{item.progression &&
` · ${item.progression.map((p) => `${p.from}${p.to} ${p.label}`).join(' → ')}`}
</p>
{item.summary && <p class="mt-3 text-body text-ink-muted">{item.summary}</p>}
{item.highlights && (
<ul class="mt-3 flex flex-col gap-2">
{item.highlights.map((h) => (
<li class="flex gap-3 text-body text-ink-muted">
<span class="mt-2 h-1 w-1 shrink-0 rounded-full bg-accent" aria-hidden="true" />
<span>{h}</span>
</li>
))}
</ul>
)}
</div>
) : (
<div class="relative py-2.5">
<span class="absolute -left-8 top-4 h-2 w-2 rounded-full bg-ink-faint ring-4 ring-surface-0" aria-hidden="true" />
<p class="text-small text-ink-muted">
<span class="font-mono text-ink-faint">{period(item.period.from, item.period.to)}</span>
<span class="mx-2">·</span>
<span class="text-ink">{item.role}</span>
<span class="mx-2">·</span>
{item.employer}
</p>
</div>
)}
</>
))
}
</div>
</section>