Personal site for Arya Kavian. Astro, fully static, no client framework, ~1 kB of JavaScript (a clock and a card trick). Deploys to GitHub Pages on push.
npm install
npm run dev # http://localhost:4321
npm run build # -> dist/The site is a tarot spread on a dark table: the hero is Arya's card, sections are numbered arcana, skills are four suits taken from the illustration (storms, night sky, crystals, foliage), and projects are cards laid out by hand. Aged paper, gold leaf, charcoal — not neon.
GSAP + ScrollTrigger, all of it in src/scripts/motion.ts. The page is fully
readable without it — nothing is hidden in CSS, so if the script fails the
site is simply still. prefers-reduced-motion disables everything except the
scroll rail.
| Effect | Where |
|---|---|
| Overture: frame, card and type settle in sequence on load | overture() |
| Scroll-linked zoom — the card grows toward you as the hero exits | heroZoom() |
| Numeral turns over, then the title reveals | sectionHeads() |
| Suits and project cards are dealt onto the table, then handed back to CSS | dealCards() |
| Paper research card settles rather than lands | researchCard() |
| Codex lines arrive from the reading edge (flips in RTL) | codex() |
| Half-flip tease on the trick card | trickTease() |
| Gold scroll rail across the top | rail() |
| Smooth anchor jumps, offset for the sticky header | smoothAnchors() |
Three rules this file follows, each of which was a bug once:
- No two animations on the same property of the same element. The overture
animates the individual hero lines; the scroll tween animates the
.hero__typewrapper around them. When both touched the same elements, the scrubbed tween recorded its start values mid-intro and animated from whatever opacity the fade happened to be at. clearPropswhen a tween finishes on anything with a CSS hover state, so GSAP's inline transform doesn't outrank the stylesheet.- No
scroll-behavior: smoothin CSS — it fights ScrollTrigger's scrub. Anchor scrolling goes through ScrollToPlugin instead. For the same reasonbodyusesoverflow-x: cliprather thanhidden:hiddenmakes the body a scroll container and ScrollTrigger then measures the wrong scroller.
| What | File |
|---|---|
| Name, email, timezone, social links, photo list | src/config.ts |
| All UI text and page copy, per language | src/i18n/en.json, src/i18n/fa.json |
| Projects | src/content/work/<lang>/*.md |
| Research | src/content/research/<lang>/*.md |
| Writing | src/content/writing/<lang>/*.md |
| Colours, type, spacing | src/styles/global.css (:root block) |
Entries are ordered by their order frontmatter field. If a language has no
entries in a collection, the English ones are shown instead — so translations
can arrive late without breaking anything.
Create src/content/work/en/my-project.md:
---
title: Project name
question: The question it started as.
year: '2025'
suit: bolt # bolt | moon | crystal | leaf
tags: ['Python', 'Docker']
link: https://github.com/...
linkLabel: Source
order: 1
---
Two or three sentences. What was hard, not which framework you used.Drop files in public/photos/, then list them in photos in src/config.ts.
The section shows an empty state until then.
- Copy
src/i18n/en.jsontosrc/i18n/<code>.jsonand translate it. - Import it in
src/i18n/index.tsand add it todicts(and toRTL_LOCALESif the script is right-to-left). - Add the code to
i18n.localesinastro.config.mjs. - Create
src/pages/<code>/index.astro— copysrc/pages/fa/index.astro.
Persian is already wired up at /fa/, including RTL layout and Vazirmatn.
.github/workflows/deploy.yml builds and publishes on every push to main.
Once, in the repo: Settings → Pages → Build and deployment → Source:
GitHub Actions. Nothing else to configure — the workflow works out the site
URL and, for project pages, the /<repo> base path.
For a custom domain, add public/CNAME containing the domain.
-
src/config.ts— real email, and confirm the GitHub / LinkedIn / Medium / ORCID URLs -
src/content/work/en/— replace the twoexample-*.mdfiles with real projects -
src/content/research/en/llmsecguard.md— add the paperlink -
src/content/writing/en/*.md— add articlelinks