Use Mono.png as profile picture and favicon; sync proof statuses

- Dossier proof statuses now come from projects.ts
- Mono.png replaces the placeholder silhouette and the SVG favicon
- Header, hero and bust tweaks

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-03 21:20:37 +02:00
co-authored by claude
parent fd72e1082a
commit e268f2a359
10 changed files with 178 additions and 86 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ npm run lint
- `profile.ts`: name, title, stance, dossier fields, links, map lock.
- `dossier.ts`: the five About files.
- `projects.ts`: projects, statuses, node-map positions, featured nodes.
- `public/profile/pfp.svg`: placeholder profile picture. Replace this file (or point `profile.pfp` at a new one).
- `public/profile/Mono.png`: profile picture (the favicon is a copy at `src/app/icon.png`). Replace the file, or point `profile.pfp` at a new one.
- `public/terrain.svg`: hero terrain contours (Dajt elevation data).
- `src/app/globals.css`: colour tokens, bracket/grid utilities, and all motion.
-11
View File
@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 200">
<!-- PLACEHOLDER profile picture: replace this file with the real one. -->
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#3a4247"/>
<stop offset="1" stop-color="#101315"/>
</linearGradient>
</defs>
<path fill="url(#g)" d="M84,28 C108,28 122,46 122,72 C122,96 112,114 100,122 L102,136 C130,142 156,156 164,182 L166,200 L2,200 L4,182 C12,156 38,142 66,136 L68,122 C56,114 46,96 46,72 C46,46 60,28 84,28Z"/>
<path fill="none" stroke="#f2f4f5" stroke-opacity=".3" d="M84,28 C108,28 122,46 122,72 C122,96 112,114 100,122 L102,136 C130,142 156,156 164,182 L166,200 L2,200 L4,182 C12,156 38,142 66,136 L68,122 C56,114 46,96 46,72 C46,46 60,28 84,28Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 788 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" fill="#050607"/>
<path d="M3,8.5 V3 H8.5 M15.5,3 H21 V8.5 M21,15.5 V21 H15.5 M8.5,21 H3 V15.5" stroke="#f2f4f5" stroke-width="1.8"/>
<rect x="9.25" y="9.25" width="5.5" height="5.5" fill="#4dff95"/>
</svg>

Before

Width:  |  Height:  |  Size: 314 B

