Personal portfolio site for Arpit — a brutalist, multi-page design built with a markdown-driven Python build system.
content/
meta.md ← <title>, description, OG tags, favicon path
hero.md ← home page heading + tagline
footer.md ← location, credits, license (kv-list)
labels.md ← all UI copy (keys fixed, values editable)
work/
intro.md ← work page opening tagline
about.md ← "What is a …" section
toolkit.md ← skills tag cloud
projects.md ← section heading for the projects grid
articles.md ← published writings
projects/ ← one .md per project (auto-discovered, sorted alphabetically)
*.md
play/
intro.md ← play page opening tagline
lately.md ← recent activity (kv-list)
playground.md ← external project cards (name, url, image, desc, year)
ideas.md ← rolodex links
interests.md ← curiosity tags
template.html ← shared HTML shell with {{placeholders}}
style.css ← all styling with design tokens
script.js ← theme, seasons, live data (Open-Meteo)
build.py ← assembles dist/ (Python 3)
requirements.txt ← Python dependencies (Pillow)
assets/
fonts/
SchibstedGrotesk.ttf ← used by Pillow for OG image generation
MaterialSymbolsSharp.woff2 ← self-hosted icon font (downloaded by build.py)
monthly/
og-01.png … og-12.png ← pre-generated seasonal OG images (one per month)
playground/
*.png ← screenshot previews for playground cards (manual)
og-image.png ← copied from monthly/ at build time
favicon.png ← generated fresh on every build (static design)
CNAME ← custom domain
CLAUDE.md ← instructions for Claude Code
dist/
index.html ← home
work/index.html ← work page
play/index.html ← play page
work/<slug>/index.html ← project case study pages (one per projects/*.md)
assets/
CNAME
CSS and JS are inlined into every HTML file. Asset paths are rewritten per page depth.
- / — full-viewport hero with name + tagline
- /work — about, toolkit tags, project cards (3-col grid, internal links), published writings (3-col grid, show-more in batches of 6)
- /play — lately, playground cards (2-col grid, external links with screenshot previews), rolodex (5 random links per visit with shuffle), curiosity tags
- /work/<slug>/ — project case study: problem as h1, org/year meta, tags + visit button, overview, what i did, highlights, back to work
| Token | Mobile | Desktop | Use |
|---|---|---|---|
--text-display |
32px | 128px | hero h1 |
--text-heading |
24px | 36px | page intros, section titles, about h2, case study h1 |
--text-subhead |
18px | 24px | hero tagline |
--text-body |
15px | 17px | about text, tags, project/article names, case study meta |
--text-small |
13px | 13px | article meta, footer, rolodex, lately |
--text-ui |
11px | 11px | nav links, toast, season picker, article/project tags |
| Weight | Use |
|---|---|
| 400 | default — body, tags, descriptions |
| 500 | headings — section titles, about h2 |
| 700 | emphasis — project/article name, nav, toast, lately value |
| Value | Use |
|---|---|
| 1 | hero h1 |
| 1.05 | section titles |
| 1.3 | case study problem heading |
| 1.4 | --text-small and --text-ui |
| 1.5 | --text-body and above |
| Token | Value | Use |
|---|---|---|
--ls-ui |
1px | nav, toast, tags, buttons (--text-ui contexts) |
--ls-label |
0.5px | meta labels, footer, small text (--text-small contexts) |
| Token | Value | Use |
|---|---|---|
--space-xl |
clamp(64px, 10vh, 120px) | page intro top, hero bottom |
--space-lg |
clamp(48px, 7vh, 80px) | between sections, footer |
--space-md |
clamp(24px, 3vw, 40px) | title-to-content, inner gaps |
--space-sm |
12px | box gap (alias --box-gap) |
--box-gap |
— | alias for --space-sm |
--box-pad |
12px 16px |
inner padding for bordered boxes |
--tag-pad |
2px 8px |
padding inside pill tags (cards) |
--tag-gap |
6px |
gap between tags in a group |
--bar-h |
40px | height of all bars and buttons |
--bar-pad |
20px | top/bottom padding inside top-bar and nav |
--btn-pad-x |
16px | horizontal padding in text bar-boxes |
--inner-gap |
8px | tight gap within component items (cards, list rows) |
--padding |
clamp(24px, 5vw, 64px) | horizontal page margin |
| Token | Value | Use |
|---|---|---|
--icon-sz |
16px | inline/small icons |
--icon-sz-lg |
18px | bar-box and primary icons |
--nav-logo-sz |
14px | season picker dot |
Note: Material Symbols opsz in font-variation-settings must match the rendered font-size value numerically.
--black/--white— swap in dark mode (#1a1a1a/#ffffff→#ffffff/#000000)--accent— seasonal, set by JS per month (12 Bengaluru bloom colours); separate--accent-lightand--accent-darkvariants--on-accent—#ffffffin light mode,#000000in dark mode; always use this for text/icons on accent-coloured fills- Theme and season persist within a session via
sessionStorage; weather/AQI cached 1h inlocalStorage
Hover pattern (all bordered interactive box elements): background: var(--accent), color: var(--on-accent), border-color: var(--accent). Accent-coloured children (arrows, labels, publisher) must also override to var(--on-accent). Always inside @media (hover: hover). Applies to: .bar-box, .project, .article, .lately-item, .rolodex-item, .playground-card, .nav-link-box, .season-option.
| Breakpoint | Behaviour |
|---|---|
| > 1024px | 3-col grids (projects, articles) |
| ≤ 1024px | 2-col grids (projects, articles); playground stays 2-col |
| ≤ 768px | 1-col everything; lately items wrap; about goes single column |
python3 build.pyWith image generation (OG image + favicon):
# First time
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# Build
.venv/bin/python3 build.pyEach build:
- Downloads a subsetted
MaterialSymbolsSharp.woff2(~25KB) from the Google Fonts API - Copies the current month's pre-generated OG image from
assets/monthly/ - Generates the favicon (static design, requires Pillow)
- Renders all pages from markdown content
- Inlines minified CSS + JS into each HTML file
- Writes output to
dist/, copies assets
To pre-generate all 12 monthly OG images (requires Pillow):
python3 build.py --gen-monthlyEdit any file in content/, then run python3 build.py. Inline syntax available in all content files:
**bold**→ accent-coloured highlight[text](url)→ link (opens in new tab)- key: value→ structured kv data
Each project lives in its own file at content/work/projects/<filename>.md. Files are sorted alphabetically — prefix with a number to control order. The slug is derived from - org: automatically, or set explicitly with - slug:.
The card on the work page is an internal link to the case study at /work/<slug>/. External visit links appear only on the case study page.
<!-- CARD -->
- problem: The problem this project addresses, as a question or statement.
- org: Organisation Name
- year: 2024
- tags: tag1, tag2
<!-- IDENTITY -->
- url: https://example.com/
- slug: custom-slug ← optional; auto-derived from org if omitted
<!-- CASE STUDY -->
## overview
Case study overview paragraph(s). Each non-empty line becomes a <p>.
## what i did
- Bullet one
- Bullet two
## highlights
- [Link text](https://url.com/) or plain text resultSection headings (## overview, ## what i did, ## highlights) are displayed as written — customise per project. Sections with no content are silently skipped.
Case study page layout:
- Problem statement as
<h1>(full viewport height intro, anchored bottom) org · yearmeta line with visit site icon button[↗]- Tags row at bar height, with visit button aligned right
- Content sections below
Each article is a ## Title block in content/work/articles.md. First 6 are visible; the rest are behind a show-more button (batches of 6). Displayed in file order (add newest at top):
## Article Title
- publisher: Publication Name
- date: Mon YYYY
- tags: tag1, tag2
- url: https://url.com/In content/play/playground.md, each line is one project card. Format:
[Name](https://url) | image.png | one-line description | year
image.png— filename only; file must exist inassets/playground/year— optional; displayed in accent colour below the name
Example:
# playground
[memories](https://photos.thedataareclean.com) | photos.png | experiments behind the viewfinder. | 2026
[musings](https://musings.thedataareclean.com) | musings.png | a place for my thoughts and ideas. | 2026Cards render as a 2-col grid. Each card links externally with an arrow_outward indicator.
In content/play/lately.md, use [title](url) for a linked entry or leave blank to hide:
- read: [Book Title](https://example.com)
- listened: [Podcast](https://example.com)
- watched:Available keys: read, listened, watched, cooked, explored, played, built, learned, ran, cycled, photographed, brewed, visited, wrote.
In content/play/ideas.md, add [name](url) bullet entries. 5 random items are shown per visit with a shuffle button.
All UI copy lives in content/labels.md. Keys are fixed; only values change:
| Key | Default | Used for |
|---|---|---|
case-study |
case study | project card link text (cards link internally, no arrow) |
visit-site |
visit site | aria-label on the icon-only visit button on case study pages |
back-to-work |
← back to work | case study page back link |
show-more |
show more | articles expand button |
email-copied |
email copied | clipboard toast message |
season-info |
colours from bengaluru's seasonal blooms | season picker footnote |
Add the Material Symbols Sharp icon name to ICON_NAMES in build.py. The next build downloads an updated subset font automatically.
The footer shows live data for Bengaluru via Open-Meteo (free, no API key):
- Local time —
Asia/Kolkatatimezone, updates every 60s, pauses when tab is hidden - Temperature — Open-Meteo forecast API, cached 1h in localStorage
- AQI — Open-Meteo air quality API (US AQI), cached 1h in localStorage
Three GitHub Actions workflows:
| Workflow | Trigger | What it does |
|---|---|---|
pages.yml |
push to main |
Runs build.py, deploys dist/ to GitHub Pages |
monthly-build.yml |
1st of every month | Runs build.py with Pillow, commits updated dist/ + seasonal OG image + favicon |
lately-reminder.yml |
every Sunday 9am IST | Opens a GitHub issue to update content/play/lately.md (skips if one is already open) |
Tag significant releases for easy rollback:
git tag -a v1.x.x -m "short description"
git push origin v1.x.xThen mark a GitHub release at github.com/<user>/<repo>/releases/new — select the tag, write a short changelog, publish.
Versioning: major = redesign, minor = new feature/section/page, patch = content update or bug fix.