commit f7643602cbc2b72b3f79bd2c7be1679da80ad03d Author: karim Date: Mon Jun 29 01:37:03 2026 +0200 import kgva site diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..222f1a5 --- /dev/null +++ b/.claude/settings.local.json @@ -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>.*/\\\\1/p' about.html)", + "Bash(sed -n 's/.*
\\\\\\(.*\\\\\\)<\\\\/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" + ] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..426bf3c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +public/ +resources/ +.git/ +.github/ +node_modules/ +*.log diff --git a/.hugo_build.lock b/.hugo_build.lock new file mode 100644 index 0000000..e69de29 diff --git a/ADMIN.md b/ADMIN.md new file mode 100644 index 0000000..7c4ac92 --- /dev/null +++ b/ADMIN.md @@ -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:///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:///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: +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f771a12 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..b86e654 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,5 @@ +--- +title: "{{ replace .Name "-" " " }}" +date: {{ .Date }} +draft: true +--- diff --git a/archetypes/portfolio.md b/archetypes/portfolio.md new file mode 100644 index 0000000..5d23e44 --- /dev/null +++ b/archetypes/portfolio.md @@ -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. diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..594f97a --- /dev/null +++ b/assets/css/custom.css @@ -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"; } diff --git a/assets/css/site.css b/assets/css/site.css new file mode 100644 index 0000000..be108f9 --- /dev/null +++ b/assets/css/site.css @@ -0,0 +1,3 @@ +@media(pointer:fine){*{cursor:none!important}} + +@font-face{font-family:iosevka curly slab;font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/iosevka-curly-slab-400.woff2)format('woff2')}@font-face{font-family:iosevka curly slab;font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/iosevka-curly-slab-700.woff2)format('woff2')}:root{--spacing-base:1.5em;--font-family-mono:'Iosevka Curly Slab', ui-monospace, monospace;--font-family-sans:'Iosevka Curly Slab', ui-monospace, monospace;--font-size-base:1em;--font-size-small:0.9em;--font-size-code:0.95em;--spacing-xs:calc(var(--spacing-base) * 0.25);--spacing-sm:calc(var(--spacing-base) * 0.5);--spacing-md:var(--spacing-base);--spacing-lg:calc(var(--spacing-base) * 2);--bg-h:0;--bg-s:0%;--bg-l:99%;--color-bg-primary:hsl(var(--bg-h) var(--bg-s) var(--bg-l));--color-bg-secondary:hsl(var(--bg-h) var(--bg-s) calc(var(--bg-l) - 2%));--color-border:hsl(var(--bg-h) var(--bg-s) calc(var(--bg-l) - 6%));--color-selection-bg:hsl(var(--bg-h) var(--bg-s) calc(var(--bg-l) - 13%));--color-text-primary:hsl(var(--bg-h) var(--bg-s) 8%);--color-text-muted:hsl(var(--bg-h) var(--bg-s) 50%);--color-text-code:hsl(var(--bg-h) var(--bg-s) 30%);--container-width:34rem;--pre-border-radius:0.75rem;--inline-border-radius:0.375rem;--gap-base:1em;--gap-small:0.5rem;--breakpoint-mobile:600px;color-scheme:light}.no-scrollbar{scrollbar-width:none;-ms-overflow-style:none}.no-scrollbar::-webkit-scrollbar{display:none}.list-unstyled{list-style:none;margin-left:0}.text-muted{color:var(--color-text-muted)}.no-underline{text-decoration:none;border-bottom:none}::selection{background:var(--color-selection-bg);color:var(--color-text-primary)}.skip-link{position:absolute;top:-100%;left:0;padding:var(--spacing-sm)var(--spacing-md);background:var(--color-bg-primary);border:1px solid var(--color-border);z-index:1000;opacity:0;transition:opacity .2s ease-in-out}.skip-link:focus{position:fixed;top:var(--spacing-sm);left:var(--spacing-sm);opacity:1}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--color-text-primary);outline-offset:2px}*{margin:0;padding:0;font:inherit;color:var(--color-text-primary);box-sizing:border-box}ul{list-style-type:disc;margin-left:1.5rem}ol{list-style-type:decimal;margin-left:1.5rem}html{font-size:90%;margin:0 0 0 calc(100vw - 100%);-webkit-text-size-adjust:100%;height:100%}body{font:14px/var(--spacing-base)var(--font-family-sans);background:var(--color-bg-primary);min-height:100%;display:grid;grid-template-rows:auto 1fr auto;grid-template-columns:minmax(0,var(--container-width));justify-content:center;padding:var(--spacing-lg);gap:var(--spacing-lg)}h1,h2,h3,h4,h5,.time,time,footer{font-family:var(--font-family-mono)}a{display:inline-block;margin-left:-.16666em;text-decoration:none;border-bottom:1px dotted;white-space:pre-wrap;word-wrap:break-word}a:hover{border-bottom:1px solid}p,pre{margin:var(--spacing-base)0}h1,h2,h3,h4,h5{margin:var(--spacing-base)0;font-weight:700}h2{counter-increment:h2;counter-reset:h3}h3{counter-increment:h3;counter-reset:h4}h4{counter-increment:h4;counter-reset:h5}h5{counter-increment:h5}h2::before{content:counter(h2)". "}h3::before{content:counter(h2)"." counter(h3)". "}h4::before{content:counter(h2)"." counter(h3)"." counter(h4)". "}h5::before{content:counter(h2)"." counter(h3)"." counter(h4)"." counter(h5)". "}time{color:var(--color-text-muted)}footer{padding:var(--spacing-sm)0;text-align:center}footer p{margin:.25em 0}pre{border:1px solid var(--color-border);padding:var(--spacing-sm)var(--spacing-md);border-radius:var(--pre-border-radius);white-space:pre-wrap;word-wrap:break-word;font-family:var(--font-family-mono);font-size:var(--font-size-code);line-height:1.6;color:var(--color-text-code);overflow-x:auto;box-shadow:0 1px 2px 0 var(--color-border)}pre code{background:0 0;border:none;padding:0;font-size:inherit;color:inherit}p>code,li>code,h1>code,h2>code,h3>code,h4>code,h5>code{background:var(--color-bg-secondary);border:1px solid var(--color-border);border-radius:var(--inline-border-radius);padding:.2em .4em;font-family:var(--font-family-mono);font-size:var(--font-size-code);color:var(--color-text-code);white-space:pre-wrap;word-wrap:break-word}.terms-list{padding-bottom:var(--spacing-base)}.terms-list ul{list-style-type:none;margin-left:0}.terms-list ul li{display:inline-block;font-style:italic;font-size:var(--font-size-small);color:var(--color-text-muted);padding:0 3px}.path-nav{font-family:var(--font-family-mono);padding:var(--spacing-base)0;white-space:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;-ms-overflow-style:none;width:100%}.path-nav::-webkit-scrollbar{display:none}.path-nav ol{display:flex;flex-wrap:nowrap;list-style:none;margin:0;padding:0}.path-nav li{display:flex;align-items:center;color:var(--color-text-muted);flex-shrink:0;max-width:200px}.path-nav li.current{max-width:none}.path-nav li a{text-decoration:none;border-bottom:none;padding:0 .25em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--color-text-muted)}.path-nav li.current a{color:var(--color-text-primary);font-weight:700;overflow:visible;text-overflow:clip;white-space:normal}.path-nav a:hover{text-decoration:underline}.back-nav{margin-bottom:var(--spacing-base)}.back-link{color:var(--color-text-muted);border-bottom:none}.back-link:hover{color:var(--color-text-primary);text-decoration:none}.time-list ul{list-style:none;margin-left:0;display:grid;grid-template-columns:1fr 1fr;gap:2rem;padding:0}.time-list li{display:flex;flex-direction:column;gap:0;padding:0}@media(max-width:600px){.time-list ul{grid-template-columns:1fr}.time-list time{font-size:var(--font-size-small)}}.archive{margin:var(--spacing-base)0}.archive-year{margin:calc(var(--spacing-base) * 1.5)0 var(--spacing-sm);font-size:1.2em;color:var(--color-text-primary)}.archive-month{margin:var(--spacing-md)0 var(--spacing-sm);font-size:1.1em;color:var(--color-text-muted)}.archive-item{display:flex;align-items:baseline;gap:var(--gap-small);margin:var(--spacing-sm)0;padding-left:1em}.archive-item time{font-family:var(--font-family-mono);font-size:var(--font-size-small);color:var(--color-text-muted);min-width:2em}.archive-item a{font-size:var(--font-size-base)}.terminal-nav{font-family:var(--font-family-mono);border-top:1px solid var(--color-border);margin-top:calc(var(--spacing-base) * 2);padding-top:var(--spacing-base)}.terminal-nav nav ul{list-style:none;padding:0;margin:0;display:flex;gap:1em;justify-content:center}.terminal-nav nav ul li{display:inline-block}.terminal-nav nav ul li a,.terminal-nav .back-link{color:var(--color-text-muted);border-bottom:none;text-decoration:none}.terminal-nav nav ul li a:hover,.terminal-nav .back-link:hover{color:var(--color-text-primary)}.terminal-nav nav ul li a.active{color:var(--color-text-primary);font-weight:700}.terminal-nav .back-nav{margin:calc(var(--spacing-base) * .5)0}img{display:block;max-width:100%;height:auto;margin:var(--spacing-base)auto}figure{margin:var(--spacing-base)0;text-align:center}figure img{margin:0 auto}figcaption{color:var(--color-text-muted);font-size:var(--font-size-small);margin-top:calc(var(--spacing-base)/2)}table{width:100%;margin:var(--spacing-base)0;border-collapse:collapse}thead th,th{font-weight:700;text-align:center;border-bottom:2px solid var(--color-border);padding:var(--spacing-sm)}td{padding:var(--spacing-sm);border-bottom:1px solid var(--color-border)}strong{font-weight:700}em{font-style:italic}blockquote{margin:var(--spacing-base)0;padding:var(--spacing-sm)var(--spacing-base);border:1px solid var(--color-border);border-radius:var(--pre-border-radius);background:var(--color-bg-secondary);box-shadow:0 1px 2px 0 var(--color-border);font-style:italic}blockquote>:first-child{margin-top:0}blockquote>:last-child{margin-bottom:0}.toc{margin:var(--spacing-base)0;padding:var(--spacing-sm);border:1px solid var(--color-border);background:var(--color-bg-secondary);border-radius:var(--pre-border-radius);box-shadow:0 1px 2px 0 var(--color-border)}.toc .toc-content a{text-decoration:none;color:var(--color-text-primary);font-size:var(--font-size-small)}.toc .toc-content ol li{list-style-type:disc!important}.time{margin:var(--spacing-md)0}.reading-time,.lastmod{color:var(--color-text-muted);font-size:var(--font-size-small)}@media(max-width:600px){.path-nav li{max-width:100px}.path-nav li:first-child{max-width:none}.path-nav li.current{max-width:none}}@media(max-width:600px){body{padding-top:2em}pre{max-width:calc(100vw - 4em)}}@media print{body{background:#fff;color:#000;padding:0;display:block}header,footer,.terminal-nav,.skip-link,.toc,.back-nav{display:none!important}main{max-width:100%}a{border-bottom:none;color:#000}a[href]::after{content:" (" attr(href)")";font-size:.8em;color:gray}a[href^="#"]::after,a[href^="javascript:"]::after{content:""}pre,blockquote,img{page-break-inside:avoid}h1,h2,h3,h4,h5{page-break-after:avoid}}.list-image{aspect-ratio:9/5;overflow:hidden}.list-image img{width:100%;height:100%;object-fit:cover;display:block;margin:0}.list-title-row{display:flex;justify-content:space-between;align-items:baseline;margin-top:.4rem}.list-title{margin:0}.list-meta{display:flex;gap:8px}img{filter:grayscale(100%);transition:filter .4s ease}img:hover{filter:grayscale(0%)}.auto-carousel img{filter:none!important}body{padding:var(--spacing-sm)1.5rem;gap:var(--spacing-sm)}img{margin:0}.project-nav{display:flex;gap:1em;margin:var(--spacing-base)0;padding-top:var(--spacing-base);border-top:1px solid var(--color-border);font-family:var(--font-family-mono)}.project-nav a{color:var(--color-text-muted);border-bottom:none}.project-nav a:hover{color:var(--color-text-primary)}.project-nav .next{margin-left:auto;text-align:right}.auto-carousel{position:relative;margin:1rem 0!important}.auto-carousel ul{overflow:auto;display:flex;flex-wrap:nowrap;scroll-snap-type:x mandatory;scroll-snap-points-y:repeat(100%);scroll-behavior:smooth;background:0 0;-ms-overflow-style:none;scrollbar-width:none;margin:0!important;padding:0;cursor:grab;user-select:none}.auto-carousel ul.grabbing{cursor:grabbing;scroll-snap-type:none}.auto-carousel ul::-webkit-scrollbar{display:none}.auto-carousel ul li{position:relative;min-width:100%;list-style:none;scroll-snap-align:start;pointer-events:none}.auto-carousel ul li>img{border-radius:0}.auto-carousel ul li>div{display:flex;justify-content:center;align-items:center;color:#fff;font-weight:700;font-size:20px}.auto-carousel ol{display:none}.auto-carousel .prev,.auto-carousel .next{position:absolute;top:50%;left:.5rem;transform:translateY(-50%);z-index:9;display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;padding:0;background:0 0;border:none;color:#fff;font:inherit;font-size:1.75rem;line-height:1;user-select:none;cursor:pointer;mix-blend-mode:difference;opacity:.7;transition:opacity .2s ease}.auto-carousel .prev:hover,.auto-carousel .next:hover{opacity:1}.auto-carousel .next{left:auto;right:.5rem} \ No newline at end of file diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..2fca313 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,5 @@ +--- +title: "startpage" +description: "architecture studio" +heroImage: "/img/Triest_Rozzol_Melara.webp" +--- diff --git a/content/about.md b/content/about.md new file mode 100644 index 0000000..85660cb --- /dev/null +++ b/content/about.md @@ -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). + +
+
location
lucerne, switzerland
+
degree
ba architecture
+
status
ma in progress
+
available
architecture collaborations · commissions · open-source
+
email
hi@karimgabrielevarano.xyz
+
studio
studio
+
open
openbureau
+
diff --git a/content/colophon.md b/content/colophon.md new file mode 100644 index 0000000..e988ad9 --- /dev/null +++ b/content/colophon.md @@ -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. diff --git a/content/cv.md b/content/cv.md new file mode 100644 index 0000000..b58f0e4 --- /dev/null +++ b/content/cv.md @@ -0,0 +1,30 @@ +--- +title: "cv" +description: "education, experience, skills" +--- + +**education** + +
+
····
ma architecture · hslu · in progress
+
····
ba architecture · ost & hslu
+
····
design school
+
+ +**experience** + +
+
····
electrical engineering · site & planning
+
+ +**skills** + +
+
design
architecture · graphic · editorial · web
+
open source
curation, self-hosting, ICT setup & support
+
software
rhino · hugo · ····
+
+ +--- + +[hi@karimgabrielevarano.xyz](mailto:hi@karimgabrielevarano.xyz) diff --git a/content/legal-notice.md b/content/legal-notice.md new file mode 100644 index 0000000..cbfbdd1 --- /dev/null +++ b/content/legal-notice.md @@ -0,0 +1,6 @@ +--- +title: "legal notice" +description: "legal notice, privacy policy and licences" +layout: "legal" +dotfile: true +--- diff --git a/content/legal/_index.md b/content/legal/_index.md new file mode 100644 index 0000000..25f1d24 --- /dev/null +++ b/content/legal/_index.md @@ -0,0 +1,5 @@ +--- +build: + render: never + list: never +--- diff --git a/content/legal/disclaimer.md b/content/legal/disclaimer.md new file mode 100644 index 0000000..4c45272 --- /dev/null +++ b/content/legal/disclaimer.md @@ -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. diff --git a/content/legal/licences.md b/content/legal/licences.md new file mode 100644 index 0000000..b42082b --- /dev/null +++ b/content/legal/licences.md @@ -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). diff --git a/content/legal/privacy.md b/content/legal/privacy.md new file mode 100644 index 0000000..52f9dcc --- /dev/null +++ b/content/legal/privacy.md @@ -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. diff --git a/content/legal/responsible.md b/content/legal/responsible.md new file mode 100644 index 0000000..bf88c26 --- /dev/null +++ b/content/legal/responsible.md @@ -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) diff --git a/content/openbureau.md b/content/openbureau.md new file mode 100644 index 0000000..d37a900 --- /dev/null +++ b/content/openbureau.md @@ -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) diff --git a/content/portfolio/_index.md b/content/portfolio/_index.md new file mode 100644 index 0000000..5dc1ccf --- /dev/null +++ b/content/portfolio/_index.md @@ -0,0 +1,4 @@ +--- +title: "portfolio" +description: "selected projects" +--- diff --git a/content/portfolio/dojo.md b/content/portfolio/dojo.md new file mode 100644 index 0000000..0073cde --- /dev/null +++ b/content/portfolio/dojo.md @@ -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. diff --git a/content/portfolio/museo.md b/content/portfolio/museo.md new file mode 100644 index 0000000..179ca76 --- /dev/null +++ b/content/portfolio/museo.md @@ -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. diff --git a/content/portfolio/studytrip-trieste.md b/content/portfolio/studytrip-trieste.md new file mode 100644 index 0000000..05b18fa --- /dev/null +++ b/content/portfolio/studytrip-trieste.md @@ -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" } +--- diff --git a/content/portfolio/textilsteinwerk.md b/content/portfolio/textilsteinwerk.md new file mode 100644 index 0000000..a1e513e --- /dev/null +++ b/content/portfolio/textilsteinwerk.md @@ -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. diff --git a/content/portfolio/the-shadow-over-emmen.md b/content/portfolio/the-shadow-over-emmen.md new file mode 100644 index 0000000..68de744 --- /dev/null +++ b/content/portfolio/the-shadow-over-emmen.md @@ -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" +--- diff --git a/content/studio.md b/content/studio.md new file mode 100644 index 0000000..9e51ede --- /dev/null +++ b/content/studio.md @@ -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) diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..2b4d13c --- /dev/null +++ b/deploy/Caddyfile @@ -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 +} diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..73d2c2b --- /dev/null +++ b/deploy/README.md @@ -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 && pct destroy `), or keep it a day as rollback. + +This never deletes the live site before the replacement is proven. + +## Updating later + +``` +pct exec -- 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). diff --git a/deploy/provision-lxc.sh b/deploy/provision-lxc.sh new file mode 100755 index 0000000..1ba156a --- /dev/null +++ b/deploy/provision-lxc.sh @@ -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." diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..de797b7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + web: + build: . + image: kgva + container_name: kgva + ports: + - "8080:80" + restart: unless-stopped diff --git a/hugo.toml b/hugo.toml new file mode 100644 index 0000000..4ab09f8 --- /dev/null +++ b/hugo.toml @@ -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/" diff --git a/layouts/404.html b/layouts/404.html new file mode 100644 index 0000000..e6a8d85 --- /dev/null +++ b/layouts/404.html @@ -0,0 +1,16 @@ +{{ define "bodyclass" }}page-404{{ end }} +{{ define "main" }} +
+