+36 -10
View File
@@ -8,17 +8,31 @@ import { useRef } from "react";
* "wireframe", a scan sweeps across and resolves it to solid, and on desktop
* the layers tilt apart slightly with the pointer. No 3D library needed.
*/
export function Bust({ src, delay = "0s", className = "" }: { src: string; delay?: string; className?: string }) {
export function Bust({
src,
delay = "0s",
className = "",
}: {
src: string;
delay?: string;
className?: string;
}) {
const tiltRef = useRef<HTMLDivElement>(null);
const meshRef = useRef<HTMLDivElement>(null);
function onMove(e: React.PointerEvent<HTMLDivElement>) {
if (e.pointerType !== "mouse" || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
if (
e.pointerType !== "mouse" ||
matchMedia("(prefers-reduced-motion: reduce)").matches
)
return;
const r = e.currentTarget.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width - 0.5;
const y = (e.clientY - r.top) / r.height - 0.5;
if (tiltRef.current) tiltRef.current.style.transform = `rotateX(${(-y * 8).toFixed(2)}deg) rotateY(${(x * 10).toFixed(2)}deg)`;
if (meshRef.current) meshRef.current.style.transform = `translate(${(x * -6).toFixed(1)}px, ${(y * -6).toFixed(1)}px)`;
if (tiltRef.current)
tiltRef.current.style.transform = `rotateX(${(-y * 8).toFixed(2)}deg) rotateY(${(x * 10).toFixed(2)}deg)`;
if (meshRef.current)
meshRef.current.style.transform = `translate(${(x * -6).toFixed(1)}px, ${(y * -6).toFixed(1)}px)`;
}
function onLeave() {
@@ -28,11 +42,14 @@ export function Bust({ src, delay = "0s", className = "" }: { src: string; delay
return (
<div
className={`relative aspect-[168/200] [perspective:600px] ${className}`}
className={`relative aspect-square [perspective:600px] ${className}`}
onPointerMove={onMove}
onPointerLeave={onLeave}
>
<div ref={tiltRef} className="absolute inset-0 transition-transform duration-200 ease-out [transform-style:preserve-3d]">
<div
ref={tiltRef}
className="absolute inset-0 transition-transform duration-200 ease-out [transform-style:preserve-3d]"
>
{/* wireframe pass */}
<div className="absolute inset-0 [mask-image:repeating-linear-gradient(0deg,#000_0_1px,transparent_1px_5px)]">
<Image
@@ -44,15 +61,24 @@ export function Bust({ src, delay = "0s", className = "" }: { src: string; delay
/>
</div>
{/* solid pass */}
<div className="bust-solid seq absolute inset-0" style={{ "--d": delay } as React.CSSProperties}>
<Image src={src} alt="Profile picture" fill sizes="168px" className="object-cover" />
<div
className="bust-solid seq absolute inset-0"
style={{ "--d": delay } as React.CSSProperties}
>
<Image
src={src}
alt="Profile picture"
fill
sizes="168px"
className="object-cover"
/>
</div>
{/* residual mesh that floats a few px above the photo */}
<div
{/*<div
ref={meshRef}
aria-hidden="true"
className="absolute inset-0 opacity-[0.12] transition-transform duration-200 ease-out [background-image:repeating-linear-gradient(0deg,#f2f4f5_0_1px,transparent_1px_8px)] [mask-image:linear-gradient(90deg,#000,transparent_70%)]"
/>
/>*/}
{/* scan line */}
<div
aria-hidden="true"
+96 -27
View File
@@ -1,5 +1,10 @@
import Link from "next/link";
import { dossierFields, lock, profile, type DossierField } from "@/content/profile";
import {
dossierFields,
lock,
profile,
type DossierField,
} from "@/content/profile";
import { Bust } from "./Bust";
import { Scramble } from "./Scramble";
import { LocalTime, SessionStamp } from "./Telemetry";
@@ -12,22 +17,42 @@ const d = (s: number) => ({ "--d": `${s}s` }) as React.CSSProperties;
const DOSSIER_START = 1.35;
const DOSSIER_STAGGER = 0.14;
function DossierList({ fields, className = "" }: { fields: DossierField[]; className?: string }) {
function DossierList({
fields,
className = "",
}: {
fields: DossierField[];
className?: string;
}) {
return (
<dl className={`grid grid-cols-[124px_minmax(0,1fr)] gap-y-2.5 border-t border-white/8 pt-[18px] text-xs leading-[1.45] xl:grid-cols-[116px_minmax(0,1fr)] ${className}`}>
<dl
className={`grid grid-cols-[124px_minmax(0,1fr)] gap-y-2.5 border-t border-white/8 pt-[18px] text-xs leading-[1.45] xl:grid-cols-[116px_minmax(0,1fr)] ${className}`}
>
{fields.map((f, i) => {
const delay = DOSSIER_START + i * DOSSIER_STAGGER;
return (
<div key={f.label} className="contents">
<dt className="pt-[3px] text-[9px] uppercase tracking-[0.22em] text-mute">{f.label}</dt>
<dt className="pt-[3px] text-[9px] uppercase tracking-[0.22em] text-mute">
{f.label}
</dt>
<dd className="flex flex-wrap gap-x-3.5">
{f.links
? f.links.map((l, j) => (
<a key={l.href} href={l.href} className="border-b border-white/25 hover:border-accent">
<Scramble text={l.label} delay={delay + j * 0.2} step={30} />
{f.links ? (
f.links.map((l, j) => (
<a
key={l.href}
href={l.href}
className="border-b border-white/25 hover:border-accent"
>
<Scramble
text={l.label}
delay={delay + j * 0.2}
step={30}
/>
</a>
))
: <Scramble text={f.value} delay={delay} step={30} />}
) : (
<Scramble text={f.value} delay={delay} step={30} />
)}
</dd>
</div>
);
@@ -38,7 +63,10 @@ function DossierList({ fields, className = "" }: { fields: DossierField[]; class
export function Hero() {
return (
<section aria-labelledby="hero-title" className="relative overflow-hidden border-b border-line xl:flex xl:min-h-dvh xl:items-center">
<section
aria-labelledby="hero-title"
className="relative overflow-hidden border-b border-line xl:flex xl:min-h-dvh xl:items-center"
>
<TerrainLayer />
<div className="relative z-10 mx-auto w-full max-w-[1440px] pt-[68px] md:pt-[84px] xl:h-[864px]">
@@ -57,7 +85,12 @@ export function Hero() {
<Scramble text="LOCK ACQUIRED" delay={0.85} step={35} nowrap />
</div>
<div>
<Scramble text={`${lock.lat} · ${lock.lon}`} delay={0.95} step={30} nowrap />
<Scramble
text={`${lock.lat} · ${lock.lon}`}
delay={0.95}
step={30}
nowrap
/>
</div>
<div className="uppercase">
<Scramble text={lock.place} delay={1.05} step={30} nowrap />
@@ -67,7 +100,9 @@ export function Hero() {
className="trace seq absolute top-[233px] right-[510px] left-[calc(var(--lx)+66px)] hidden h-px bg-accent/55 xl:block"
style={d(0.9)}
>
<span className="absolute -top-4 left-0 text-[9px] whitespace-nowrap tracking-[0.2em] text-accent">TRACE ▸ 0001</span>
<span className="absolute -top-4 left-0 text-[9px] whitespace-nowrap tracking-[0.2em] text-accent">
TRACE ▸ 0001
</span>
<span className="absolute -top-[3px] -right-[3px] size-[7px] bg-accent" />
</div>
</div>
@@ -76,7 +111,11 @@ export function Hero() {
{/* Headline */}
<div className="xl:absolute xl:top-[364px] xl:left-16 xl:w-[620px]">
<p className="mb-3 text-[9px] tracking-[0.28em] text-mute xl:mb-5 xl:text-[10px] xl:tracking-[0.3em]">
<Scramble text="TARGET RESOLVED · SUBJECT 0001" delay={0.2} step={15} />
<Scramble
text="TARGET RESOLVED · SUBJECT 0001"
delay={0.2}
step={15}
/>
</p>
<h1
id="hero-title"
@@ -86,10 +125,13 @@ export function Hero() {
<br />
<Scramble text="& BUILDER" delay={0.5} step={35} nowrap />
</h1>
<p className="mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]">
{/*<p className="mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]">
<Scramble text={profile.stance} delay={0.7} step={15} />
</p>
<div className="resolve seq mt-7 flex flex-col gap-2.5 sm:flex-row sm:flex-wrap sm:gap-3 xl:mt-10" style={d(1.4)}>
</p>*/}
<div
className="resolve seq mt-7 flex flex-col gap-2.5 sm:flex-row sm:flex-wrap sm:gap-3 xl:mt-10"
style={d(1.4)}
>
<Link
href="/projects/"
className="inline-flex min-h-12 items-center justify-between gap-3 bg-ink px-[22px] text-xs font-medium tracking-[0.2em] text-night transition-colors hover:bg-accent hover:text-night"
@@ -100,7 +142,7 @@ export function Hero() {
href="/about/"
className="bracket inline-flex min-h-12 items-center justify-between gap-3 border border-white/12 px-[22px] text-xs tracking-[0.2em] transition-colors [--bc:rgb(242_244_245/0.6)] hover:border-transparent hover:text-accent hover:[--bc:var(--color-accent)]"
>
OPEN DOSSIER <span aria-hidden="true">↗</span>
ABOUT ME <span aria-hidden="true">↗</span>
</Link>
</div>
<DossierList fields={dossierFields} className="mt-9 xl:hidden" />
@@ -120,37 +162,64 @@ export function Hero() {
<figure className="bracket w-[112px] flex-none [--bc:rgb(242_244_245/0.4)] xl:w-[168px]">
<Bust src={profile.pfp} delay="1.2s" />
{profile.pfpIsPlaceholder && (
<figcaption className="pt-2 pb-0.5 text-[9px] tracking-[0.2em] text-mute max-xl:hidden">PFP · PLACEHOLDER</figcaption>
<figcaption className="pt-2 pb-0.5 text-[9px] tracking-[0.2em] text-mute max-xl:hidden">
PFP · PLACEHOLDER
</figcaption>
)}
</figure>
<div className="min-w-0 xl:pt-1">
<div className="text-[9px] tracking-[0.24em] text-mute">ALIAS</div>
<h2 id="profile-name" className="mt-1.5 font-display text-[28px] leading-none font-semibold tracking-[0.06em] uppercase xl:text-4xl">
<Scramble text={profile.displayName.toUpperCase()} delay={1.2} step={90} nowrap />
<div className="text-[9px] tracking-[0.24em] text-mute">
ALIAS
</div>
<h2
id="profile-name"
className="mt-1.5 font-display text-[28px] leading-none font-semibold tracking-[0.06em] uppercase xl:text-4xl"
>
<Scramble
text={profile.displayName.toUpperCase()}
delay={1.2}
step={90}
nowrap
/>
</h2>
<div className="mt-2.5 text-[9px] leading-[1.9] tracking-[0.16em] text-mute xl:mt-[22px]">
<div className="max-xl:hidden">MESH 2.5D · RESOLVED</div>
<div className="max-xl:hidden">CLEARANCE · PUBLIC</div>
{profile.pfpIsPlaceholder && <div className="xl:hidden">PFP · PLACEHOLDER</div>}
{profile.pfpIsPlaceholder && (
<div className="xl:hidden">PFP · PLACEHOLDER</div>
)}
</div>
</div>
</div>
<DossierList fields={dossierFields} className="mt-[22px] hidden xl:grid" />
<DossierList
fields={dossierFields}
className="mt-[22px] hidden xl:grid"
/>
</article>
</div>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 hidden xl:block">
<div className="relative mx-auto h-20 max-w-[1440px]">
{/* Corner telemetry, pinned to the bottom of the screen-tall hero */}
<div aria-hidden="true" className="appear seq absolute bottom-[22px] left-16 hidden text-[9px] leading-[1.8] tracking-[0.16em] text-mute/70 xl:block" style={d(0.2)}>
<div
aria-hidden="true"
className="appear seq absolute bottom-[22px] left-16 hidden text-[9px] leading-[1.8] tracking-[0.16em] text-mute/70 xl:block"
style={d(0.2)}
>
<SessionStamp />
</div>
<div aria-hidden="true" className="absolute bottom-[22px] left-1/2 hidden -translate-x-1/2 text-[9px] tracking-[0.3em] text-mute xl:block">
<div
aria-hidden="true"
className="absolute bottom-[22px] left-1/2 hidden -translate-x-1/2 text-[9px] tracking-[0.3em] text-mute xl:block"
>
SCROLL ▾ NODES
</div>
<div aria-hidden="true" className="appear seq absolute right-16 bottom-[22px] hidden text-right text-[9px] leading-[1.8] tracking-[0.16em] xl:block" style={d(0.2)}>
<div
aria-hidden="true"
className="appear seq absolute right-16 bottom-[22px] hidden text-right text-[9px] leading-[1.8] tracking-[0.16em] xl:block"
style={d(0.2)}
>
<div className="text-accent">
<LocalTime />
</div>
-9
View File
@@ -1,9 +0,0 @@
/** selimaj.dev mark: a lock bracket around a point. */
export function Mark({ size = 24, className }: { size?: number; className?: string }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path d="M2.5,8 V2.5 H8 M16,2.5 H21.5 V8 M21.5,16 V21.5 H16 M8,21.5 H2.5 V16" stroke="currentColor" strokeWidth="1.3" />
<rect x="9.5" y="9.5" width="5" height="5" fill="currentColor" />
</svg>
);
}
+26 -7
View File
@@ -3,7 +3,6 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { Mark } from "./Mark";
import { SessionClock } from "./Telemetry";
const links = [
@@ -13,7 +12,9 @@ const links = [
];
const isCurrent = (pathname: string, href: string) =>
href === "/" ? pathname === "/" : pathname.startsWith(href.replace(/\/$/, ""));
href === "/"
? pathname === "/"
: pathname.startsWith(href.replace(/\/$/, ""));
export function SiteHeader() {
const pathname = usePathname();
@@ -25,8 +26,11 @@ export function SiteHeader() {
className={`absolute inset-x-0 top-0 z-30 ${isHome ? "" : "border-b border-line"}`}
>
<div className="mx-auto flex max-w-[1440px] items-center justify-between gap-6 px-4 py-3 md:px-16 md:py-[22px]">
<Link href="/" className="flex min-h-11 items-center gap-3" aria-label="selimaj.dev home">
<Mark />
<Link
href="/"
className="flex min-h-11 items-center gap-3"
aria-label="selimaj.dev home"
>
<span className="font-display text-sm font-semibold tracking-[0.24em]">
SELIMAJ<span className="text-mute">.DEV</span>
</span>
@@ -40,7 +44,14 @@ export function SiteHeader() {
onClick={() => setOpen((o) => !o)}
>
{open ? "CLOSE" : "MENU"}
<svg width="18" height="12" viewBox="0 0 18 12" fill="none" stroke="currentColor" aria-hidden="true">
<svg
width="18"
height="12"
viewBox="0 0 18 12"
fill="none"
stroke="currentColor"
aria-hidden="true"
>
<path d="M0,1 H18 M6,6 H18 M0,11 H18" />
</svg>
</button>
@@ -61,7 +72,12 @@ export function SiteHeader() {
onClick={() => setOpen(false)}
className={`flex min-h-11 items-center gap-2 uppercase md:py-3.5 ${current ? "text-ink" : "text-mute hover:text-accent"}`}
>
{current && <span aria-hidden="true" className="size-1.5 bg-accent shadow-[0_0_8px_var(--color-accent)]" />}
{current && (
<span
aria-hidden="true"
className="size-1.5 bg-accent shadow-[0_0_8px_var(--color-accent)]"
/>
)}
{l.label}
</Link>
</li>
@@ -70,7 +86,10 @@ export function SiteHeader() {
</ul>
</nav>
<div aria-hidden="true" className="hidden text-right text-[10px] leading-relaxed tracking-[0.14em] text-mute lg:block">
<div
aria-hidden="true"
className="hidden text-right text-[10px] leading-relaxed tracking-[0.14em] text-mute lg:block"
>
<SessionClock />
</div>
</div>
+13 -10
View File
@@ -1,4 +1,7 @@
import type { Status } from "./projects";
import { projectById, type Status } from "./projects";
/** Proof statuses always come from the project list, so they can't drift. */
const statusOf = (id: string): Status => projectById(id)!.status;
export type Proof = {
label: string;
@@ -36,7 +39,7 @@ export const dossier: DossierFile[] = [
label: "Orus",
note: "My open-source brand for privacy and opsec tools",
href: "/projects/#orus",
status: "LIVE",
status: statusOf("orus"),
},
],
},
@@ -52,9 +55,9 @@ export const dossier: DossierFile[] = [
"Osias is a blockchain-powered AI agent for trading emotions and behavior. Alongside it sits a Minecraft economy concept with jurisdictions, taxes, and IPOs.",
],
proof: [
{ label: "Osias", note: "osias.trade", href: "https://osias.trade", status: "BUILDING" },
{ label: "Orus", note: "Open-source privacy and opsec brand", href: "/projects/#orus", status: "LIVE" },
{ label: "EquitySMP", note: "Real-world-style server economy", href: "/projects/#equitysmp", status: "BUILDING" },
{ label: "Osias", note: "osias.trade", href: "https://osias.trade", status: statusOf("osias") },
{ label: "Orus", note: "Open-source privacy and opsec brand", href: "/projects/#orus", status: statusOf("orus") },
{ label: "EquitySMP", note: "Real-world-style server economy", href: "/projects/#equitysmp", status: statusOf("equitysmp") },
],
},
{
@@ -69,9 +72,9 @@ export const dossier: DossierFile[] = [
"The interesting problems are usually one layer lower than where most people stop.",
],
proof: [
{ label: "Saturn", note: "Minecraft client, written from scratch", href: "/projects/#saturn", status: "ARCHIVED" },
{ label: "Enclave + transport protocol", note: "Chat platform, and the protocol it runs on", href: "/projects/#enclave", status: "BUILDING" },
{ label: "git.selimaj.dev", note: "Gitea on a Docker and Coolify homelab", href: "https://git.selimaj.dev", status: "LIVE" },
{ label: "Saturn", note: "Minecraft client, written from scratch", href: "/projects/#saturn", status: statusOf("saturn") },
{ label: "Enclave + transport protocol", note: "Chat platform, and the protocol it runs on", href: "/projects/#enclave", status: statusOf("enclave") },
{ label: "git.selimaj.dev", note: "Gitea on a Docker and Coolify homelab", href: "https://git.selimaj.dev", status: statusOf("git") },
],
},
{
@@ -102,8 +105,8 @@ export const dossier: DossierFile[] = [
"Long-range PC and 4G-controlled RC planes built around a custom watercooled powertrain. Algorithmic trading. Minecraft with friends.",
],
proof: [
{ label: "Cyberdecks", note: "Build photos and logs to come", href: "/projects/#briefcase-deck", status: "BUILDING" },
{ label: "RC planes", note: "Build photos and logs to come", href: "/projects/#rc-planes", status: "BUILDING" },
{ label: "Cyberdecks", note: "Build photos and logs to come", href: "/projects/#briefcase-deck", status: statusOf("briefcase-deck") },
{ label: "RC planes", note: "Build photos and logs to come", href: "/projects/#rc-planes", status: statusOf("rc-planes") },
],
},
];
+4 -4
View File
@@ -8,9 +8,9 @@ export const profile = {
displayName: "Selimaj",
title: "Entrepreneur & Builder",
stance: "I build the layer underneath.",
// PLACEHOLDER: replace this one file to swap the profile picture.
pfp: "/profile/pfp.svg",
pfpIsPlaceholder: true,
// Replace this one file to swap the profile picture (the favicon is src/app/icon.png).
pfp: "/profile/Mono.png",
pfpIsPlaceholder: false,
github: "https://github.com/selimaj-dev",
git: "https://git.selimaj.dev",
} as const;
@@ -35,7 +35,7 @@ export const dossierFields: DossierField[] = [
{ label: "Based", value: "Tirana, Albania" },
{ label: "Brands", value: "Orus" },
// PLACEHOLDER decision: remove this entry to stop stating it publicly.
{ label: "Building since", value: "12" },
{ label: "Age", value: "16" },
{ label: "Focus", value: "Privacy, independence, opsec, dev tooling" },
{ label: "Known for", value: "Saturn, Enclave" },
{ label: "Record", value: "Formerly #1 on Discipline, Albania" },