import kgva site

This commit is contained in:
2026-06-29 01:37:03 +02:00
commit f7643602cb
103 changed files with 2015 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
{
"permissions": {
"allow": [
"Bash(hugo version *)",
"Bash(ls -la .claude/ 2>/dev/null; echo \"---SEARCH for hugo sites---\"; find /home/karim -maxdepth 3 -name \"hugo.toml\" -o -maxdepth 3 -name \"config.toml\" 2>/dev/null | grep -v node_modules | head -20; echo \"---shibui---\"; find /home/karim -maxdepth 4 -iname \"*shibui*\" 2>/dev/null | head)",
"Read(//home/karim/**)",
"WebFetch(domain:www.baenziger-hug.com)",
"WebFetch(domain:karimgabrielevarano.xyz)",
"WebSearch",
"Bash(hugo --gc --minify)",
"Bash(hugo server *)",
"Bash(curl -s -o /dev/null -w \"homepage: HTTP %{http_code}\\\\n\" http://localhost:1313/)",
"Bash(curl -s -o /dev/null -w \"work page: HTTP %{http_code}\\\\n\" http://localhost:1313/work/)",
"Bash(sed -n 's/.*<main id=main-content role=main>\\\\\\(.*\\\\\\)<\\\\/main>.*/\\\\1/p' about.html)",
"Bash(sed -n 's/.*<main id=main-content role=main>\\\\\\(.*\\\\\\)<\\\\/main>.*/\\\\1/p' work.html)",
"Bash(curl -s -o /dev/null -w \"home HTTP %{http_code}\\\\n\" http://localhost:1313/)",
"Bash(curl -s -o /dev/null -w \"work HTTP %{http_code}\\\\n\" http://localhost:1313/work/)",
"WebFetch(domain:git.kgva.ch)",
"Bash(echo \"=== ssh hosts configured ===\"; sed -n 's/^[Hh]ost //p' ~/.ssh/config 2>/dev/null || echo \"\\(no ~/.ssh/config\\)\"; echo \"=== ssh keys present ===\"; ls ~/.ssh/*.pub 2>/dev/null || echo \"\\(none\\)\"; echo \"=== known_hosts \\(host names only\\) ===\"; cut -d' ' -f1 ~/.ssh/known_hosts 2>/dev/null | tr ',' '\\\\n' | sort -u | head -20 || echo \"\\(none\\)\")",
"Bash(chmod +x deploy/provision-lxc.sh)",
"Bash(ssh -o BatchMode=yes -o ConnectTimeout=6 git@git.openbureau.ch)",
"Bash(bash /tmp/claude-1000/-home-karim-kgva/2d6c60c0-d07a-4092-8118-4f88d75db9c3/scratchpad/pssh.sh 'pct exec 104 -- sh -lc \"grep -rn -A4 karimgabrielevarano /etc/caddy/ 2>/dev/null | head -40\"')",
"Bash(bash /tmp/claude-1000/-home-karim-kgva/2d6c60c0-d07a-4092-8118-4f88d75db9c3/scratchpad/pssh.sh 'pct exec 130 -- sh -lc \"\\(nginx -v\\) 2>&1; \\(caddy version\\) 2>&1; echo files:; ls /var/www/karimgabrielevarano.xyz | head\"')",
"Bash(bash /tmp/claude-1000/-home-karim-kgva/2d6c60c0-d07a-4092-8118-4f88d75db9c3/scratchpad/pssh.sh 'ls /var/lib/vz/template/cache/ 2>/dev/null; pveam list local 2>/dev/null | grep -i debian | head')",
"Bash(bash /tmp/claude-1000/-home-karim-kgva/2d6c60c0-d07a-4092-8118-4f88d75db9c3/scratchpad/pssh.sh 'pvesm status 2>/dev/null')",
"Bash(bash /tmp/claude-1000/-home-karim-kgva/2d6c60c0-d07a-4092-8118-4f88d75db9c3/scratchpad/pssh.sh 'bash -lc '\\\\'' *)",
"Bash(bash /tmp/claude-1000/-home-karim-kgva/2d6c60c0-d07a-4092-8118-4f88d75db9c3/scratchpad/pssh.sh 'rm -f /tmp/kgva-site.tgz && echo \"node temp removed\"; echo \"rollback snapshot still present:\"; zfs list -t snapshot | grep kgva-website')",
"Bash(shred -u /tmp/claude-1000/-home-karim-kgva/2d6c60c0-d07a-4092-8118-4f88d75db9c3/scratchpad/ap.sh)",
"Bash(rm -f __TRACKED_VAR__/site.tgz)",
"Bash(bash /tmp/claude-1000/-home-karim-kgva/2d6c60c0-d07a-4092-8118-4f88d75db9c3/scratchpad/pssh.sh 'pct exec 120 -- sh -c \"cd /var/lib/gitea && su gitea -s /bin/bash -c \\\\\"/usr/local/bin/gitea admin user list --config /etc/gitea/app.ini\\\\\" 2>&1\"')"
],
"additionalDirectories": [
"/home/karim/.ssh"
]
}
}
+6
View File
@@ -0,0 +1,6 @@
public/
resources/
.git/
.github/
node_modules/
*.log
View File
+68
View File
@@ -0,0 +1,68 @@
# Admin (Decap CMS)
A lightweight, git-based editor at `/admin`. It commits markdown straight to the
Gitea repo — no database, no server. The public site stays static; rebuilding it
after an edit is a separate step (see *Deploy* below).
## 1. Put the repo on Gitea
The site must live in a Gitea repo (e.g. `git.kgva.ch/karim/kgva`). Set the same
`repo` and `branch` in `static/admin/config.yml`.
## 2. Create a Gitea OAuth app
In Gitea: **Settings → Applications → Create OAuth2 application**.
- Redirect URI: `https://<your-site>/admin/`
- Copy the **Client ID** into `static/admin/config.yml``backend.app_id`.
Decap uses PKCE, so no client secret and no auth proxy are needed.
## 3. Edit
Open `https://<your-site>/admin/`, log in with Gitea. New project:
*Portfolio → New Project*; images upload into `static/img` and are referenced as
`/img/...`. Saving commits to the repo on the configured branch.
## 4. Deploy (rebuild after an edit)
Because the site is baked into the Docker image, a commit must trigger a rebuild.
Pick one:
- **Manual:** on the host, `docker compose up -d --build`.
- **Gitea Actions** (`.gitea/workflows/deploy.yml`), sketch:
```yaml
on: { push: { branches: [main] } }
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker compose up -d --build # on a runner with access to the host
```
(Adjust to your Proxmox/LXC setup — same place OPENBUREAU deploys.)
- **Webhook:** point a Gitea webhook at a tiny script on the host that runs the
`docker compose` line.
## Local editing (no auth)
```
npx decap-server # in one terminal
# uncomment `local_backend: true` in config.yml
hugo server # in another
# open http://localhost:1313/admin/
```
## Self-hosting the CMS bundle (optional)
To avoid the CDN (in the spirit of the colophon), download the bundle once and
point `static/admin/index.html` at it:
```
curl -L https://unpkg.com/decap-cms@^3.3.3/dist/decap-cms.js \
-o static/admin/decap-cms.js
# then in index.html: <script src="/admin/decap-cms.js"></script>
```
+12
View File
@@ -0,0 +1,12 @@
# ── build the static site with Hugo (extended) ───────────────────────────
# pin a version if you want reproducible builds, e.g. hugomods/hugo:exts-0.163.3
FROM hugomods/hugo:exts AS build
WORKDIR /src
COPY . .
RUN hugo --gc --minify
# ── serve the static output with nginx ────────────────────────────────────
FROM nginx:alpine
COPY --from=build /src/public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
+5
View File
@@ -0,0 +1,5 @@
---
title: "{{ replace .Name "-" " " }}"
date: {{ .Date }}
draft: true
---
+16
View File
@@ -0,0 +1,16 @@
---
title: "{{ replace .Name "-" " " }}"
slug: "{{ .Name }}"
date: {{ .Date }}
draft: true
thumbnail: "/img/{{ .Name }}/thumbnail.webp"
studies: "/studies/hslu/ba/semester_00"
schools: ["hslu"]
degrees: ["ba"]
semesters: ["semester_00"]
images:
- { src: "/img/{{ .Name }}/01.webp", name: "01.webp" }
- { src: "/img/{{ .Name }}/02.webp", name: "02.webp" }
---
a short description of the project.
+312
View File
@@ -0,0 +1,312 @@
/* ══════════════════════════════════════════════════════════════
custom.css — extends shibui WITHOUT touching the theme.
- top bar reads like a terminal prompt: "/ karimgabrielevarano_ ls"
- clicking "ls" lists the pages, stacked, inverted on hover
- only the work index breaks the narrow column
remove this file → original shibui look returns.
══════════════════════════════════════════════════════════════ */
/* ─────────────────────── SHELL PROMPT ─────────────────────── */
.prompt {
font-family: var(--font-family-mono);
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 0;
line-height: 1.5;
}
.prompt__user { font-weight: 700; }
.prompt__path { color: var(--color-text-muted); }
/* breadcrumb segments look like plain prompt text at rest, fill on hover */
.prompt__seg {
margin-left: 0;
border-bottom: none;
background: none;
color: inherit;
}
.prompt__seg:hover {
background: var(--color-text-primary);
color: var(--color-bg-primary);
border-bottom: none;
}
/* the typeable command — looks like shell text, native caret hidden so our
blinking "_" is the cursor */
.prompt__input {
margin-left: 0.6ch;
min-width: 0;
font-weight: 700;
color: var(--color-text-primary);
white-space: pre;
outline: none;
caret-color: transparent;
}
/* blinking terminal cursor, sits right after the typed text */
.caret {
font-weight: 700;
color: var(--color-text-primary);
animation: blink 1.1s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
/* always-open file menu (the shell's "ls") — only shown on the home page;
on subpages it stays in the DOM (hidden) so typed commands still resolve */
.menu { margin: var(--spacing-sm) 0 0; }
body:not(.home) .menu { display: none; }
/* easter-egg output line — takes no space until it actually has output */
.prompt__out {
font-family: var(--font-family-mono);
color: var(--color-text-muted);
margin: 0;
white-space: pre-wrap;
}
.prompt__out:not(:empty) { margin: 0.35rem 0 0; }
/* ─────────────────────── HOME IMAGE ─────────────────────── */
/* sits low enough that opening the ls overlay never moves it */
.home .hero__img { margin: 2rem 0 0; text-align: left; }
.hero__img img { width: 100%; height: auto; margin: 0; }
/* ─────────────────── WORK INDEX (ls -l style) ─────────────────── */
.tui { font-family: var(--font-family-mono); }
.tui__cmd { margin: 0; color: var(--color-text-muted); }
.tui__sym { color: var(--color-text-muted); }
.tui__total { margin: 0 0 0.6rem; }
.tui__ls { list-style: none; margin: 0; padding: 0; }
.tui__ls li { margin: 0; }
.tui__ls a {
display: grid;
grid-template-columns: 11ch 5ch 1fr;
gap: 0 1ch;
margin: 0;
padding: 0 0.25em;
border-bottom: none;
color: var(--color-text-primary);
white-space: nowrap;
}
.tui__perm,
.tui__date { color: var(--color-text-muted); }
.tui__ls a:hover,
.tui__ls a.is-selected {
background: var(--color-text-primary);
color: var(--color-bg-primary);
}
.tui__ls a:hover .tui__perm,
.tui__ls a:hover .tui__date,
.tui__ls a:hover .tui__name,
.tui__ls a.is-selected .tui__perm,
.tui__ls a.is-selected .tui__date,
.tui__ls a.is-selected .tui__name { color: var(--color-bg-primary); }
/* preview pane (ranger-style) below the listing */
.tui__preview { margin: 1.25rem 0 0; }
.tui__preview img {
width: 100%;
aspect-ratio: 3 / 2;
object-fit: cover;
height: auto;
margin: 0;
display: block;
transition: opacity 0.25s ease;
}
/* project page: image files (no date column), show full image (contain) */
.tui--files .tui__ls a { grid-template-columns: 11ch 1fr; }
/* long listings (>8 entries) scroll instead of pushing the preview down */
.tui--files .tui__ls {
max-height: 12em; /* ~8 rows at line-height 1.5 */
overflow-y: auto;
}
.tui--files .tui__preview img {
aspect-ratio: auto; /* keep each image's own ratio — no letterboxing */
width: auto;
max-width: 100%;
max-height: 80vh;
height: auto;
object-fit: contain;
background: none;
}
/* info.md text preview */
.tui__text { font-size: var(--font-size-base); text-align: left; }
.tui__text p { text-align: left; }
.tui__text > :first-child { margin-top: 0; }
.tui__text > :last-child { margin-bottom: 0; }
/* prev/next as shell "cd" commands — same vocabulary as the rest of the site */
.proj-nav {
font-family: var(--font-family-mono);
margin: 1.5rem 0 0;
display: flex;
gap: 1.5ch;
}
.proj-nav a[rel="next"] { margin-left: auto; } /* → always pinned right */
.proj-nav a {
border-bottom: none;
color: var(--color-text-muted);
}
.proj-nav a:hover {
background: var(--color-text-primary);
color: var(--color-bg-primary);
border-bottom: none;
}
/* studies path → clickable taxonomy filters, but indistinguishable from plain
text (no underline, no hover, inherits the surrounding colour) */
.proj-path__seg,
.proj-path__seg:hover {
margin-left: 0;
border-bottom: none;
background: none;
color: inherit;
}
.proj-path__seg--last { font-weight: 700; }
/* secret `doom` — a small window floating over the site, sized to the 4:3 game */
.doom-window {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(92vw, 640px);
z-index: 9999;
display: flex;
flex-direction: column;
background: #000;
border: 1px solid var(--color-text-primary);
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.45);
}
.doom-window__bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0.25rem 0.25rem 0.6rem;
font-family: var(--font-family-mono);
font-size: var(--font-size-small);
color: #fff;
background: #000;
cursor: move;
user-select: none;
}
.doom-window__close {
cursor: pointer;
color: #fff;
background: none;
border: none;
font: inherit;
line-height: 1;
padding: 0 0.6ch;
}
.doom-window__close:hover { color: #000; background: #fff; }
.doom-window iframe {
width: 100%;
aspect-ratio: 4 / 3;
height: auto;
border: 0;
background: #000;
display: block;
}
/* about: terminal-style metadata / contact list */
.meta {
display: grid;
grid-template-columns: max-content 1fr;
column-gap: 1.5ch;
row-gap: 0.15rem;
font-family: var(--font-family-mono);
margin: var(--spacing-base) 0 0;
}
.meta dt { color: var(--color-text-muted); }
.meta dd { margin: 0; }
.meta a { margin-left: 0; }
/* `dark` / `light` theme toggle — overrides the theme's colour variables */
html.dark {
color-scheme: dark;
--color-bg-primary: hsl(0 0% 8%);
--color-bg-secondary: hsl(0 0% 12%);
--color-border: hsl(0 0% 22%);
--color-selection-bg: hsl(0 0% 32%);
--color-text-primary: hsl(0 0% 92%);
--color-text-muted: hsl(0 0% 58%);
--color-text-code: hsl(0 0% 78%);
}
/* `sl` — a steam locomotive that rolls across the screen */
.sl {
position: fixed;
top: 38%;
left: 0;
margin: 0;
z-index: 9998;
white-space: pre;
font-family: var(--font-family-mono);
font-size: 12px;
line-height: 1;
color: var(--color-text-primary);
pointer-events: none;
}
/* boot splash — systemd/Arch-style log, but in the site's own look:
light background, same mono font, constrained to the site column width */
#boot { display: none; }
html.booting #boot {
display: block;
position: fixed;
inset: 0;
z-index: 10000;
overflow-y: auto;
scrollbar-width: none;
padding: 0.8rem 0;
background: var(--color-bg-primary);
cursor: none;
}
html.booting #boot::-webkit-scrollbar { display: none; }
.boot__log {
width: 100%;
max-width: var(--container-width);
margin: 0 auto;
padding: 0 1.5rem;
border: 0;
background: none;
box-shadow: none;
border-radius: 0;
font-family: var(--font-family-mono);
font-size: var(--font-size-small);
line-height: 1.6;
color: var(--color-text-primary);
white-space: pre-wrap;
word-break: break-word;
}
.boot__line { white-space: pre-wrap; }
.boot__ok { font-weight: 700; }
.boot__dim { color: var(--color-text-muted); }
/* text isn't selectable anywhere — except the command line, which you type in */
body {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.prompt__input {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
}
/* all images always in colour (selection drives what's shown, not hover) */
img { filter: none; }
/* make the [hidden] attribute win over .tui__preview img { display:block } */
[hidden] { display: none !important; }
.tui__status {
font-family: var(--font-family-mono);
font-size: var(--font-size-small);
color: var(--color-text-muted);
margin-top: 0.4rem;
}
/* frozen/locked preview indicator */
.filebrowser.is-frozen .tui__status { color: var(--color-text-primary); }
.filebrowser.is-frozen .tui__status::after { content: " ● locked"; }
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
---
title: "startpage"
description: "architecture studio"
heroImage: "/img/Triest_Rozzol_Melara.webp"
---
+19
View File
@@ -0,0 +1,19 @@
---
title: "about"
description: "about karim gabriele varano"
layout: "about"
---
my name is karim gabriele varano. i grew up in the rhine valley of st. gallen. before architecture, i worked in electrical engineering, on site and in planning. i went to design school, and from there into architecture studies at ost and hslu. i hold a ba in architecture and i'm currently working towards my master's. what draws me to architecture is its depth. it moves between the weight of a material and the weight of a social crisis. it borrows from engineering, sociology, art, politics. it is not a science. it is where sciences meet, and where that meeting has to result in something real and inhabitable.
i'm open to collaborations in architecture. if you're building something, [let's talk](mailto:hi@karimgabrielevarano.xyz).
<dl class="meta">
<dt>location</dt><dd>lucerne, switzerland</dd>
<dt>degree</dt><dd>ba architecture</dd>
<dt>status</dt><dd>ma in progress</dd>
<dt>available</dt><dd>architecture collaborations · commissions · open-source</dd>
<dt>email</dt><dd><a href="mailto:hi@karimgabrielevarano.xyz">hi@karimgabrielevarano.xyz</a></dd>
<dt>studio</dt><dd><a href="/studio/">studio</a></dd>
<dt>open</dt><dd><a href="/openbureau/">openbureau</a></dd>
</dl>
+17
View File
@@ -0,0 +1,17 @@
---
title: "colophon"
description: "how this site is made"
dotfile: true
---
this site is a small terminal.
it's static. hugo turns my text into plain html files, and that's all the server ever hands out. no database, no backend, nothing to log into. it runs on almost any host.
the bar at the top is a real shell. you can type in it. work, about, studio, openbureau take you to those pages. help shows the basics, tab completes a word, the up arrow brings back what you typed before. not everything it knows is in the list. every page is also a file listing, so you browse the projects, and even the legal notice, like directories and read them on the spot.
the rest is small things. the type is iosevka curly slab, hosted here and not pulled from anyone. the cursor is drawn pixel by pixel. there's a dark mode if you type dark. nothing is tracked, no cookies, no analytics.
it follows the same idea as [openbureau](/openbureau/): own your tools, keep your formats readable. the content is cc by-sa, hugo and the theme are open source (the [legal notice](/legal-notice/) has the details), and the site builds into a docker image so anyone can run their own copy.
written between the rhine valley and lucerne.
+30
View File
@@ -0,0 +1,30 @@
---
title: "cv"
description: "education, experience, skills"
---
**education**
<dl class="meta">
<dt>····</dt><dd>ma architecture · hslu · in progress</dd>
<dt>····</dt><dd>ba architecture · ost &amp; hslu</dd>
<dt>····</dt><dd>design school</dd>
</dl>
**experience**
<dl class="meta">
<dt>····</dt><dd>electrical engineering · site &amp; planning</dd>
</dl>
**skills**
<dl class="meta">
<dt>design</dt><dd>architecture · graphic · editorial · web</dd>
<dt>open source</dt><dd>curation, self-hosting, ICT setup &amp; support</dd>
<dt>software</dt><dd>rhino · hugo · ····</dd>
</dl>
---
[hi@karimgabrielevarano.xyz](mailto:hi@karimgabrielevarano.xyz)
+6
View File
@@ -0,0 +1,6 @@
---
title: "legal notice"
description: "legal notice, privacy policy and licences"
layout: "legal"
dotfile: true
---
+5
View File
@@ -0,0 +1,5 @@
---
build:
render: never
list: never
---
+12
View File
@@ -0,0 +1,12 @@
---
build:
render: never
list: never
---
**DISCLAIMER**
the author assumes no liability for the accuracy, completeness, or timeliness of the content provided. any claims for damages arising from the use or non-use of the information offered are excluded.
operators of externally linked pages bear sole responsibility for their content. no legal violations were apparent at the time of linking. should any violation become known, the link will be removed immediately.
architectural drawings and project documentation published on this website are shared for informational and portfolio purposes only. they do not constitute building permits, structural approvals, or professional advice.
+16
View File
@@ -0,0 +1,16 @@
---
build:
render: never
list: never
---
**LICENCES**
**WEBSITE CONTENT**\
all texts, images, and project documentation on this website — unless otherwise noted — are published under the [creative commons attribution-sharealike 4.0 international (cc by-sa 4.0)](https://creativecommons.org/licenses/by-sa/4.0/) licence. you are free to share and adapt the material, provided you give appropriate credit and distribute your contributions under the same licence.
**FRAMEWORK — HUGO**\
this website was built with [hugo](https://gohugo.io), an open-source static site generator, licensed under the [apache license 2.0](https://github.com/gohugoio/hugo/blob/master/LICENSE).
**THEME — SHIBUI**\
the theme shibui by nguyen tuan khanh is licensed under the [mit license](https://github.com/ntk148v/shibui/blob/master/LICENSE).
+26
View File
@@ -0,0 +1,26 @@
---
build:
render: never
list: never
---
**PRIVACY POLICY**
the protection of your personal data is important to me. this policy explains how data is handled on this website, in accordance with the swiss federal act on data protection (revfadp, in force since 1 september 2023).
**DATA CONTROLLER**\
karim gabriele varano, fluhmühlerain 1, 6015 lucerne — contact details as above.
**SERVER LOG**\
the hosting provider automatically collects technical access data in server log files. this includes pages visited, date and time, browser type, operating system, referrer url, and ip address. this data is used solely for technical and statistical purposes and is not merged with other data sources or used for marketing.
**CONTACT BY EMAIL**\
if you contact me by email, your details will be stored for the purpose of processing your enquiry. this data will not be passed on to third parties without your consent and will be deleted upon conclusion of the correspondence, unless a statutory retention obligation applies.
**YOUR RIGHTS**\
under the revfadp, you have the right to request information about your stored personal data, as well as its correction, deletion, or restriction of processing. please contact me at the address above.
you also have the right to lodge a complaint with the competent supervisory authority — the federal data protection and information commissioner (fdpic): [www.edoeb.admin.ch](https://www.edoeb.admin.ch)
**CHANGES**\
this privacy policy may be updated as needed. the current version is always available on this page.
+11
View File
@@ -0,0 +1,11 @@
---
build:
render: never
list: never
---
**RESPONSIBLE FOR CONTENT**
karim gabriele varano\
fluhmühlerain 1, 6015 lucerne, switzerland\
[hi@karimgabrielevarano.xyz](mailto:hi@karimgabrielevarano.xyz)
+26
View File
@@ -0,0 +1,26 @@
---
title: "openbureau"
description: "open and self-owned tools for architecture"
---
architecture runs on tools. the tools shape what gets thought, drawn, and built. most are proprietary: subscriptions that can be cancelled, file formats only their owners can read, work that disappears when a license lapses. in a profession that already runs on precarious labor, that's rent extraction.
free and open source software is the cleanest answer, but not the only one. what i'm after is ownership and openness. tools you own instead of rent. file formats anyone can read. data that doesn't sit behind a paywall. code as a commons when possible. when it isn't, at least open data.
openbureau is where i collect what i build along these lines. small tools, written for my own practice, released so other people can use or fork them.
beyond what i build, openbureau is also a service: i curate open-source software that carries no fixed costs and no lock-in, set it up, and support it, so a studio or small practice can run on tools it owns instead of subscriptions it rents.
**RAPPORT**\
2026
a studio management tool for small architecture offices. project management, time tracking, quotes and invoicing, bookkeeping, employee records, payroll, meeting minutes. all in one place instead of five subscriptions patched together. what exists is either too small (single-purpose apps that don't talk to each other) or too big (enterprise suites built for offices of a hundred people, with a budget to match). small practices end up paying for software designed for someone else and filling the gaps with spreadsheets. rapport is the tool i wanted to use myself. free software, self-hostable, no per-seat licensing. your data stays on your devices.
[repo](https://rapport.openbureau.ch)
**DOSSIER**\
2026
a rhino plugin for early-phase architectural design. walls, doors, windows, slabs, stairs, levels, and a 2d drawing layer that feels like a 2d drawing layer. native rhino objects, no parallel database to sync. what exists is either expensive proprietary software, or full bim systems built for office-scale documentation phases that don't fit how you actually think during a sketch. open source bim tools live in blender or freecad and follow a 3d-first logic — useful, but not how you 2d draw a plan. students and small offices end up either paying, pirating, or working around in plain rhino. dossier is the tool i wanted to use myself during my master's and my own work. why rhino and not a fully open source cad? rhino sells perpetual licences you actually own instead of subscriptions you rent, and its file formats (3dm, opennurbs) and apis (rhinocommon, eto.forms) are well documented and readable outside the application. that's not free software, but it's a workable position on ownership and open data and where some architects already work. freecad or librecad would be more philosophically open, but adoption in architecture is marginal. dossier meets the discipline where it is. open data, established community, enough infrastructure to build on.
[repo](https://dossier.openbureau.ch)
+4
View File
@@ -0,0 +1,4 @@
---
title: "portfolio"
description: "selected projects"
---
+19
View File
@@ -0,0 +1,19 @@
---
title: "dojo"
slug: "dojo"
date: 2023-01-10
thumbnail: "/img/sem02/thumbnail.webp"
studies: "/studies/ost/ba/semester_02"
schools: ["ost"]
degrees: ["ba"]
semesters: ["semester_02"]
images:
- { src: "/img/sem02/01.webp", name: "timber-model.webp" }
- { src: "/img/sem02/02.webp", name: "long-elevation.webp" }
- { src: "/img/sem02/03.webp", name: "section.webp" }
- { src: "/img/sem02/04.webp", name: "front-elevation.webp" }
- { src: "/img/sem02/05.webp", name: "side-elevation.webp" }
- { src: "/img/sem02/06.webp", name: "joint-axonometric.webp" }
---
at the centre of this student project is the constructive exploration of the timber joint, not as a static detail but as a spatially effective system. starting from a basic joint that works without diagonal bracing and relies solely on horizontal and vertical beams, a spatial principle was developed that is both rational and atmospheric. the particularity lies in its scalability: the identical joint is applied at different scales, from the fine grid of the facade structure to the load-bearing order of the two-storey building. this creates a continuous constructive system that carries the entire building while simultaneously making itself visible. the dojo is organised around an open inner courtyard. the clear timber construction mediates between openness and concentration, between lightness and rigour, qualities derived from the martial arts. the design understands itself as an architectural reflection on structure, discipline, and spatial perception.
+21
View File
@@ -0,0 +1,21 @@
---
title: "museo"
slug: "museo"
date: 2022-01-10
thumbnail: "/img/sem01/thumbnail.webp"
studies: "/studies/ost/ba/semester_01"
schools: ["ost"]
degrees: ["ba"]
semesters: ["semester_01"]
images:
- { src: "/img/sem01/01.webp", name: "cutaway-model-front.webp" }
- { src: "/img/sem01/08.webp", name: "exploded-axonometric.webp" }
- { src: "/img/sem01/02.webp", name: "plan-ground-floor.webp" }
- { src: "/img/sem01/03.webp", name: "plan-upper-floor.webp" }
- { src: "/img/sem01/04.webp", name: "section.webp" }
- { src: "/img/sem01/05.webp", name: "cutaway-model-angle.webp" }
- { src: "/img/sem01/06.webp", name: "site-plan.webp" }
- { src: "/img/sem01/07.webp", name: "street-linocut.webp" }
---
tonhalle museo is a speculative student project at the intersection of architecture, sound, and theory. two seemingly contradictory programmes, concert hall and museum, converge within a single building. this convergence is not harmonised but deliberately intensified: different spatial logics, usage dynamics, and atmospheres collide, creating a field of tension that shapes the architectural expression. the design is based on an auditory analysis, a five-minute sound sequence served as the starting point for the spatial structure. the focus was not on function or form, but on the translation of sound into spatial dimensions. volume, rhythm, density, and silence were translated into proportions, transitions, and materiality. the result is a built fragment, a deliberately incomplete idea, a spatial body that does not seek to explain, but rather to make itself experiential. tonhalle museo understands itself as an attempt to conceive architecture through listening, as a spatial score between concentration and distraction, between performance and contemplation.
+29
View File
@@ -0,0 +1,29 @@
---
title: "studytrip to trieste"
slug: "studytrip-trieste"
date: 2024-09-27
thumbnail: "/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-01.webp"
studies: "/studies/hslu/ba/semester_05"
schools: ["hslu"]
degrees: ["ba"]
semesters: ["semester_05"]
images:
- { src: "/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-01.webp", name: "canale-grande-01.webp" }
- { src: "/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-02.webp", name: "canale-grande-02.webp" }
- { src: "/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-03.webp", name: "canale-grande-03.webp" }
- { src: "/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-04.webp", name: "canale-grande-04.webp" }
- { src: "/img/studytrip-trieste/Triest_Casa_Bartoli-02.webp", name: "casa-bartoli.webp" }
- { src: "/img/studytrip-trieste/Triest_Cpo_Androna_Campo_Marzio-01.webp", name: "campo-marzio.webp" }
- { src: "/img/studytrip-trieste/Triest_Kathedrale_San_Giusto-02.webp", name: "san-giusto.webp" }
- { src: "/img/studytrip-trieste/Triest_Liceo_Artistico-02.webp", name: "liceo-artistico-01.webp" }
- { src: "/img/studytrip-trieste/Triest_Liceo_Artistico-03.webp", name: "liceo-artistico-02.webp" }
- { src: "/img/studytrip-trieste/Triest_Liceo_Artistico-05.webp", name: "liceo-artistico-03.webp" }
- { src: "/img/studytrip-trieste/Triest_Liceo_Artistico-07.webp", name: "liceo-artistico-04.webp" }
- { src: "/img/studytrip-trieste/Triest_Palmanova-01.webp", name: "palmanova.webp" }
- { src: "/img/studytrip-trieste/Triest_Rozzol_Melara-01.webp", name: "rozzol-melara-01.webp" }
- { src: "/img/studytrip-trieste/Triest_Rozzol_Melara-02.webp", name: "rozzol-melara-02.webp" }
- { src: "/img/studytrip-trieste/Triest_Rozzol_Melara-03.webp", name: "rozzol-melara-03.webp" }
- { src: "/img/studytrip-trieste/Triest_Rozzol_Melara-04.webp", name: "rozzol-melara-04.webp" }
- { src: "/img/studytrip-trieste/Triest_Rozzol_Melara-05.webp", name: "rozzol-melara-05.webp" }
- { src: "/img/studytrip-trieste/Triest_Stanjel-01.webp", name: "stanjel.webp" }
---
+20
View File
@@ -0,0 +1,20 @@
---
title: "textilsteinwerk"
slug: "textilsteinwerk"
date: 2025-01-10T10:00:00
thumbnail: "/img/sem05/03.webp"
studies: "/studies/hslu/ba/semester_05"
schools: ["hslu"]
degrees: ["ba"]
semesters: ["semester_05"]
images:
- { src: "/img/sem05/01.webp", name: "context-model.webp" }
- { src: "/img/sem05/02.webp", name: "sectional-model.webp" }
- { src: "/img/sem05/03.webp", name: "hall-interior.webp" }
- { src: "/img/sem05/04.webp", name: "facade-model.webp" }
- { src: "/img/sem05/05.webp", name: "dwelling-interior.webp" }
- { src: "/img/sem05/section01.webp", name: "long-section.webp" }
- { src: "/img/sem05/section02.webp", name: "hall-section.webp" }
---
the séchoir-halle is a student project exploring the themes of commerce, existing structures, and adaptive reuse within former industrial architecture. a former industrial building rooted in local textile history is preserved in its identity while being given a new programme. where viscose fabrics were once hung to dry, new building blocks made from recycled textiles are produced today — load-bearing elements crafted from old fabrics using a ramming process, based on circular economy principles and material reuse. the existing structure is retained and complemented by an additional storey above one half of the hall, continuing its raw, functional atmosphere rather than replacing it. new residential units in direct proximity engage in dialogue with the industrial architecture, creating a mixed-use quarter with productive, social, and residential functions.
@@ -0,0 +1,7 @@
---
title: "the shadow over emmen"
slug: "the-shadow-over-emmen"
date: 2025-01-10T09:00:00
thumbnail: "/img/shadowofemmen/thumbnail.webp"
vimeo: "1184987222"
---
+20
View File
@@ -0,0 +1,20 @@
---
title: "studio"
description: "design across scales, and the open tools underneath"
---
i work independently across scales, from buildings to books to screens, and the open infrastructure underneath them. it sounds like several jobs, but it's one: design with a bias toward things you own rather than rent, toward formats anyone can read, toward work that outlives a licence. the same logic that runs [openbureau](/openbureau/).
**architecture**\
concept and design work, from early studies to drawings.
**open-source ICT**, via [openbureau](/openbureau/)\
i curate, set up, and support open-source software that carries no fixed costs, so a studio runs on tools it owns instead of subscriptions it rents.
**graphic, editorial & web**\
visual identity, graphic design, book and publication layout, and websites built to be fast, readable, and yours, like this one.
---
**available** for architecture collaborations, commissions, consulting, and open-source work.\
[hi@karimgabrielevarano.xyz](mailto:hi@karimgabrielevarano.xyz)
+9
View File
@@ -0,0 +1,9 @@
# Caddy site block for the website. Replace the upstream IP with the NEW
# container's IP, reload caddy (`caddy reload` / `systemctl reload caddy`),
# verify, then retire the old container. Keeping the domain here means the
# cutover is a one-line upstream change — no IP recycling needed.
karimgabrielevarano.xyz, www.karimgabrielevarano.xyz {
encode zstd gzip
reverse_proxy CHANGE_ME_NEW_CONTAINER_IP:8080
}
+32
View File
@@ -0,0 +1,32 @@
# Deploy
The site is a static Hugo build served by nginx in one container
(`docker compose up -d --build`). Recommended hosting: an LXC on Proxmox, with
Caddy in front terminating TLS and reverse-proxying the domain.
## Prerequisites
1. The repo must be on Gitea (e.g. `git.kgva.ch/karim/kgva`). The `REPO` URL in
`provision-lxc.sh` and `backend.repo` in `static/admin/config.yml` point there.
## Safe cutover (no downtime, with rollback)
1. On the Proxmox node, run (free CTID, **new/temporary** IP):
```
CTID=141 IP=10.0.0.50/24 GW=10.0.0.1 BRIDGE=vmbr0 STORAGE=local-lvm \
./provision-lxc.sh
```
2. Test it directly: `curl -I http://10.0.0.50:8080`
3. Point Caddy's upstream at the new IP (`deploy/Caddyfile`), reload Caddy,
open the domain, click around.
4. Only once it's verified: stop/remove the **old** container
(`pct stop <old> && pct destroy <old>`), or keep it a day as rollback.
This never deletes the live site before the replacement is proven.
## Updating later
```
pct exec <CTID> -- bash -lc 'cd /opt/kgva && git pull && docker compose up -d --build'
```
or wire a Gitea Action / webhook to run that on push (see ../ADMIN.md).
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Provision an LXC for the kgva website (Docker + compose). RUN ON THE PROXMOX NODE.
#
# SAFE CUTOVER (recommended): create this NEW container on a FREE/temporary IP,
# test it, then point Caddy's upstream at it. Remove the OLD container only after
# the new one is verified. Do NOT reuse the old IP up front (IP conflict + no
# rollback). Override any value via env, e.g.: CTID=141 IP=10.0.0.50/24 ./provision-lxc.sh
set -euo pipefail
CTID="${CTID:-141}" # must be free: pct status $CTID → should error
HOSTNAME_="${HOSTNAME_:-kgva}"
IP="${IP:-CHANGE_ME/24}" # NEW/temporary ip, NOT the old one yet
GW="${GW:-CHANGE_ME}"
BRIDGE="${BRIDGE:-vmbr0}"
STORAGE="${STORAGE:-local-lvm}"
TEMPLATE="${TEMPLATE:-local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst}"
REPO="${REPO:-https://git.kgva.ch/karim/kgva.git}"
DISK="${DISK:-6}"; CORES="${CORES:-2}"; RAM="${RAM:-1024}"
echo "Creating LXC $CTID ($HOSTNAME_) on $IP ..."
pct create "$CTID" "$TEMPLATE" \
--hostname "$HOSTNAME_" --cores "$CORES" --memory "$RAM" \
--rootfs "${STORAGE}:${DISK}" \
--net0 "name=eth0,bridge=${BRIDGE},ip=${IP},gw=${GW}" \
--features nesting=1 --unprivileged 1 --onboot 1
pct start "$CTID"
sleep 6
pct exec "$CTID" -- bash -lc '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get install -y ca-certificates curl git
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release; echo $VERSION_CODENAME) stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
rm -rf /opt/kgva && git clone '"$REPO"' /opt/kgva
cd /opt/kgva && docker compose up -d --build
'
echo
echo "done. test the new container: curl -I http://${IP%/*}:8080"
echo "then point Caddy at ${IP%/*}:8080 (see deploy/Caddyfile), reload caddy,"
echo "verify the domain, and only THEN remove the old container."
+8
View File
@@ -0,0 +1,8 @@
services:
web:
build: .
image: kgva
container_name: kgva
ports:
- "8080:80"
restart: unless-stopped
+52
View File
@@ -0,0 +1,52 @@
baseURL = "https://karimgabrielevarano.xyz/"
defaultContentLanguage = "en"
title = "karimgabrielevarano"
enableEmoji = true
enableRobotsTXT = true
[taxonomies]
school = "schools"
degree = "degrees"
semester = "semesters"
[languages]
[languages.en]
locale = "en_US"
weight = 1
[params]
description = "architecture studio"
email = "hi@karimgabrielevarano.xyz"
author = "karim gabriele varano"
ogImage = "/img/Triest_Rozzol_Melara.webp"
# shell prompt identity (root@…, karim@…, etc.)
shellUser = "karimgabrielevarano"
shellHost = "xyz"
[menu]
[[menu.main]]
name = "portfolio"
url = "/portfolio/"
weight = 1
[[menu.main]]
name = "about"
url = "/about/"
weight = 2
[[menu.main]]
name = "studio"
url = "/studio/"
weight = 3
[[menu.main]]
name = "openbureau"
url = "/openbureau/"
weight = 4
[markup]
[markup.goldmark.renderer]
unsafe = true
[markup.tableOfContents]
startLevel = 2
endLevel = 3
[permalinks]
portfolio = "/portfolio/:slug/"
+16
View File
@@ -0,0 +1,16 @@
{{ define "bodyclass" }}page-404{{ end }}
{{ define "main" }}
<div class="tui">
<p class="tui__cmd"><span class="tui__sym">$</span> cd <span class="err404">.</span></p>
<p class="tui__total">cd: no such file or directory: <span class="err404">.</span></p>
<p>error 404 — nothing lives here.</p>
<p><a href="{{ "/" | relURL }}">cd ~</a></p>
</div>
<script>
(function () {
var p = (location.pathname || "/").replace(/\/$/, "") || "/";
var n = document.querySelectorAll(".err404");
for (var i = 0; i < n.length; i++) n[i].textContent = p;
})();
</script>
{{ end }}
+27
View File
@@ -0,0 +1,27 @@
{{ define "bodyclass" }}page-about{{ end }}
{{ define "main" }}
{{ $cv := .Site.GetPage "/cv" }}
<div class="tui tui--files filebrowser">
<p class="tui__cmd"><span class="tui__sym">$</span> ls</p>
<p class="tui__cmd tui__total">total {{ if $cv }}2{{ else }}1{{ end }}</p>
<ul class="tui__ls">
<li><a href="{{ "/" | relURL }}" data-name="..">
<span class="tui__perm">drwxr-xr-x</span><span class="tui__name">..</span>
</a></li>
<li><a data-name="info.md" data-text="info.md" class="is-selected">
<span class="tui__perm">-rw-r--r--</span><span class="tui__name">info.md</span>
</a></li>
{{ if $cv }}
<li><a data-name="cv.md" data-text="cv.md">
<span class="tui__perm">-rw-r--r--</span><span class="tui__name">cv.md</span>
</a></li>
{{ end }}
</ul>
<figure class="tui__preview">
<div class="tui__text" data-text="info.md">{{ .Content }}</div>
{{ if $cv }}<div class="tui__text" data-text="cv.md" hidden>{{ $cv.Content }}</div>{{ end }}
<figcaption class="tui__status">info.md</figcaption>
</figure>
</div>
{{ end }}
+20
View File
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="{{ .Site.Language.Lang | default "en" }}" dir="ltr">
{{ partial "head.html" . }}
<body class="{{ block "bodyclass" . }}{{ end }}">
<div id="boot" aria-hidden="true"><pre class="boot__log"></pre></div>
<a href="#main-content" class="skip-link">Skip to content</a>
{{ partial "header.html" . }}
<main id="main-content" role="main">
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
<script src="{{ "js/cursor.js" | relURL }}" defer></script>
<script src="{{ "js/shell.js" | relURL }}" defer></script>
<script src="{{ "js/filebrowser.js" | relURL }}" defer></script>
<script src="{{ "js/nav.js" | relURL }}" defer></script>
<script src="{{ "js/boot.js" | relURL }}" defer></script>
<script>document.addEventListener('contextmenu',function(e){e.preventDefault();});</script>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
{{ define "bodyclass" }}page-legal{{ end }}
{{ define "main" }}
{{ $files := slice
(.Site.GetPage "/legal/responsible")
(.Site.GetPage "/legal/disclaimer")
(.Site.GetPage "/legal/privacy")
(.Site.GetPage "/legal/licences") }}
{{ $names := slice "responsible.md" "disclaimer.md" "privacy.md" "licences.md" }}
<div class="tui tui--files filebrowser">
<p class="tui__cmd"><span class="tui__sym">$</span> ls</p>
<p class="tui__cmd tui__total">total {{ len $files }}</p>
<ul class="tui__ls">
<li><a href="{{ "/" | relURL }}" data-name="..">
<span class="tui__perm">drwxr-xr-x</span><span class="tui__name">..</span>
</a></li>
{{ range $i, $p := $files }}{{ if $p }}
<li><a data-name="{{ index $names $i }}" data-text="{{ index $names $i }}"{{ if eq $i 0 }} class="is-selected"{{ end }}>
<span class="tui__perm">-rw-r--r--</span><span class="tui__name">{{ index $names $i }}</span>
</a></li>
{{ end }}{{ end }}
</ul>
<figure class="tui__preview">
{{ range $i, $p := $files }}{{ if $p }}
<div class="tui__text" data-text="{{ index $names $i }}"{{ if ne $i 0 }} hidden{{ end }}>{{ $p.Content }}</div>
{{ end }}{{ end }}
<figcaption class="tui__status">responsible.md</figcaption>
</figure>
</div>
{{ end }}
+7
View File
@@ -0,0 +1,7 @@
{{ define "main" }}
{{ if .Params.toc }}
<nav class="toc"><strong>TABLE OF CONTENTS</strong><div class="toc-content">{{ .TableOfContents }}</div></nav>
{{ end }}
{{ .Content }}
<div class="terminal-nav"><div class="back-nav"><a href="{{ "/" | relURL }}" class="back-link">../</a></div></div>
{{ end }}
+18
View File
@@ -0,0 +1,18 @@
{{ define "bodyclass" }}page-portfolio{{ end }}
{{ define "main" }}
{{ $up := "/" }}{{ if eq .Kind "term" }}{{ $up = "/portfolio/" }}{{ end }}
<div class="tui tui--files filebrowser">
<p class="tui__cmd"><span class="tui__sym">$</span> ls</p>
<p class="tui__cmd tui__total">total {{ len .Pages }}</p>
<ul class="tui__ls">
<li><a href="{{ $up | relURL }}" data-name="..">
<span class="tui__perm">drwxr-xr-x</span><span class="tui__name">..</span>
</a></li>
{{ range .Pages }}
<li><a href="{{ .RelPermalink }}" data-name="{{ .LinkTitle }}">
<span class="tui__perm">drwxr-xr-x</span><span class="tui__name">{{ .LinkTitle }}/</span>
</a></li>
{{ end }}
</ul>
</div>
{{ end }}
+8
View File
@@ -0,0 +1,8 @@
{{ define "bodyclass" }}home{{ end }}
{{ define "main" }}
{{ with .Params.heroImage }}
<figure class="hero__img">
<img src="{{ . | relURL }}" alt="" width="1200" height="1504" fetchpriority="high">
</figure>
{{ end }}
{{ end }}
+5
View File
@@ -0,0 +1,5 @@
<footer role="contentinfo">
<p>© {{ now.Year }} karimgabrielevarano.xyz</p>
<p><a href="{{ "/colophon/" | relURL }}" data-name="colophon">colophon</a> · <a href="{{ "/legal-notice/" | relURL }}" data-name="legal-notice">legal notice</a></p>
<a href="{{ "/cv/" | relURL }}" data-name="cv" hidden>cv</a>
</footer>
+60
View File
@@ -0,0 +1,60 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#111111">
<script>try{if(localStorage.getItem('kgv_theme')==='dark')document.documentElement.classList.add('dark');}catch(e){}</script>
<script>try{if(!sessionStorage.getItem('kgv_booted')){document.documentElement.classList.add('booting');setTimeout(function(){document.documentElement.classList.remove('booting');},3000);}}catch(e){}</script>
{{ $isHome := .IsHome }}
{{ $desc := .Site.Params.description }}{{ with .Description }}{{ $desc = . }}{{ end }}
<title>{{ if $isHome }}{{ .Site.Title }} — {{ .Site.Params.description }}{{ else }}{{ .Title }} — {{ .Site.Title }}{{ end }}</title>
<meta name="description" content="{{ $desc }}">
<meta name="author" content="{{ .Site.Params.author }}">
<link rel="canonical" href="{{ .Permalink }}">
{{/* social card image: project thumbnail, else home hero, else site default */}}
{{ $ogimg := .Site.Params.ogImage }}
{{ with .Params.heroImage }}{{ $ogimg = . }}{{ end }}
{{ with .Params.thumbnail }}{{ $ogimg = . }}{{ end }}
{{ $ogabs := "" }}{{ with $ogimg }}{{ $ogabs = . | absURL }}{{ end }}
<meta property="og:site_name" content="{{ .Site.Title }}">
<meta property="og:title" content="{{ if $isHome }}{{ .Site.Title }}{{ else }}{{ .Title }}{{ end }}">
<meta property="og:description" content="{{ $desc }}">
<meta property="og:type" content="{{ if eq .Section "portfolio" }}article{{ else }}website{{ end }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:locale" content="{{ .Site.Language.Lang | default "en" }}">
{{ with $ogabs }}<meta property="og:image" content="{{ . }}">{{ end }}
<meta name="twitter:card" content="{{ if $ogabs }}summary_large_image{{ else }}summary{{ end }}">
<meta name="twitter:title" content="{{ if $isHome }}{{ .Site.Title }}{{ else }}{{ .Title }}{{ end }}">
<meta name="twitter:description" content="{{ $desc }}">
{{ with $ogabs }}<meta name="twitter:image" content="{{ . }}">{{ end }}
<link rel="preload" as="font" type="font/woff2" href="{{ "fonts/iosevka-curly-slab-400.woff2" | relURL }}" crossorigin>
{{ $site := resources.Get "css/site.css" }}
{{ $custom := resources.Get "css/custom.css" }}
{{ $css := slice $site $custom | resources.Concat "css/bundle.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}">
<link rel="icon" href="{{ "favicon.svg" | relURL }}" type="image/svg+xml">
<link rel="icon" href="{{ "favicon-32.png" | relURL }}" sizes="32x32" type="image/png">
<link rel="apple-touch-icon" href="{{ "apple-touch-icon.png" | relURL }}">
<link rel="mask-icon" href="{{ "favicon.svg" | relURL }}" color="#111111">
{{ if $isHome }}
<script type="application/ld+json">
{{ ( dict
"@context" "https://schema.org"
"@type" "Person"
"name" .Site.Params.author
"url" .Site.BaseURL
"email" .Site.Params.email
"jobTitle" "Architect"
"address" ( dict "@type" "PostalAddress" "addressLocality" "Lucerne" "addressCountry" "CH" )
) | jsonify | safeJS }}
</script>
{{ end }}
</head>
+21
View File
@@ -0,0 +1,21 @@
<header role="banner">
<div class="topbar" data-home="{{ "/" | relURL }}" data-info="{{ .Site.Params.description }}" data-email="{{ .Site.Params.email }}">
<div class="prompt" aria-label="shell">
<a class="prompt__seg prompt__user" href="{{ "/" | relURL }}">{{ .Site.Params.shellUser }}@{{ .Site.Params.shellHost }}</a><span class="prompt__path">:~{{ if not .IsHome }}{{ $rel := strings.TrimSuffix "/" .RelPermalink }}{{ $parts := after 1 (split $rel "/") }}{{ $last := sub (len $parts) 1 }}{{ $acc := "" }}{{ range $i, $seg := $parts }}{{ $acc = printf "%s/%s" $acc $seg }}{{ $disp := $seg }}{{ if and $.Params.dotfile (eq $i $last) }}{{ $disp = printf ".%s" $seg }}{{ end }}/<a class="prompt__seg" href="{{ (printf "%s/" $acc) | relURL }}">{{ $disp }}</a>{{ end }}{{ end }}$</span><span class="prompt__input" contenteditable="true" role="textbox" aria-label="type a command, then press enter" spellcheck="false"></span><span class="caret">_</span>
</div>
<p class="prompt__out" aria-live="polite"></p>
<nav class="menu tui tui--files" aria-label="Main menu">
<p class="tui__cmd"><span class="tui__sym">$</span> ls</p>
<ul class="tui__ls">
{{ range .Site.Menus.main }}<li><a href="{{ .URL | relURL }}" data-name="{{ .Name | lower }}">
<span class="tui__perm">drwxr-xr-x</span><span class="tui__name">{{ .Name | lower }}/</span>
</a></li>{{ end }}
</ul>
</nav>
{{ if eq .Section "portfolio" }}
<nav class="routes" hidden aria-hidden="true">
{{ range (where .Site.RegularPages "Section" "portfolio") }}<a href="{{ .RelPermalink }}" data-name="{{ .Slug }}"></a>{{ end }}
</nav>
{{ end }}
</div>
</header>
+27
View File
@@ -0,0 +1,27 @@
{{ define "bodyclass" }}page-portfolio{{ end }}
{{ define "main" }}
{{ $sorted := sort .Pages "Date" "desc" }}
{{ $first := index $sorted 0 }}
<div class="tui filebrowser">
<p class="tui__cmd"><span class="tui__sym">$</span> ls</p>
<p class="tui__cmd tui__total">total {{ len $sorted }}</p>
<ul class="tui__ls">
<li><a href="{{ "/" | relURL }}" data-name="..">
<span class="tui__perm">drwxr-xr-x</span><span class="tui__date"></span><span class="tui__name">..</span>
</a></li>
{{ range $i, $p := $sorted }}
<li><a href="{{ $p.RelPermalink }}"
data-img="{{ with $p.Params.thumbnail }}{{ . | relURL }}{{ end }}"
data-name="{{ $p.Slug }}"
{{ if eq $i 0 }}class="is-selected"{{ end }}>
<span class="tui__perm">drwxr-xr-x</span><span class="tui__date">{{ $p.Date.Format "2006" }}</span><span class="tui__name">{{ $p.Slug }}/</span>
</a></li>
{{ end }}
</ul>
<figure class="tui__preview">
<img src="{{ $first.Params.thumbnail | relURL }}" alt="{{ $first.Slug }}">
<figcaption class="tui__status">{{ $first.Slug }}/&nbsp;&nbsp;{{ $first.Date.Format "2006" }}</figcaption>
</figure>
</div>
{{ end }}
+63
View File
@@ -0,0 +1,63 @@
{{ define "bodyclass" }}page-portfolio{{ end }}
{{ define "main" }}
{{ $hasText := ne (.RawContent | strings.TrimSpace) "" }}
{{ $images := .Params.images }}
{{ with .Params.vimeo }}
<div style="padding:56.25% 0 0;position:relative">
<iframe src="https://player.vimeo.com/video/{{ . }}?badge=0&autopause=0&player_id=0&app_id=58479"
loading="lazy" frameborder="0"
allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share"
referrerpolicy="strict-origin-when-cross-origin"
style="position:absolute;top:0;left:0;width:100%;height:100%"
title="{{ $.Title }}"></iframe>
</div>
{{ end }}
{{ if or $images $hasText }}
<div class="tui tui--files filebrowser">
<p class="tui__cmd"><span class="tui__sym">$</span> ls</p>
{{ $total := len $images }}{{ if $hasText }}{{ $total = add $total 1 }}{{ end }}
<p class="tui__cmd tui__total">total {{ $total }}</p>
<ul class="tui__ls">
<li><a href="{{ "/portfolio/" | relURL }}" data-name="..">
<span class="tui__perm">drwxr-xr-x</span><span class="tui__name">..</span>
</a></li>
{{ if $hasText }}
<li><a data-name="info.md" data-text="info.md" class="is-selected">
<span class="tui__perm">-rw-r--r--</span><span class="tui__name">info.md</span>
</a></li>
{{ end }}
{{ range $i, $im := $images }}
<li><a data-img="{{ $im.src | relURL }}" data-name="{{ $im.name }}"{{ if and (not $hasText) (eq $i 0) }} class="is-selected"{{ end }}>
<span class="tui__perm">-rw-r--r--</span><span class="tui__name">{{ $im.name }}</span>
</a></li>
{{ end }}
</ul>
<figure class="tui__preview">
{{ if $hasText }}<div class="tui__text" data-text="info.md">{{ .Content }}</div>{{ end }}
<img{{ if $hasText }} hidden{{ end }} src="{{ with $images }}{{ (index . 0).src | relURL }}{{ end }}" alt="{{ .Title }}">
<figcaption class="tui__status">{{ if $hasText }}info.md{{ else if $images }}{{ (index $images 0).name }}{{ end }}</figcaption>
</figure>
</div>
{{ end }}
{{ with .Params.studies }}
{{ $segs := after 2 (split . "/") }}
{{ $taxos := slice "schools" "degrees" "semesters" }}
{{ $last := sub (len $segs) 1 }}
<p class="proj-path">/studies/{{ range $i, $seg := $segs }}{{ if $i }}/{{ end }}{{ $terms := $.GetTerms (index $taxos $i) }}{{ if $terms }}{{ $t := index $terms 0 }}<a class="proj-path__seg{{ if eq $i $last }} proj-path__seg--last{{ end }}" href="{{ $t.RelPermalink }}">{{ $seg }}</a>{{ else }}{{ $seg }}{{ end }}{{ end }}</p>
{{ end }}
<div class="time"><time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2006" }}&nbsp;</time></div>
{{ $list := sort (where .Site.RegularPages "Section" "portfolio") "Date" "desc" }}
{{ $idx := -1 }}
{{ range $i, $p := $list }}{{ if eq $p.RelPermalink $.RelPermalink }}{{ $idx = $i }}{{ end }}{{ end }}
<nav class="proj-nav" aria-label="Project navigation">
{{ $older := add $idx 1 }}{{ $newer := sub $idx 1 }}
{{ if lt $older (len $list) }}<a href="{{ (index $list $older).RelPermalink }}" rel="prev" aria-label="older project"></a>{{ end }}
{{ if ge $newer 0 }}<a href="{{ (index $list $newer).RelPermalink }}" rel="next" aria-label="newer project"></a>{{ end }}
</nav>
{{ end }}
+5
View File
@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Disallow: /admin/
Sitemap: {{ "sitemap.xml" | absURL }}
+19
View File
@@ -0,0 +1,19 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# clean URLs: /portfolio/museo/ → its index.html, else 404
location / {
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
# long cache for fingerprinted/static assets
location ~* \.(css|js|woff2|webp|svg|png|jpe?g|ico)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
}
+109
View File
@@ -0,0 +1,109 @@
# Decap CMS — a lightweight, git-based admin for this Hugo site.
# It commits markdown straight to the Gitea repo; the public site stays static.
# Setup (Gitea OAuth app, deploy trigger): see /ADMIN.md.
backend:
name: gitea
repo: karim/kgva # TODO: owner/repo on git.kgva.ch
base_url: https://git.kgva.ch # your Gitea instance
branch: main # TODO: your default branch
app_id: REPLACE_WITH_GITEA_OAUTH_CLIENT_ID
# For editing locally without auth: run `npx decap-server` and uncomment:
# local_backend: true
media_folder: static/img
public_folder: /img
collections:
# ── Projects ────────────────────────────────────────────────────────────
- name: portfolio
label: Portfolio
label_singular: Project
folder: content/portfolio
create: true
slug: "{{slug}}"
summary: "{{title}}"
editor:
preview: false
fields:
- { label: Title, name: title, widget: string }
- { label: Slug, name: slug, widget: string, hint: "url part, e.g. textilsteinwerk" }
- { label: Date, name: date, widget: datetime }
- { label: Draft, name: draft, widget: boolean, default: true, required: false }
- { label: Thumbnail, name: thumbnail, widget: image, required: false }
- { label: Studies path, name: studies, widget: string, required: false, hint: "/studies/hslu/ba/semester_05" }
- { label: School, name: schools, widget: select, multiple: true, options: [hslu, ost], required: false }
- { label: Degree, name: degrees, widget: select, multiple: true, options: [ba, ma], required: false }
- { label: Semester, name: semesters, widget: list, required: false, hint: "e.g. semester_05" }
- { label: Vimeo ID, name: vimeo, widget: string, required: false }
- label: Images
name: images
widget: list
required: false
summary: "{{fields.name}}"
fields:
- { label: File, name: src, widget: image }
- { label: Name (caption), name: name, widget: string }
- { label: Description, name: body, widget: markdown, required: false }
# ── Single pages ────────────────────────────────────────────────────────
- name: pages
label: Pages
files:
- label: Home (hero image)
name: home
file: content/_index.md
fields:
- { label: Title, name: title, widget: string }
- { label: Description, name: description, widget: string }
- { label: Hero image, name: heroImage, widget: image }
- label: About
name: about
file: content/about.md
fields:
- { label: Title, name: title, widget: hidden, default: about }
- { label: Description, name: description, widget: string }
- { label: Layout, name: layout, widget: hidden, default: about }
- { label: Body, name: body, widget: markdown }
- label: Studio
name: studio
file: content/studio.md
fields:
- { label: Title, name: title, widget: hidden, default: studio }
- { label: Description, name: description, widget: string }
- { label: Body, name: body, widget: markdown }
- label: Openbureau
name: openbureau
file: content/openbureau.md
fields:
- { label: Title, name: title, widget: hidden, default: openbureau }
- { label: Description, name: description, widget: string }
- { label: Body, name: body, widget: markdown }
- label: CV
name: cv
file: content/cv.md
fields:
- { label: Title, name: title, widget: hidden, default: cv }
- { label: Description, name: description, widget: string }
- { label: Body, name: body, widget: markdown }
- label: Colophon
name: colophon
file: content/colophon.md
fields:
- { label: Title, name: title, widget: hidden, default: colophon }
- { label: Description, name: description, widget: string }
- { label: Dotfile, name: dotfile, widget: hidden, default: true }
- { label: Body, name: body, widget: markdown }
# ── Legal notice sections ────────────────────────────────────────────────
- name: legal
label: Legal notice
label_singular: Section
folder: content/legal
create: true
slug: "{{slug}}"
fields:
# keep these hidden ones so the section stays a non-rendered fragment
- { label: build, name: build, widget: hidden, default: { render: never, list: never } }
- { label: Body, name: body, widget: markdown }
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>kgva · admin</title>
</head>
<body>
<!-- Decap CMS. To self-host instead of the CDN, download the bundle to
static/admin/decap-cms.js and point the src there (see ADMIN.md). -->
<script src="https://unpkg.com/decap-cms@^3.3.3/dist/decap-cms.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="32" fill="#111111"/>
<text x="32" y="46" font-family="ui-monospace, 'Cascadia Code', 'SF Mono', Menlo, Consolas, monospace" font-size="40" font-weight="700" fill="#ffffff" text-anchor="middle">k</text>
</svg>

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 KiB

+1
View File
@@ -0,0 +1 @@
document.querySelectorAll(".auto-carousel").forEach(e=>{const t=e.querySelector("ul");if(!t)return;const n=[...t.children],o=[...e.querySelectorAll("ol li")],p=e.querySelector(".prev"),u=e.querySelector(".next");let r=!1,c=0,l=0;const f=()=>{if(n.length<2)return;const e=n[1].offsetLeft-n[0].offsetLeft;if(e<=0)return;const s=Math.min(Math.round(t.scrollLeft/e),n.length-1);o.forEach((e,t)=>e.classList.toggle("selected",t===s)),n.forEach((e,t)=>e.classList.toggle("selected",t===s))},i=e=>{const s=n[e];s&&(t.scrollLeft=s.offsetLeft)},d=()=>Math.max(0,o.findIndex(e=>e.classList.contains("selected"))),s=()=>t.classList.add("interacted");o[0]?.classList.add("selected"),n[0]?.classList.add("selected"),u?.addEventListener("click",()=>{s(),i((d()+1)%n.length)}),p?.addEventListener("click",()=>{s(),i((d()-1+n.length)%n.length)}),o.forEach((e,t)=>{e.querySelector("a")?.addEventListener("click",e=>{e.preventDefault(),s(),i(t)})});let a=!1;t.addEventListener("scroll",()=>{if(a)return;a=!0,requestAnimationFrame(()=>{a=!1,f()})},{passive:!0}),t.addEventListener("mousedown",e=>{r=!0,s(),t.classList.add("grabbing"),c=e.pageX-t.offsetLeft,l=t.scrollLeft});const h=()=>{r=!1,t.classList.remove("grabbing")};t.addEventListener("mouseleave",h),t.addEventListener("mouseup",h),t.addEventListener("mousemove",e=>{if(!r)return;e.preventDefault();const n=e.pageX-t.offsetLeft;t.scrollLeft=l-(n-c)*3}),t.addEventListener("touchstart",s,{passive:!0});const m=Number(e.getAttribute("duration"));m>0&&!matchMedia("(prefers-reduced-motion: reduce)").matches&&setInterval(()=>{if(t.classList.contains("interacted"))return;if(e.matches(":hover"))return;u?.click()},m)})
+109
View File
@@ -0,0 +1,109 @@
/* boot splash — a short systemd/Arch-Linux-style boot, shown once per session
(first visit + every new tab). pjax means it never replays on navigation.
click or press any key to skip. safe: head fallback timeout clears it. */
(function () {
var html = document.documentElement;
if (!html.classList.contains("booting")) return;
var boot = document.getElementById("boot");
var log = boot && boot.querySelector(".boot__log");
var ended = false;
function done() {
if (ended) return;
ended = true;
document.removeEventListener("keydown", skip, true);
document.removeEventListener("click", skip, true);
try { sessionStorage.setItem("kgv_booted", "1"); } catch (e) {}
if (!boot) { html.classList.remove("booting"); return; }
boot.style.transition = "opacity .3s ease";
boot.style.opacity = "0";
setTimeout(function () {
html.classList.remove("booting");
if (boot.parentNode) boot.parentNode.removeChild(boot);
}, 300);
}
function skip() { done(); }
if (!log) { done(); return; }
document.addEventListener("keydown", skip, true);
document.addEventListener("click", skip, true);
// [type, text, delay-after-ms]. type: 'k' plain kernel/info, 'ok' green [ OK ],
// 'st' "Starting…", 'b' blank, 'd' dim.
var L = [
["k", "karimgabrielevarano.xyz 1.0 (tty1)", 90],
["d", "kgv-sh · hugo static runtime · pjax router", 320],
["b", "", 60],
["k", "Welcome to karimgabrielevarano.xyz", 120],
["ok", "Loaded index.html", 70],
["ok", "Loaded css/bundle.css", 60],
["ok", "Loaded fonts/iosevka-curly-slab.woff2", 90],
["ok", "Loaded js/cursor.js", 55],
["ok", "Loaded js/shell.js", 55],
["ok", "Loaded js/filebrowser.js", 55],
["ok", "Loaded js/nav.js", 55],
["ok", "Loaded js/boot.js", 80],
["ok", "Parsed DOM — 0 errors", 120],
["st", "Mounting routes...", 240],
["ok", "Mounted /", 50],
["ok", "Mounted /portfolio", 50],
["ok", "Mounted /about", 50],
["ok", "Mounted /studio", 50],
["ok", "Mounted /openbureau", 50],
["ok", "Mounted /cv", 50],
["ok", "Mounted /colophon", 110],
["st", "Prerendering portfolio...", 220],
["ok", "rendered portfolio/textilsteinwerk", 60],
["ok", "rendered portfolio/the-shadow-over-emmen", 60],
["ok", "rendered portfolio/studytrip-trieste", 60],
["ok", "rendered portfolio/dojo", 60],
["ok", "rendered portfolio/museo", 110],
["ok", "Hydrated shell · cursor · theme", 100],
["ok", "Reached target Interactive.", 260],
["b", "", 110],
["k", "karimgabrielevarano login: karim (automatic)", 180],
["k", "welcome_", 500]
];
function render(item) {
var type = item[0], text = item[1];
var line = document.createElement("div");
line.className = "boot__line";
if (type === "ok") {
var s = document.createElement("span");
s.className = "boot__ok";
s.textContent = "[ OK ] ";
line.appendChild(s);
line.appendChild(document.createTextNode(text));
} else if (type === "st") {
line.className += " boot__dim";
line.appendChild(document.createTextNode("[ ] " + text));
} else if (type === "d") {
line.className += " boot__dim";
line.textContent = text;
} else if (type === "b") {
line.innerHTML = "&nbsp;";
} else {
line.textContent = text;
}
log.appendChild(line);
boot.scrollTop = boot.scrollHeight; // keep the newest line in view
}
var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) {
L.forEach(render);
setTimeout(done, 700);
return;
}
var i = 0;
(function next() {
if (ended) return;
if (i >= L.length) { setTimeout(done, 450); return; }
var item = L[i];
render(item);
i++;
setTimeout(next, item[2] || 110);
})();
})();
+53
View File
@@ -0,0 +1,53 @@
/* custom pixel cursor — appears only once we know the real pointer position,
so it never flashes at a stale spot on page changes. native cursor is hidden
via CSS (*{cursor:none}). */
(function () {
if (!window.matchMedia("(pointer: fine)").matches) return;
var c = document.createElement("canvas");
c.style.cssText =
"position:fixed;left:0;top:0;pointer-events:none;z-index:9999;" +
"image-rendering:pixelated;will-change:transform;opacity:0;" +
"transform:translate3d(-200px,-200px,0);";
document.body.appendChild(c);
var ctx = c.getContext("2d");
var px = 2;
var art = [
"_BB_______", "BRRB______", "BRRRB_____", "BRRRRB____", "BRRRRRB___",
"BRRRRRRB__", "BRRRRRRB__", "BRRRRSB___", "BRRSRRB___", "_BBBRRB___", "____BBB___"
];
var rows = art.length;
var cols = Math.max.apply(null, art.map(function (r) { return r.length; }));
c.width = cols * px; c.height = rows * px;
c.style.width = cols * px + "px"; c.style.height = rows * px + "px";
art.forEach(function (line, y) {
for (var x = 0; x < line.length; x++) {
var ch = line[x];
if (ch === "B") ctx.fillStyle = "#1a1a1a";
else if (ch === "R") ctx.fillStyle = "#ffffff";
else if (ch === "S") ctx.fillStyle = "#b0b0b0";
else continue;
ctx.fillRect(x * px, y * px, px, px);
}
});
var mx = -200, my = -200, raf = 0, shown = false;
function place() {
raf = 0;
c.style.transform = "translate3d(" + mx + "px," + my + "px,0)";
if (!shown) { shown = true; c.style.opacity = "1"; }
}
function move(e) {
mx = e.clientX; my = e.clientY;
if (!raf) raf = requestAnimationFrame(place);
}
function hide() { shown = false; c.style.opacity = "0"; }
document.addEventListener("mousemove", move, { passive: true });
document.addEventListener("mouseenter", function (e) { mx = e.clientX; my = e.clientY; place(); }, { passive: true });
// hide when the pointer leaves the window or the page is navigating away
document.addEventListener("mouseleave", hide, { passive: true });
window.addEventListener("blur", hide);
window.addEventListener("pagehide", hide);
})();

Some files were not shown because too many files have changed in this diff Show More