$ cd .

+

cd: no such file or directory: .

+

error 404 — nothing lives here.

+

cd ~

+
+ +{{ end }} diff --git a/layouts/_default/about.html b/layouts/_default/about.html new file mode 100644 index 0000000..a83a584 --- /dev/null +++ b/layouts/_default/about.html @@ -0,0 +1,27 @@ +{{ define "bodyclass" }}page-about{{ end }} +{{ define "main" }} +{{ $cv := .Site.GetPage "/cv" }} +
+

$ ls

+

total {{ if $cv }}2{{ else }}1{{ end }}

+ + +
+
{{ .Content }}
+ {{ if $cv }}{{ end }} +
info.md
+
+
+{{ end }} diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 0000000..db95f0b --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,20 @@ + + +{{ partial "head.html" . }} + + + + {{ partial "header.html" . }} +
+ {{ block "main" . }}{{ end }} +
+ {{ partial "footer.html" . }} + + + + + + + + + diff --git a/layouts/_default/legal.html b/layouts/_default/legal.html new file mode 100644 index 0000000..5c956c2 --- /dev/null +++ b/layouts/_default/legal.html @@ -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" }} +
+

$ ls

+

total {{ len $files }}

+ + +
+ {{ range $i, $p := $files }}{{ if $p }} +
{{ $p.Content }}
+ {{ end }}{{ end }} +
responsible.md
+
+
+{{ end }} diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100644 index 0000000..9f2d1bb --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,7 @@ +{{ define "main" }} +{{ if .Params.toc }} + +{{ end }} +{{ .Content }} + +{{ end }} diff --git a/layouts/_default/taxonomy.html b/layouts/_default/taxonomy.html new file mode 100644 index 0000000..5fa9341 --- /dev/null +++ b/layouts/_default/taxonomy.html @@ -0,0 +1,18 @@ +{{ define "bodyclass" }}page-portfolio{{ end }} +{{ define "main" }} +{{ $up := "/" }}{{ if eq .Kind "term" }}{{ $up = "/portfolio/" }}{{ end }} +
+

$ ls

+

total {{ len .Pages }}

+ +
+{{ end }} diff --git a/layouts/index.html b/layouts/index.html new file mode 100644 index 0000000..88d770d --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,8 @@ +{{ define "bodyclass" }}home{{ end }} +{{ define "main" }} +{{ with .Params.heroImage }} +
+ +
+{{ end }} +{{ end }} diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html new file mode 100644 index 0000000..12581c1 --- /dev/null +++ b/layouts/partials/footer.html @@ -0,0 +1,5 @@ + diff --git a/layouts/partials/head.html b/layouts/partials/head.html new file mode 100644 index 0000000..8845097 --- /dev/null +++ b/layouts/partials/head.html @@ -0,0 +1,60 @@ + + + + + + + + + {{ $isHome := .IsHome }} + {{ $desc := .Site.Params.description }}{{ with .Description }}{{ $desc = . }}{{ end }} + {{ if $isHome }}{{ .Site.Title }} — {{ .Site.Params.description }}{{ else }}{{ .Title }} — {{ .Site.Title }}{{ end }} + + + + + {{/* 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 }} + + + + + + + + {{ with $ogabs }}{{ end }} + + + + + {{ with $ogabs }}{{ end }} + + + + {{ $site := resources.Get "css/site.css" }} + {{ $custom := resources.Get "css/custom.css" }} + {{ $css := slice $site $custom | resources.Concat "css/bundle.css" | minify | fingerprint }} + + + + + + + + {{ if $isHome }} + + {{ end }} + diff --git a/layouts/partials/header.html b/layouts/partials/header.html new file mode 100644 index 0000000..51efabb --- /dev/null +++ b/layouts/partials/header.html @@ -0,0 +1,21 @@ +
+
+
+ {{ .Site.Params.shellUser }}@{{ .Site.Params.shellHost }}:~{{ 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 }}/{{ $disp }}{{ end }}{{ end }}$_ +
+

+ + {{ if eq .Section "portfolio" }} + + {{ end }} +
+
diff --git a/layouts/portfolio/list.html b/layouts/portfolio/list.html new file mode 100644 index 0000000..abb37e7 --- /dev/null +++ b/layouts/portfolio/list.html @@ -0,0 +1,27 @@ +{{ define "bodyclass" }}page-portfolio{{ end }} +{{ define "main" }} +{{ $sorted := sort .Pages "Date" "desc" }} +{{ $first := index $sorted 0 }} +
+

$ ls

+

total {{ len $sorted }}

+ + +
+ {{ $first.Slug }} +
{{ $first.Slug }}/  {{ $first.Date.Format "2006" }}
+
+
+{{ end }} diff --git a/layouts/portfolio/single.html b/layouts/portfolio/single.html new file mode 100644 index 0000000..8dc7705 --- /dev/null +++ b/layouts/portfolio/single.html @@ -0,0 +1,63 @@ +{{ define "bodyclass" }}page-portfolio{{ end }} +{{ define "main" }} +{{ $hasText := ne (.RawContent | strings.TrimSpace) "" }} +{{ $images := .Params.images }} + +{{ with .Params.vimeo }} +
+ +
+{{ end }} + +{{ if or $images $hasText }} +
+

$ ls

+ {{ $total := len $images }}{{ if $hasText }}{{ $total = add $total 1 }}{{ end }} +

total {{ $total }}

+ + +
+ {{ if $hasText }}
{{ .Content }}
{{ end }} + +
{{ if $hasText }}info.md{{ else if $images }}{{ (index $images 0).name }}{{ end }}
+
+
+{{ end }} + +{{ with .Params.studies }} + {{ $segs := after 2 (split . "/") }} + {{ $taxos := slice "schools" "degrees" "semesters" }} + {{ $last := sub (len $segs) 1 }} +

/studies/{{ range $i, $seg := $segs }}{{ if $i }}/{{ end }}{{ $terms := $.GetTerms (index $taxos $i) }}{{ if $terms }}{{ $t := index $terms 0 }}{{ $seg }}{{ else }}{{ $seg }}{{ end }}{{ end }}

+{{ end }} + +
+ +{{ $list := sort (where .Site.RegularPages "Section" "portfolio") "Date" "desc" }} +{{ $idx := -1 }} +{{ range $i, $p := $list }}{{ if eq $p.RelPermalink $.RelPermalink }}{{ $idx = $i }}{{ end }}{{ end }} + +{{ end }} diff --git a/layouts/robots.txt b/layouts/robots.txt new file mode 100644 index 0000000..bb939fb --- /dev/null +++ b/layouts/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / +Disallow: /admin/ + +Sitemap: {{ "sitemap.xml" | absURL }} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d9fa393 --- /dev/null +++ b/nginx.conf @@ -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"; + } +} diff --git a/static/admin/config.yml b/static/admin/config.yml new file mode 100644 index 0000000..661a832 --- /dev/null +++ b/static/admin/config.yml @@ -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 } diff --git a/static/admin/index.html b/static/admin/index.html new file mode 100644 index 0000000..43cd0a8 --- /dev/null +++ b/static/admin/index.html @@ -0,0 +1,14 @@ + + + + + + + kgva · admin + + + + + + diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png new file mode 100644 index 0000000..6c4785a Binary files /dev/null and b/static/apple-touch-icon.png differ diff --git a/static/favicon-32.png b/static/favicon-32.png new file mode 100644 index 0000000..0dceb35 Binary files /dev/null and b/static/favicon-32.png differ diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..ab964d4 --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,4 @@ + + + k + diff --git a/static/fonts/iosevka-curly-slab-400.woff2 b/static/fonts/iosevka-curly-slab-400.woff2 new file mode 100644 index 0000000..33946f1 Binary files /dev/null and b/static/fonts/iosevka-curly-slab-400.woff2 differ diff --git a/static/fonts/iosevka-curly-slab-700.woff2 b/static/fonts/iosevka-curly-slab-700.woff2 new file mode 100644 index 0000000..ac01a56 Binary files /dev/null and b/static/fonts/iosevka-curly-slab-700.woff2 differ diff --git a/static/icon-512.png b/static/icon-512.png new file mode 100644 index 0000000..077f199 Binary files /dev/null and b/static/icon-512.png differ diff --git a/static/img/Triest_Rozzol_Melara.webp b/static/img/Triest_Rozzol_Melara.webp new file mode 100644 index 0000000..849941d Binary files /dev/null and b/static/img/Triest_Rozzol_Melara.webp differ diff --git a/static/img/sem01/01.webp b/static/img/sem01/01.webp new file mode 100644 index 0000000..d80c93f Binary files /dev/null and b/static/img/sem01/01.webp differ diff --git a/static/img/sem01/02.webp b/static/img/sem01/02.webp new file mode 100644 index 0000000..27f2a91 Binary files /dev/null and b/static/img/sem01/02.webp differ diff --git a/static/img/sem01/03.webp b/static/img/sem01/03.webp new file mode 100644 index 0000000..47c0300 Binary files /dev/null and b/static/img/sem01/03.webp differ diff --git a/static/img/sem01/04.webp b/static/img/sem01/04.webp new file mode 100644 index 0000000..a2a9870 Binary files /dev/null and b/static/img/sem01/04.webp differ diff --git a/static/img/sem01/05.webp b/static/img/sem01/05.webp new file mode 100644 index 0000000..5a2deb0 Binary files /dev/null and b/static/img/sem01/05.webp differ diff --git a/static/img/sem01/06.webp b/static/img/sem01/06.webp new file mode 100644 index 0000000..6270848 Binary files /dev/null and b/static/img/sem01/06.webp differ diff --git a/static/img/sem01/07.webp b/static/img/sem01/07.webp new file mode 100644 index 0000000..b206231 Binary files /dev/null and b/static/img/sem01/07.webp differ diff --git a/static/img/sem01/08.webp b/static/img/sem01/08.webp new file mode 100644 index 0000000..d07204f Binary files /dev/null and b/static/img/sem01/08.webp differ diff --git a/static/img/sem01/thumbnail.webp b/static/img/sem01/thumbnail.webp new file mode 100644 index 0000000..e990ed9 Binary files /dev/null and b/static/img/sem01/thumbnail.webp differ diff --git a/static/img/sem02/01.webp b/static/img/sem02/01.webp new file mode 100644 index 0000000..bbff783 Binary files /dev/null and b/static/img/sem02/01.webp differ diff --git a/static/img/sem02/02.webp b/static/img/sem02/02.webp new file mode 100644 index 0000000..571ede3 Binary files /dev/null and b/static/img/sem02/02.webp differ diff --git a/static/img/sem02/03.webp b/static/img/sem02/03.webp new file mode 100644 index 0000000..d3fd20d Binary files /dev/null and b/static/img/sem02/03.webp differ diff --git a/static/img/sem02/04.webp b/static/img/sem02/04.webp new file mode 100644 index 0000000..0a037b9 Binary files /dev/null and b/static/img/sem02/04.webp differ diff --git a/static/img/sem02/05.webp b/static/img/sem02/05.webp new file mode 100644 index 0000000..5a88a78 Binary files /dev/null and b/static/img/sem02/05.webp differ diff --git a/static/img/sem02/06.webp b/static/img/sem02/06.webp new file mode 100644 index 0000000..dd871a7 Binary files /dev/null and b/static/img/sem02/06.webp differ diff --git a/static/img/sem02/thumbnail.webp b/static/img/sem02/thumbnail.webp new file mode 100644 index 0000000..bbff783 Binary files /dev/null and b/static/img/sem02/thumbnail.webp differ diff --git a/static/img/sem05/01.webp b/static/img/sem05/01.webp new file mode 100644 index 0000000..5acfa22 Binary files /dev/null and b/static/img/sem05/01.webp differ diff --git a/static/img/sem05/02.webp b/static/img/sem05/02.webp new file mode 100644 index 0000000..41d81cc Binary files /dev/null and b/static/img/sem05/02.webp differ diff --git a/static/img/sem05/03.webp b/static/img/sem05/03.webp new file mode 100644 index 0000000..805031b Binary files /dev/null and b/static/img/sem05/03.webp differ diff --git a/static/img/sem05/04.webp b/static/img/sem05/04.webp new file mode 100644 index 0000000..391e9c0 Binary files /dev/null and b/static/img/sem05/04.webp differ diff --git a/static/img/sem05/05.webp b/static/img/sem05/05.webp new file mode 100644 index 0000000..72885b2 Binary files /dev/null and b/static/img/sem05/05.webp differ diff --git a/static/img/sem05/section01.webp b/static/img/sem05/section01.webp new file mode 100644 index 0000000..bd8af2e Binary files /dev/null and b/static/img/sem05/section01.webp differ diff --git a/static/img/sem05/section02.webp b/static/img/sem05/section02.webp new file mode 100644 index 0000000..77ca3a8 Binary files /dev/null and b/static/img/sem05/section02.webp differ diff --git a/static/img/shadowofemmen/thumbnail.webp b/static/img/shadowofemmen/thumbnail.webp new file mode 100644 index 0000000..33211de Binary files /dev/null and b/static/img/shadowofemmen/thumbnail.webp differ diff --git a/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-01.webp b/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-01.webp new file mode 100644 index 0000000..5d0a73b Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-01.webp differ diff --git a/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-02.webp b/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-02.webp new file mode 100644 index 0000000..c5aa4cd Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-02.webp differ diff --git a/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-03.webp b/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-03.webp new file mode 100644 index 0000000..6790b1a Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-03.webp differ diff --git a/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-04.webp b/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-04.webp new file mode 100644 index 0000000..ab7e518 Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Canale_Grande_SantAntonio_Tramaturgo-04.webp differ diff --git a/static/img/studytrip-trieste/Triest_Casa_Bartoli-02.webp b/static/img/studytrip-trieste/Triest_Casa_Bartoli-02.webp new file mode 100644 index 0000000..bd9a1a5 Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Casa_Bartoli-02.webp differ diff --git a/static/img/studytrip-trieste/Triest_Cpo_Androna_Campo_Marzio-01.webp b/static/img/studytrip-trieste/Triest_Cpo_Androna_Campo_Marzio-01.webp new file mode 100644 index 0000000..e7f5800 Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Cpo_Androna_Campo_Marzio-01.webp differ diff --git a/static/img/studytrip-trieste/Triest_Kathedrale_San_Giusto-02.webp b/static/img/studytrip-trieste/Triest_Kathedrale_San_Giusto-02.webp new file mode 100644 index 0000000..41e13d3 Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Kathedrale_San_Giusto-02.webp differ diff --git a/static/img/studytrip-trieste/Triest_Liceo_Artistico-02.webp b/static/img/studytrip-trieste/Triest_Liceo_Artistico-02.webp new file mode 100644 index 0000000..398f014 Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Liceo_Artistico-02.webp differ diff --git a/static/img/studytrip-trieste/Triest_Liceo_Artistico-03.webp b/static/img/studytrip-trieste/Triest_Liceo_Artistico-03.webp new file mode 100644 index 0000000..c7da7a9 Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Liceo_Artistico-03.webp differ diff --git a/static/img/studytrip-trieste/Triest_Liceo_Artistico-05.webp b/static/img/studytrip-trieste/Triest_Liceo_Artistico-05.webp new file mode 100644 index 0000000..3be8b49 Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Liceo_Artistico-05.webp differ diff --git a/static/img/studytrip-trieste/Triest_Liceo_Artistico-07.webp b/static/img/studytrip-trieste/Triest_Liceo_Artistico-07.webp new file mode 100644 index 0000000..bfff1aa Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Liceo_Artistico-07.webp differ diff --git a/static/img/studytrip-trieste/Triest_Palmanova-01.webp b/static/img/studytrip-trieste/Triest_Palmanova-01.webp new file mode 100644 index 0000000..e2ec8db Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Palmanova-01.webp differ diff --git a/static/img/studytrip-trieste/Triest_Rozzol_Melara-01.webp b/static/img/studytrip-trieste/Triest_Rozzol_Melara-01.webp new file mode 100644 index 0000000..398f4d7 Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Rozzol_Melara-01.webp differ diff --git a/static/img/studytrip-trieste/Triest_Rozzol_Melara-02.webp b/static/img/studytrip-trieste/Triest_Rozzol_Melara-02.webp new file mode 100644 index 0000000..c1da3af Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Rozzol_Melara-02.webp differ diff --git a/static/img/studytrip-trieste/Triest_Rozzol_Melara-03.webp b/static/img/studytrip-trieste/Triest_Rozzol_Melara-03.webp new file mode 100644 index 0000000..5c8a6d8 Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Rozzol_Melara-03.webp differ diff --git a/static/img/studytrip-trieste/Triest_Rozzol_Melara-04.webp b/static/img/studytrip-trieste/Triest_Rozzol_Melara-04.webp new file mode 100644 index 0000000..b1c299e Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Rozzol_Melara-04.webp differ diff --git a/static/img/studytrip-trieste/Triest_Rozzol_Melara-05.webp b/static/img/studytrip-trieste/Triest_Rozzol_Melara-05.webp new file mode 100644 index 0000000..e3ba4ad Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Rozzol_Melara-05.webp differ diff --git a/static/img/studytrip-trieste/Triest_Stanjel-01.webp b/static/img/studytrip-trieste/Triest_Stanjel-01.webp new file mode 100644 index 0000000..000d3ef Binary files /dev/null and b/static/img/studytrip-trieste/Triest_Stanjel-01.webp differ diff --git a/static/js/auto-carousel.js b/static/js/auto-carousel.js new file mode 100644 index 0000000..3c0be53 --- /dev/null +++ b/static/js/auto-carousel.js @@ -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)}) \ No newline at end of file diff --git a/static/js/boot.js b/static/js/boot.js new file mode 100644 index 0000000..687df5b --- /dev/null +++ b/static/js/boot.js @@ -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 = " "; + } 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); + })(); +})(); diff --git a/static/js/cursor.js b/static/js/cursor.js new file mode 100644 index 0000000..aa5c5d1 --- /dev/null +++ b/static/js/cursor.js @@ -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); +})(); diff --git a/static/js/filebrowser.js b/static/js/filebrowser.js new file mode 100644 index 0000000..c8602a0 --- /dev/null +++ b/static/js/filebrowser.js @@ -0,0 +1,94 @@ +/* tui file browser — preview pane (image OR text panes), hover + freeze + + keyboard. re-binds itself after pjax navigation (document listeners are added + once; rows/preview are re-queried per page). */ +(function () { + "use strict"; + var rows = [], img = null, texts = [], cap = null, cur = 0, frozen = false, container = null; + + function hideTexts() { texts.forEach(function (t) { t.hidden = true; }); } + function showText(k) { texts.forEach(function (t) { t.hidden = (t.getAttribute("data-text") !== k); }); } + function setFrozen(v) { frozen = v; if (container) container.classList.toggle("is-frozen", v); } + function showImg(src) { + if (!img) return; + img.hidden = false; + if (img.getAttribute("src") === src) { img.style.opacity = "1"; return; } + img.style.opacity = "0"; + img.onload = img.onerror = function () { img.style.opacity = "1"; }; + img.src = src; + if (img.complete) img.style.opacity = "1"; // already cached → no fade + } + + function select(i) { + if (!rows.length) return; + if (i < 0) i = 0; + if (i >= rows.length) i = rows.length - 1; + cur = i; + for (var r = 0; r < rows.length; r++) rows[r].classList.remove("is-selected"); + var a = rows[i]; + a.classList.add("is-selected"); + if (a.scrollIntoView) a.scrollIntoView({ block: "nearest" }); + + var src = a.getAttribute("data-img"); + var tkey = a.getAttribute("data-text"); + if (src) { showImg(src); hideTexts(); } + else if (tkey) { if (img) img.hidden = true; showText(tkey); } + else { if (img) img.hidden = true; hideTexts(); } + + if (cap) { + var n = a.querySelector(".tui__name"); + var y = a.querySelector(".tui__date"); + cap.textContent = (n ? n.textContent.trim() : a.getAttribute("data-name")) + (y ? " " + y.textContent.trim() : ""); + } + } + + function bind() { + var list = document.querySelector(".filebrowser .tui__ls"); + container = list ? list.closest(".filebrowser") : null; + rows = list ? Array.prototype.slice.call(list.querySelectorAll("a[data-name]")) : []; + img = document.querySelector(".tui__preview img"); + texts = Array.prototype.slice.call(document.querySelectorAll(".tui__preview .tui__text")); + cap = document.querySelector(".tui__status"); + cur = 0; frozen = false; + if (!rows.length) return; + + rows.forEach(function (a, i) { + a.addEventListener("mouseenter", function () { if (!frozen) select(i); }); + a.addEventListener("click", function (e) { + if (a.getAttribute("href")) return; // navigation rows behave normally + e.preventDefault(); + if (frozen && cur === i) setFrozen(false); + else { select(i); setFrozen(true); } + }); + }); + + var start = rows.findIndex(function (a) { return a.classList.contains("is-selected"); }); + select(start < 0 ? 0 : start); + + // warm the cache so switching between gallery images is instant + var srcs = rows.map(function (a) { return a.getAttribute("data-img"); }).filter(Boolean); + if (srcs.length) { + var preload = function () { srcs.forEach(function (s) { var im = new Image(); im.src = s; }); }; + if (window.requestIdleCallback) requestIdleCallback(preload); else setTimeout(preload, 300); + } + } + + // one-time document listeners (reference the current bound state) + document.addEventListener("click", function (e) { + if (e.target.closest && e.target.closest(".tui__ls a")) return; + if (frozen) setFrozen(false); + }); + document.addEventListener("keydown", function (e) { + var t = e.target; + if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return; + if (!rows.length) return; + if (e.key === "ArrowDown") { e.preventDefault(); select(cur + 1); } + else if (e.key === "ArrowUp") { e.preventDefault(); select(cur - 1); } + else if (e.key === "Enter") { + var href = rows[cur] && rows[cur].getAttribute("href"); + if (href) { if (window.pjax) window.pjax(href); else window.location.href = href; } + } + }); + + bind(); + document.addEventListener("pjax:load", bind); +})(); diff --git a/static/js/nav.js b/static/js/nav.js new file mode 100644 index 0000000..0639879 --- /dev/null +++ b/static/js/nav.js @@ -0,0 +1,85 @@ +/* pjax — client-side navigation. swaps the page content instead of a full + reload, so the cursor, shell and theme stay alive (no flashing, no re-fetch + of css/js). pure progressive enhancement: if fetch/history/DOMParser are + missing, or anything goes wrong, links fall back to normal navigation. */ +(function () { + "use strict"; + if (!window.history || !window.fetch || !window.DOMParser) return; + var MAIN = "#main-content"; + if (!document.querySelector(MAIN)) return; + + function internal(a) { + return a && a.href && a.origin === location.origin && + a.getAttribute("href").charAt(0) !== "#" && + !a.hasAttribute("download") && + (a.getAttribute("target") || "") !== "_blank"; + } + + function swap(html, url) { + var doc = new DOMParser().parseFromString(html, "text/html"); + var nm = doc.querySelector(MAIN), om = document.querySelector(MAIN); + if (!nm || !om) { location.href = url; return; } + + om.innerHTML = nm.innerHTML; + if (doc.title) document.title = doc.title; + document.body.className = doc.body.className; + + // breadcrumb (in the persistent header) — swap its inner links + var np = doc.querySelector(".prompt__path"), op = document.querySelector(".prompt__path"); + if (np && op) op.innerHTML = np.innerHTML; + + // per-section typeable routes live in .topbar — replace them + var topbar = document.querySelector(".topbar"); + var oldR = document.querySelector(".routes"); + if (oldR && oldR.parentNode) oldR.parentNode.removeChild(oldR); + var newR = doc.querySelector(".routes"); + if (newR && topbar) topbar.appendChild(document.importNode(newR, true)); + + window.scrollTo(0, 0); + document.dispatchEvent(new CustomEvent("pjax:load")); + } + + // prefetch cache: hovering a link fetches its HTML so the click is instant + var cache = {}; + function fetchHTML(url) { + return fetch(url, { credentials: "same-origin" }) + .then(function (r) { if (!r.ok) throw new Error(r.status); return r.text(); }); + } + function prefetch(url) { + if (!cache[url]) cache[url] = fetchHTML(url).catch(function () { delete cache[url]; return null; }); + } + + var busy = false; + function go(url, push) { + if (busy) return; + busy = true; + (cache[url] || fetchHTML(url)) + .then(function (html) { + busy = false; + if (!html) { location.href = url; return; } // prefetch had failed + if (push) history.pushState({ pjax: true }, "", url); + swap(html, url); + }) + .catch(function () { busy = false; location.href = url; }); + } + + document.addEventListener("mouseover", function (e) { + var a = e.target.closest ? e.target.closest("a[href]") : null; + if (internal(a) && a.href.split("#")[0] !== location.href.split("#")[0]) prefetch(a.href); + }, { passive: true }); + + document.addEventListener("click", function (e) { + if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; + var a = e.target.closest ? e.target.closest("a[href]") : null; + if (!internal(a)) return; + if (a.href.split("#")[0] === location.href.split("#")[0]) return; // same document + e.preventDefault(); + go(a.href, true); + }); + + window.addEventListener("popstate", function () { go(location.href, false); }); + history.replaceState({ pjax: true }, "", location.href); + + // bridge for programmatic navigation (shell commands, filebrowser Enter) + window.pjax = function (url) { go(url, true); }; +})(); diff --git a/static/js/shell.js b/static/js/shell.js new file mode 100644 index 0000000..193bb45 --- /dev/null +++ b/static/js/shell.js @@ -0,0 +1,322 @@ +/* header prompt → a tiny playful shell. + type a command + Enter: known targets navigate, anything else is an + easter egg (a fake shell error). the menu below is always "open" and + clickable, so typing is optional. */ +(function () { + "use strict"; + var bar = document.querySelector(".topbar"); + var input = document.querySelector(".prompt__input"); + var out = document.querySelector(".prompt__out"); + var menu = document.querySelector(".menu"); + if (!bar || !input) return; + + var home = bar.getAttribute("data-home") || "/"; + + // command → url: the always-open menu, plus any context routes (e.g. the + // portfolio project slugs, only present in the DOM while inside /portfolio/). + // rebuilt after each pjax navigation since .routes changes per section. + var routes = {}; + function buildRoutes() { + routes = {}; + Array.prototype.forEach.call(document.querySelectorAll(".menu a, .routes a, footer a[data-name]"), function (a) { + var key = (a.getAttribute("data-name") || a.textContent).trim().toLowerCase().replace(/\/+$/, ""); + if (key) routes[key] = a.getAttribute("href"); + }); + } + buildRoutes(); + document.addEventListener("pjax:load", buildRoutes); + + function pwd() { + var p = document.querySelector(".prompt__path"); + return p ? p.textContent.replace(/^:/, "").replace(/\$$/, "") : "~"; + } + + function say(msg) { if (out) out.textContent = msg; } + function navigate(u) { input.textContent = ""; if (window.pjax) window.pjax(u); else window.location.href = u; } + + function pad(n) { return (n < 10 ? "0" : "") + n; } + function dateStr() { + var d = new Date(); + var days = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]; + var mons = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]; + return days[d.getDay()] + " " + mons[d.getMonth()] + " " + pad(d.getDate()) + " " + d.getFullYear(); + } + function timeStr() { + var d = new Date(); + return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds()); + } + function infoStr() { + var lines = []; + var info = bar.getAttribute("data-info"); + var email = bar.getAttribute("data-email"); + if (info) lines.push(info); + if (email) lines.push(email); + lines.push("type: portfolio · about · studio · openbureau"); + return lines.join("\n"); + } + + // command history, kept for the whole tab session (survives navigation) + var hist = []; + try { hist = JSON.parse(sessionStorage.getItem("kgv_hist") || "[]"); } catch (e) { hist = []; } + var hpos = hist.length; + function remember(cmd) { + if (hist[hist.length - 1] !== cmd) hist.push(cmd); + hpos = hist.length; + try { sessionStorage.setItem("kgv_hist", JSON.stringify(hist.slice(-100))); } catch (e) {} + } + function historyStr() { + if (!hist.length) return "(empty)"; + return hist.map(function (c, i) { return (" " + (i + 1)).slice(-4) + " " + c; }).join("\n"); + } + + function unameStr(lc) { + return lc === "uname -a" ? "kgv-sh 1.0 architecture lucerne ch" : "kgv-sh"; + } + + // ls -a reveals the hidden dotfiles + function lsa() { + var items = ["./", "../"]; + Array.prototype.forEach.call(document.querySelectorAll(".menu a"), function (a) { + items.push((a.getAttribute("data-name") || "") + "/"); + }); + items.push(".colophon", ".legal-notice"); + return items.join(" "); + } + + function tree() { + var work = []; + Array.prototype.forEach.call(document.querySelectorAll(".routes a"), function (a) { + work.push(a.getAttribute("data-name")); + }); + var l = ["."]; + l.push("├── portfolio/"); + work.forEach(function (s, i) { l.push("│ " + (i === work.length - 1 ? "└── " : "├── ") + s + "/"); }); + l.push("├── about/"); + l.push("├── studio/"); + l.push("├── openbureau/"); + l.push("├── .colophon"); + l.push("└── .legal-notice"); + return l.join("\n"); + } + + function fetchStr() { + var up = Math.max(1, Math.round((window.performance && performance.now ? performance.now() : 0) / 1000)); + return [ + " _ karimgabrielevarano", + " | | __ -------------------", + " | |/ / os: kgv-sh 1.0", + " | < location: lucerne", + " |_|\\_\\ degree: ba architecture", + " status: ma in progress", + " uptime: " + up + "s", + " ~: portfolio · about · studio · openbureau" + ].join("\n"); + } + + function setTheme(mode) { + var dark = mode === "dark"; + document.documentElement.classList.toggle("dark", dark); + try { localStorage.setItem("kgv_theme", dark ? "dark" : "light"); } catch (e) {} + } + + // `sl` — a steam locomotive rolls across the screen (the classic `ls` typo gag) + var TRAIN = [ + " ==== ________ ___________ ", + " _D _| |_______/ \\__I_I_____===__|_________| ", + " |(_)--- | H\\________/ | | =|___ ___| ", + " / | | H | | | | ||_| |_|| ", + " | | | H |__--------------------| [___] | ", + " | ________|___H__/__|_____/[][]~\\_______| | ", + " |/ | |-----------I_____I [][] [] D |=======|__ ", + "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ", + " |/-=|___|= || || || |_____/~\\___/ ", + " \\_/ \\O=====O=====O=====O_/ \\_/ " + ].join("\n"); + function sl() { + if (document.querySelector(".sl")) return; + var pre = document.createElement("pre"); + pre.className = "sl"; + pre.textContent = TRAIN; + document.body.appendChild(pre); + pre.style.transform = "translateX(" + window.innerWidth + "px)"; + requestAnimationFrame(function () { + pre.style.transition = "transform 4s linear"; + pre.style.transform = "translateX(-" + (pre.offsetWidth + 40) + "px)"; + }); + setTimeout(function () { pre.remove(); }, 4200); + } + + // secret: `doom` boots shareware DOOM (id Software) in a draggable window + function doom() { + if (document.querySelector(".doom-window")) return; + var win = document.createElement("div"); + win.className = "doom-window"; + var bar = document.createElement("div"); + bar.className = "doom-window__bar"; + var label = document.createElement("span"); + label.className = "doom-window__title"; + label.textContent = "doom"; + var close = document.createElement("button"); + close.className = "doom-window__close"; + close.setAttribute("aria-label", "close"); + close.textContent = "✕"; + var frame = document.createElement("iframe"); + frame.src = "https://archive.org/embed/DoomsharewareEpisode"; + frame.setAttribute("allowfullscreen", ""); + frame.allow = "fullscreen; autoplay; gamepad"; + bar.appendChild(label); + bar.appendChild(close); + win.appendChild(bar); + win.appendChild(frame); + document.body.appendChild(win); + + function kill() { win.remove(); document.removeEventListener("keydown", esc); } + function esc(e) { if (e.key === "Escape") kill(); } + close.addEventListener("click", kill); + document.addEventListener("keydown", esc); + + // drag by the title bar (shield the iframe so it doesn't swallow the mouse) + bar.addEventListener("mousedown", function (e) { + if (e.target === close) return; + var r = win.getBoundingClientRect(); + win.style.left = r.left + "px"; + win.style.top = r.top + "px"; + win.style.transform = "none"; + var dx = e.clientX - r.left, dy = e.clientY - r.top; + frame.style.pointerEvents = "none"; + function mv(ev) { win.style.left = (ev.clientX - dx) + "px"; win.style.top = (ev.clientY - dy) + "px"; } + function up() { frame.style.pointerEvents = ""; document.removeEventListener("mousemove", mv); document.removeEventListener("mouseup", up); } + document.addEventListener("mousemove", mv); + document.addEventListener("mouseup", up); + e.preventDefault(); + }); + } + + function egg(lc, raw) { + var arg0 = raw.split(/\s+/)[0]; + if (/^sudo\s+make\s+me\s+a\s+sandwich$/.test(lc)) return "okay."; + if (/^make\s+me\s+a\s+sandwich$/.test(lc)) return "what? make it yourself."; + if (/^sudo\b/.test(lc) || lc === "su") return "permission denied: nice try."; + if (/^rm\b/.test(lc)) return "rm: some things you keep."; + if (lc === "whoami") return "karim"; + if (lc === "pwd") return pwd(); + if (lc === "help" || /^man\b/.test(lc)) return "nav: portfolio · about · studio · openbureau · cv\nbasics: info · contact · date · time · tree · fetch · history\ntheme: dark · light"; + if (lc === "hello" || lc === "hi" || lc === "hey") return "hi :)"; + if (lc === "exit" || lc === "logout") return "there is no exit. only architecture."; + if (lc === "coffee" || lc === "brew") return "☕ brewing… (418: i'm a teapot)"; + if (/^(cat|open|vim|nano|emacs|less)\b/.test(lc)) return arg0 + ": permission denied"; + return "zsh: command not found: " + arg0; + } + + // tab-completion over known commands + navigable routes + var COMMANDS = ["help", "man", "info", "contact", "date", "time", "tree", "fetch", "neofetch", + "uname", "history", "clear", "echo", "pwd", "whoami", "ls", "cd", "cat", "open", + "dark", "light", "theme", "sl", "doom", "coffee"]; + function complete() { + var val = input.textContent; + var parts = val.split(" "); + var last = (parts[parts.length - 1] || "").toLowerCase(); + if (!last) return; + var pool = parts.length > 1 ? Object.keys(routes) : COMMANDS.concat(Object.keys(routes)); + var matches = []; + for (var i = 0; i < pool.length; i++) { + if (pool[i].indexOf(last) === 0 && matches.indexOf(pool[i]) < 0) matches.push(pool[i]); + } + if (!matches.length) return; + var common = matches.reduce(function (a, b) { + var n = 0; while (n < a.length && a.charAt(n) === b.charAt(n)) n++; return a.slice(0, n); + }); + parts[parts.length - 1] = matches.length === 1 ? matches[0] : common; + input.textContent = parts.join(" "); + caretToEnd(); + say(matches.length > 1 ? matches.join(" ") : ""); // list options when ambiguous + } + + function run(raw) { + var cmd = raw.replace(/\s+/g, " ").trim(); + if (!cmd) return; + var lc = cmd.toLowerCase(); + remember(cmd); + + if (lc === "clear") { say(""); input.textContent = ""; return; } + if (lc === "doom") { doom(); say(""); input.textContent = ""; return; } + if (lc === "sl") { sl(); say(""); input.textContent = ""; return; } + if (lc === "dark" || lc === "light") { setTheme(lc); say(""); input.textContent = ""; return; } + if (lc === "theme") { var next = document.documentElement.classList.contains("dark") ? "light" : "dark"; setTheme(next); say("theme: " + next); input.textContent = ""; return; } + + // strip a leading verb, leading ./ and trailing slash + var t = lc.replace(/^(cd|ls|cat|open)\b\s*/, "").replace(/^\.\//, "").replace(/\/+$/, "").trim(); + + // `ls` of the current/home dir — the listing is already on screen + if (/^ls\b/.test(lc) && (t === "" || t === "~" || t === ".")) { say(""); input.textContent = ""; return; } + // home / up + if (t === "" || t === "~" || t === ".." || t === "." || t === "home" || t === "/") { navigate(home); return; } + // a known menu target + if (routes[t]) { navigate(routes[t]); return; } + + // basics that just print + if (lc === "info") { say(infoStr()); input.textContent = ""; return; } + if (lc === "contact") { say(bar.getAttribute("data-email") || ""); input.textContent = ""; return; } + if (lc === "date") { say(dateStr()); input.textContent = ""; return; } + if (lc === "time") { say(timeStr()); input.textContent = ""; return; } + if (lc === "history") { say(historyStr()); input.textContent = ""; return; } + if (lc === "tree") { say(tree()); input.textContent = ""; return; } + if (lc === "fetch" || lc === "neofetch") { say(fetchStr()); input.textContent = ""; return; } + if (lc === "uname" || lc === "uname -a") { say(unameStr(lc)); input.textContent = ""; return; } + if (/^ls\s+-(la|al|a)$/.test(lc) || lc === "ls --all") { say(lsa()); input.textContent = ""; return; } + if (lc === "echo" || lc.indexOf("echo ") === 0) { say(cmd.slice(4).trim()); input.textContent = ""; return; } + + say(egg(lc, cmd)); // unknown → shell error, fresh empty prompt + input.textContent = ""; + } + + input.addEventListener("keydown", function (e) { + if (e.key === "Enter") { e.preventDefault(); run(input.textContent); return; } + if (e.key === "Tab") { e.preventDefault(); complete(); return; } + if (e.key === "ArrowUp" && hist.length) { + e.preventDefault(); + hpos = Math.max(0, hpos - 1); + input.textContent = hist[hpos] || ""; + caretToEnd(); say(""); + } else if (e.key === "ArrowDown" && hist.length) { + e.preventDefault(); + hpos = Math.min(hist.length, hpos + 1); + input.textContent = hist[hpos] || ""; + caretToEnd(); say(""); + } + }); + input.addEventListener("input", function () { say(""); }); + + function caretToEnd() { + var r = document.createRange(); + r.selectNodeContents(input); + r.collapse(false); + var s = window.getSelection(); + s.removeAllRanges(); + s.addRange(r); + } + + // start typing anywhere → the keystroke goes straight into the command line + document.addEventListener("keydown", function (e) { + if (document.querySelector(".doom-window")) return; // playing doom + var t = e.target; + if (t === input) return; // already typing + if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return; + if (e.metaKey || e.ctrlKey || e.altKey) return; + if (e.key && e.key.length === 1) { // a printable character + e.preventDefault(); + input.focus(); + input.textContent += e.key; + caretToEnd(); + say(""); + } + }); + + // clicking anywhere on the bar focuses the prompt (but let links + text selection through) + bar.addEventListener("click", function (e) { + if (e.target.closest("a")) return; + if (window.getSelection && String(window.getSelection())) return; + input.focus(); + caretToEnd(); + }); +})();