redraw the desk as cartoon skeuomorphism: CSS-only materials (no textures), light cream desk, per-item hues, cork board, receipt, scoreboard

This commit is contained in:
Martin Dvořák 2026-09-30 09:59:00 +00:00
commit 1880a5b0db
14 changed files with 1165 additions and 989 deletions

View file

@ -2,16 +2,17 @@
> The source code for my personal website, **[dvorakmortonxd.dev](https://dvorakmortonxd.dev)**. > The source code for my personal website, **[dvorakmortonxd.dev](https://dvorakmortonxd.dev)**.
It's a **skeuomorphic personal site**: a near-black desk with anodised hardware, machined It's a **cartoon-skeuomorphic personal site**: a warm cream desk with chunky, brightly coloured
aluminium, clean printed labels and a single orange accent, with the whole KDE desktop hardware resting on it, drawn entirely in CSS — no textures, no images — with the whole KDE
simulation kept behind a physical power switch. Two modes, one content set: desktop simulation kept behind a physical power switch. Two modes, one content set:
- **Web** (default) — the desk. A monitor carries the headline with a mechanical keyboard in - **Web** (default) — the desk. A monitor carries the headline with a candy-keycap keyboard in
front of it and a small rack beside it; services are 1U rack units with health LEDs, the 18 front of it and a mini rack beside it; services are 1U rack units with coloured strips and
coding projects are labelled drives in a drawer, the machines are barcoded asset labels on a health LEDs, the 18 coding projects are labelled drives in a drawer, the machines are cards
pegboard, the contribution heatmap is printed on a label-printer sheet, friends are business pinned to a cork board, the contribution heatmap is printed on a receipt, friends are business
cards, the gym log is a clipboard and the socials are a patch panel. Textures are CC0 — see cards, the gym numbers are a scoreboard and the socials are a patch panel with coloured ports.
[`/credits`](https://dvorakmortonxd.dev/credits). [`/credits`](https://dvorakmortonxd.dev/credits) lists the type and nothing else — there are no
asset licences because there are no assets.
- **Desktop mode** — the KDE Plasma / Windows-style **desktop simulation**: draggable - **Desktop mode** — the KDE Plasma / Windows-style **desktop simulation**: draggable
snap-to-grid icons, resizable/floating windows, a Start menu, taskbar, live clock and a snap-to-grid icons, resizable/floating windows, a Start menu, taskbar, live clock and a
working in-browser terminal (Alacritty). Reached from the power switch and hidden on phones. working in-browser terminal (Alacritty). Reached from the power switch and hidden on phones.
@ -24,10 +25,12 @@ heatmap**, gym SBD and links to friends' sites.
- **Web theme is the landing view** — the site always opens on the desk; the theme is - **Web theme is the landing view** — the site always opens on the desk; the theme is
deliberately not persisted, so a reload never drops you into the desktop. deliberately not persisted, so a reload never drops you into the desktop.
- **Skeuomorphic materials, modern finish** — near-black desk, dark anodised faceplates, - **CSS-only skeuomorphism** — every surface is a flat gradient with a thick bottom edge, a soft
machined aluminium plaques, matte printed labels and mint/orange LEDs, all CSS over five CC0 cast shadow and `color-mix()` tints: slate chassis, cream plastic, paper, cork, black glass,
textures. Every section is a physical object with a contact shadow; nothing is pure white on LEDs, screws, pushpins. The web theme ships no images at all.
black or grey on black. - **Not afraid of colour** — a hue per item (orange / teal / blue / lilac / mint / coral /
yellow) cycles through every list, so the desk reads as separate pieces of hardware instead of
one grey wall.
- **Desktop mode (desktop-only)** — a physical power switch in the top rail (and in the footer) - **Desktop mode (desktop-only)** — a physical power switch in the top rail (and in the footer)
flips to the KDE-style desktop for the session. Phones never get it: the switch is hidden, so flips to the KDE-style desktop for the session. Phones never get it: the switch is hidden, so
the old iOS-style phone home screen is gone. the old iOS-style phone home screen is gone.
@ -67,15 +70,16 @@ heatmap**, gym SBD and links to friends' sites.
- **Framework:** Next.js 16 (App Router, Turbopack) - **Framework:** Next.js 16 (App Router, Turbopack)
- **Language:** TypeScript, React 19 (client components everywhere interactivity lives) - **Language:** TypeScript, React 19 (client components everywhere interactivity lives)
- **Styling:** `src/app/globals.css` holds the palette tokens, base styles, the Tailwind v4 - **Styling:** `src/app/globals.css` holds the palette tokens, base styles, the Tailwind v4
`@theme`/`@utility` classes and the scroll reveals. `src/app/web.css` is the material system `@theme`/`@utility` classes and the scroll reveals. `src/app/web.css` is the whole desk: the
(near-black desk, anodised metal, machined aluminium, printed labels, black glass, LEDs, rack materials (slate chassis, cream plastic, paper, cork, black glass), the details (screws, LEDs,
rails, rocker switch) and the object layouts. `page.css` keeps the shared component styles the pushpins, rack rails, barcode, torn receipt edge) and every object layout, all CSS with
desktop apps still use, and `skeuo.css` is the KDE desktop. `color-mix()` for tints and shades. `page.css` keeps the shared component styles the desktop
- **Fonts:** IBM Plex Sans/Mono for screens, machine labels and headings; Newsreader for things apps still use, and `skeuo.css` is the KDE desktop.
printed on paper; via `next/font/google`. The desktop keeps its system sans stack. - **Fonts:** Nunito for hardware labels and headings, Newsreader for anything printed on paper,
- **Textures:** five CC0 materials from ambientCG — `desk` (near-black wood), `metal`, `paper`, IBM Plex Sans/Mono for body copy and terminal text — all via `next/font/google`. The desktop
`carbon`, `plastic` — downscaled to 512 px in `public/textures`. Attribution is on `/credits`. keeps its system sans stack.
- **Version:** `3.3.0` (see `package.json`) - **Assets:** the web theme ships **no images**. The desktop keeps its own wallpaper and icons.
- **Version:** `4.0.0` (see `package.json`)
## 🚀 Running Locally ## 🚀 Running Locally
@ -182,7 +186,7 @@ src/app/
page.css → shared component styles still used by the desktop apps page.css → shared component styles still used by the desktop apps
skeuo.css → KDE desktop styles skeuo.css → KDE desktop styles
data.ts → socials, codingProjects, techProjects, friends, wallpapers data.ts → socials, codingProjects, techProjects, friends, wallpapers
credits/page.tsx → credit page for the textures and fonts credits/page.tsx → credit page (type only — the desk ships no assets)
revival-inc/page.tsx → embeds the bundled game at /public/revival-inc/index.html revival-inc/page.tsx → embeds the bundled game at /public/revival-inc/index.html
api/ api/
heatmap/route.ts → contribution heatmap proxy heatmap/route.ts → contribution heatmap proxy

View file

@ -34,20 +34,24 @@ When in doubt, prefer updating. A stale README **or** agents.md is treated as a
## What this project is ## What this project is
Personal portfolio site for **dvorakmortonxd.dev**. The default view is a Personal portfolio site for **dvorakmortonxd.dev**. The default view is a
**skeuomorphic desk**: a near-black surface with a monitor, mechanical keyboard, **cartoon-skeuomorphic desk**: a warm cream surface with a monitor, candy-keycap
rack, drive drawer, pegboard of asset labels, label-printer report, business keyboard, mini rack, drive drawer, cork board of pinned cards, printed receipt,
cards, clipboard and patch panel resting on it. A physical power switch flips to business cards, scoreboard and patch panel resting on it — all drawn in CSS, with
a **KDE Plasma desktop simulation**. Phones only ever get the desk. no image assets. A physical power switch flips to a **KDE Plasma desktop
simulation**. Phones only ever get the desk.
- Framework: **Next.js 16** (App Router, Turbopack), **React 19**, TypeScript. - Framework: **Next.js 16** (App Router, Turbopack), **React 19**, TypeScript.
- Styling: **Tailwind v4** for layout plus **`web.css`**, the material system - Styling: **Tailwind v4** for layout plus **`web.css`**, which is the whole desk
(near-black desk, anodised metal, machined aluminium, printed labels, black — materials (slate chassis, cream plastic, paper, cork, black glass), details
glass, LEDs, rack rails, rocker switch) over five CC0 textures in (screws, LEDs, pushpins, rails, barcode, torn receipt edge) and object layouts,
`public/textures` (credited on `/credits`). `globals.css` holds the tokens, all CSS with `color-mix()` tints. `globals.css` holds the tokens, base styles
base styles and the reveals; `skeuo.css` and `page.css` are the desktop and and the reveals; `skeuo.css` and `page.css` are the desktop and the shared
the shared desktop-app styles. desktop-app styles. **The web theme ships no images.**
- Hue system: list items take a `--tone` custom property from a shared palette
(orange, teal, blue, lilac, mint, coral, yellow), set inline, so objects are
colourful without per-item CSS.
- Path alias: `@/*` → `src/*`. - Path alias: `@/*` → `src/*`.
- Version: `3.3.0` (see `package.json`). - Version: `4.0.0` (see `package.json`).
### Scripts ### Scripts
```bash ```bash
@ -85,7 +89,7 @@ src/app/
page.css → shared component styles used by the desktop apps page.css → shared component styles used by the desktop apps
skeuo.css → KDE Plasma desktop styles skeuo.css → KDE Plasma desktop styles
data.ts → socials, codingProjects, techProjects, friends, wallpapers data.ts → socials, codingProjects, techProjects, friends, wallpapers
credits/page.tsx → credits for the CC0 textures and fonts credits/page.tsx → credits (type only — the desk ships no assets)
revival-inc/page.tsx → fullscreen iframe embedding /public/revival-inc/index.html revival-inc/page.tsx → fullscreen iframe embedding /public/revival-inc/index.html
api/ api/
heatmap/route.ts → proxy for git.dvorakmortonxd.dev contribution heatmap heatmap/route.ts → proxy for git.dvorakmortonxd.dev contribution heatmap
@ -124,14 +128,14 @@ scripts/
desktop is up so its scroll position survives; the desktop mounts on demand desktop is up so its scroll position survives; the desktop mounts on demand
(phones can never reach `skeuo`). (phones can never reach `skeuo`).
Modes: `web` = the skeuomorphic desk (default/landing), `skeuo` = KDE Plasma Modes: `web` = the cartoon-skeuomorphic desk (default/landing), `skeuo` = KDE
desktop. Plasma desktop.
The desk uses **five CC0 textures** from ambientCG (`public/textures`: near-black The desk ships **no image assets** — every surface is CSS. The only external
desk wood, metal, paper, carbon, plastic), downscaled to 512 px and credited on visual files are the social SVGs, used as monochrome CSS masks (`.social-mask`);
`/credits`. The social SVGs are used as monochrome CSS masks (`.social-mask`);
`linkedin.svg` is a filled badge that masks to a plain square, so `web-ui.tsx` `linkedin.svg` is a filled badge that masks to a plain square, so `web-ui.tsx`
renders an inline mark for it instead. The desktop still uses the image assets. renders an inline mark for it instead. The desktop still uses its own wallpaper
and icons.
--- ---
@ -242,6 +246,12 @@ not free placement. Don't confuse the two.
- Angles and offsets that hover states need to override live in CSS custom - Angles and offsets that hover states need to override live in CSS custom
properties (e.g. `--tilt` on `.w-drive`), never in inline `style` transforms — properties (e.g. `--tilt` on `.w-drive`), never in inline `style` transforms —
an inline transform wins over `:hover`. an inline transform wins over `:hover`.
- **The web desk is CSS-only by design.** No textures, no images, no `url()`
beyond the social masks. Build a surface from a flat gradient plus a thick
bottom edge (`0 5px 0 <edge-colour>`) and a soft cast shadow, and shade with
`color-mix(in srgb, var(--tone) N%, …)` instead of adding new hues to the
stylesheet. New list items take a `--tone` from the shared `TONES` palette in
`WebSite.tsx` / `web-ui.tsx` rather than hard-coding a colour.
- The desk reveals blocks with `.reveal` (see `Reveal` in `web-ui.tsx`): hidden by - The desk reveals blocks with `.reveal` (see `Reveal` in `web-ui.tsx`): hidden by
default, `.is-in` shows. On load the first screenful cascades top-to-bottom default, `.is-in` shows. On load the first screenful cascades top-to-bottom
(90ms per block, in DOM order); the rest reveal when they scroll in. **Wrap (90ms per block, in DOM order); the rest reveal when they scroll in. **Wrap

View file

@ -1,6 +1,6 @@
{ {
"name": "dvorakmortonxd", "name": "dvorakmortonxd",
"version": "3.3.0", "version": "4.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "node scripts/dev-server.mjs", "dev": "node scripts/dev-server.mjs",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -27,6 +27,17 @@ const NAV = [
{ href: "#contact", label: "contact" }, { href: "#contact", label: "contact" },
]; ];
/** One hue per item, so every list reads as separate pieces of hardware. */
const TONES = [
"var(--orange)",
"var(--teal)",
"var(--blue)",
"var(--lilac)",
"var(--mint)",
"var(--coral)",
"var(--yellow)",
];
const SERVICES: { const SERVICES: {
name: string; name: string;
desc: string; desc: string;
@ -72,11 +83,11 @@ export default function WebSite() {
return ( return (
<div className="w-page"> <div className="w-page">
{/* -------------------------------------------------------------- rail */} {/* -------------------------------------------------------------- rail */}
<header className="w-rail w-mt-metal-flush" style={{ borderRadius: 0 }}> <header className="w-rail">
<div className="mx-auto flex h-16 w-full max-w-[1120px] items-center gap-5 px-5 sm:px-8"> <div className="mx-auto flex h-16 w-full max-w-[1120px] items-center gap-5 px-5 sm:px-8">
<a href="#top" className="flex shrink-0 items-center gap-2.5" aria-label="Back to top"> <a href="#top" className="flex shrink-0 items-center gap-2.5" aria-label="Back to top">
<Led kind="on" pulse slow /> <Led kind="on" pulse slow />
<span className="w-wordmark w-engrave">dvorakmortonxd</span> <span className="w-wordmark">dvorakmortonxd</span>
</a> </a>
<nav className="ml-auto hidden items-center gap-6 md:flex" aria-label="Main"> <nav className="ml-auto hidden items-center gap-6 md:flex" aria-label="Main">
@ -95,7 +106,7 @@ export default function WebSite() {
className="desktop-toggle flex items-center gap-3" className="desktop-toggle flex items-center gap-3"
title="Flip to Desktop mode" title="Flip to Desktop mode"
> >
<span className="w-mono hidden text-[0.6rem] tracking-[0.2em] text-[var(--ftext-faint)] sm:inline"> <span className="w-mono hidden text-[0.6rem] tracking-[0.2em] text-[var(--ink-soft)] sm:inline">
DESKTOP DESKTOP
</span> </span>
<span className="w-rocker"> <span className="w-rocker">
@ -110,7 +121,7 @@ export default function WebSite() {
onClick={() => setMenuOpen((v) => !v)} onClick={() => setMenuOpen((v) => !v)}
aria-expanded={menuOpen} aria-expanded={menuOpen}
aria-label={menuOpen ? "Close menu" : "Open menu"} aria-label={menuOpen ? "Close menu" : "Open menu"}
className="w-keybtn !px-3 !py-2" className="w-keybtn"
> >
{menuOpen ? "close" : "menu"} {menuOpen ? "close" : "menu"}
</button> </button>
@ -120,7 +131,7 @@ export default function WebSite() {
{menuOpen ? ( {menuOpen ? (
<nav <nav
className="w-mt-metal-flush mx-auto mb-3 flex w-[calc(100%-2.5rem)] max-w-[1120px] flex-col gap-1 rounded-lg p-3 md:hidden" className="w-plastic mx-auto mb-3 flex w-[calc(100%-2.5rem)] max-w-[1120px] flex-col gap-1 p-3 md:hidden"
aria-label="Main" aria-label="Main"
> >
{NAV.map((item) => ( {NAV.map((item) => (
@ -128,7 +139,7 @@ export default function WebSite() {
key={item.href} key={item.href}
href={item.href} href={item.href}
onClick={() => setMenuOpen(false)} onClick={() => setMenuOpen(false)}
className="w-mono border-b border-[rgba(255,226,180,0.07)] py-3 text-[0.78rem] text-[var(--ftext-dim)] last:border-b-0" className="w-display border-b-2 border-dashed border-[var(--plastic-edge)] py-3 text-[0.9rem] text-[var(--plastic-ink)] last:border-b-0"
> >
{item.label} {item.label}
</a> </a>
@ -149,18 +160,18 @@ export default function WebSite() {
<span className="a">martin@dvorakmortonxd</span>:~$ whoami <span className="a">martin@dvorakmortonxd</span>:~$ whoami
<span className="w-caret ml-2" /> <span className="w-caret ml-2" />
</p> </p>
<h1 className="w-display mt-5 text-[clamp(1.8rem,4.1vw,2.75rem)] text-[var(--ftext)]"> <h1 className="w-display mt-5 text-[clamp(1.8rem,4.1vw,2.75rem)] text-[var(--glass-ink)]">
My own little internet — one rack, zero subscriptions. My own little internet — one rack, zero subscriptions.
</h1> </h1>
<p className="w-prose mt-5 max-w-[54ch] text-[var(--ftext-dim)]"> <p className="w-prose mt-5 max-w-[54ch] text-[var(--glass-dim)]">
I&rsquo;m Martin Dvořák. Git, media, music, an AI box and a Minecraft server all run on a I&rsquo;m Martin Dvořák. Git, media, music, an AI box and a Minecraft server all run on a
Proxmox machine in my room — plus the hardware graveyard I keep rescuing from e-waste. Proxmox machine in my room — plus the hardware graveyard I keep rescuing from e-waste.
</p> </p>
<p className="mt-7 flex flex-wrap gap-x-8 gap-y-3"> <p className="mt-7 flex flex-wrap gap-3">
<a href="#services" className="w-cmd"> <a href="#services" className="w-cmd">
$ open rack --services $ open rack --services
</a> </a>
<a href="#projects" className="w-cmd"> <a href="#projects" className="w-cmd w-cmd--quiet">
$ open drawer --projects $ open drawer --projects
</a> </a>
</p> </p>
@ -175,10 +186,10 @@ export default function WebSite() {
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<MiniRack /> <MiniRack />
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<Plaque value={String(SERVICES.length)} label="services" /> <Plaque value={String(SERVICES.length)} label="services" tone="var(--orange)" />
<Plaque value={String(codingProjects.length)} label="projects" /> <Plaque value={String(codingProjects.length)} label="projects" tone="var(--teal)" />
<Plaque value={String(techProjects.length)} label="machines" /> <Plaque value={String(techProjects.length)} label="machines" tone="var(--blue)" />
<Plaque value="401" label="kg best SBD" /> <Plaque value="401" label="kg best SBD" tone="var(--coral)" />
</div> </div>
</div> </div>
</Reveal> </Reveal>
@ -193,7 +204,7 @@ export default function WebSite() {
</Reveal> </Reveal>
<div className="grid gap-7 lg:grid-cols-[minmax(0,1.55fr)_minmax(0,1fr)] lg:items-start"> <div className="grid gap-7 lg:grid-cols-[minmax(0,1.55fr)_minmax(0,1fr)] lg:items-start">
<Reveal delay={60}> <Reveal delay={60}>
<article className="w-mt-paper w-sheet"> <article className="w-paper w-sheet">
<div className="w-sheet__head"> <div className="w-sheet__head">
<span>manifest.txt</span> <span>manifest.txt</span>
<span className="ml-auto">printed on the desk</span> <span className="ml-auto">printed on the desk</span>
@ -224,15 +235,13 @@ export default function WebSite() {
</Reveal> </Reveal>
<Reveal delay={150}> <Reveal delay={150}>
<aside className="w-mt-paper w-cardindex lg:mt-12"> <aside className="w-paper w-cardindex lg:mt-14" style={{ "--tone": "var(--teal)" } as CSSProperties}>
<p className="w-mono text-[0.62rem] tracking-[0.16em] text-[var(--ink-soft)]"> <p className="w-kicker">index card</p>
INDEX CARD <p className="w-serif mt-3 text-[1.2rem] text-[var(--ink)]">
</p>
<p className="w-serif mt-3 text-[1.15rem] text-[var(--ink)]">
&ldquo;If it has a fan, I&rsquo;ll find a use for it.&rdquo; &ldquo;If it has a fan, I&rsquo;ll find a use for it.&rdquo;
</p> </p>
<div className="w-rule my-4" /> <div className="w-rule my-4" />
<ul className="w-mono space-y-2 text-[0.66rem] text-[var(--ink-soft)]"> <ul className="w-mono space-y-2 text-[0.68rem] text-[var(--ink-soft)]">
<li>school · sssvt.cz</li> <li>school · sssvt.cz</li>
<li>gym · PPL 3–5×/week</li> <li>gym · PPL 3–5×/week</li>
<li>games · Terraria, Minecraft</li> <li>games · Terraria, Minecraft</li>
@ -251,14 +260,13 @@ export default function WebSite() {
<div className="grid gap-7 lg:grid-cols-[minmax(0,1.3fr)_minmax(0,1fr)] lg:items-start"> <div className="grid gap-7 lg:grid-cols-[minmax(0,1.3fr)_minmax(0,1fr)] lg:items-start">
<Reveal delay={60}> <Reveal delay={60}>
<div className="w-note"> <div className="w-note">
<p className="w-mono text-[0.62rem] tracking-[0.16em] text-[#6b7075]"> <span className="w-pin" aria-hidden="true" />
NOW.TXT · UPDATED 09/2026 <p className="w-kicker !text-[#8a7420]">now.txt · updated 09/2026</p>
</p> <p className="w-prose mt-4 text-[#5c4a1f]">
<p className="w-prose mt-4 text-[#34383b]">
I&rsquo;m hosting an SMP — message me on Discord if you want in. I&rsquo;m also cooking most I&rsquo;m hosting an SMP — message me on Discord if you want in. I&rsquo;m also cooking most
days, bulking through the non-summer seasons. days, bulking through the non-summer seasons.
</p> </p>
<p className="w-prose mt-3 text-[#34383b]"> <p className="w-prose mt-3 text-[#5c4a1f]">
Me and my friend{" "} Me and my friend{" "}
<a className="w-link" href="https://skopec.dev" target="_blank" rel="noopener noreferrer"> <a className="w-link" href="https://skopec.dev" target="_blank" rel="noopener noreferrer">
Pepa Pepa
@ -278,24 +286,25 @@ export default function WebSite() {
</Reveal> </Reveal>
<Reveal delay={140}> <Reveal delay={140}>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-8">
<div className="w-mt-paper w-cardindex w-cardindex--alt"> <div className="w-paper w-cardindex w-cardindex--alt" style={{ "--tone": "var(--coral)" } as CSSProperties}>
<p className="w-mono text-[0.62rem] tracking-[0.16em] text-[var(--ink-soft)]"> <p className="w-kicker">bench status</p>
BENCH STATUS <ul className="w-display mt-4 space-y-2.5 text-[0.82rem] text-[var(--ink)]">
</p> <li className="flex items-center gap-2.5">
<ul className="w-mono mt-3 space-y-2 text-[0.7rem] text-[var(--ink)]">
<li className="flex items-center gap-2">
<Led kind="on" /> smp · accepting players <Led kind="on" /> smp · accepting players
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2.5">
<Led kind="amber" pulse /> maturita.c · in development <Led kind="amber" pulse /> maturita.c · in development
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2.5">
<Led kind="on" /> gym · PPL 3–5×/week <Led kind="on" /> gym · PPL 3–5×/week
</li> </li>
</ul> </ul>
</div> </div>
<i className="w-pen ml-6 w-44" aria-hidden="true" /> <div className="flex items-end gap-6">
<i className="w-pen mb-3 w-40" aria-hidden="true" />
<span className="w-mug mr-6" aria-hidden="true" />
</div>
</div> </div>
</Reveal> </Reveal>
</div> </div>
@ -307,11 +316,13 @@ export default function WebSite() {
<SectionHead index="03" label="rack" title="Six machines, one shelf" /> <SectionHead index="03" label="rack" title="Six machines, one shelf" />
</Reveal> </Reveal>
<Reveal delay={60}> <Reveal delay={60}>
<div className="w-mt-metal w-rack"> <div className="w-chassis w-rack">
<div className="w-rack__top"> <div className="w-rack__top">
<Led kind="on" pulse slow /> <Led kind="on" pulse slow />
<span className="w-engrave text-[0.62rem] tracking-[0.2em]">RACK 01 · SERVICES</span> <span className="w-display text-[0.7rem] tracking-[0.18em] text-[var(--chassis-ink)]">
<span className="w-mono ml-auto hidden text-[0.6rem] tracking-[0.14em] text-[var(--ftext-faint)] sm:inline"> RACK 01 · SERVICES
</span>
<span className="w-mono ml-auto hidden text-[0.62rem] tracking-[0.12em] text-[var(--chassis-faint)] sm:inline">
health checked from this page health checked from this page
</span> </span>
<span className="w-rack__vent w-vents" /> <span className="w-rack__vent w-vents" />
@ -319,8 +330,8 @@ export default function WebSite() {
<div className="w-rack__grid"> <div className="w-rack__grid">
<span className="w-rails" /> <span className="w-rails" />
<div> <div>
{SERVICES.map((s) => ( {SERVICES.map((s, i) => (
<ServiceUnit key={s.name} {...s} /> <ServiceUnit key={s.name} tone={TONES[i % TONES.length]} {...s} />
))} ))}
</div> </div>
<span className="w-rails" /> <span className="w-rails" />
@ -335,8 +346,8 @@ export default function WebSite() {
<SectionHead index="04" label="drawer" title="Eighteen drives, all labelled" /> <SectionHead index="04" label="drawer" title="Eighteen drives, all labelled" />
</Reveal> </Reveal>
<Reveal delay={60}> <Reveal delay={60}>
<div className="w-mt-metal w-drawer"> <div className="w-chassis w-drawer">
<div className="w-mt-plastic w-drawer__inner w-scroll"> <div className="w-well w-drawer__inner w-scroll">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{codingProjects.map((p, i) => { {codingProjects.map((p, i) => {
const { name, status } = splitStatus(p.title); const { name, status } = splitStatus(p.title);
@ -347,7 +358,12 @@ export default function WebSite() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-drive" className="w-drive"
style={{ "--tilt": `${i % 2 ? 0.5 : -0.6}deg` } as CSSProperties} style={
{
"--tilt": `${i % 2 ? 0.6 : -0.7}deg`,
"--tone": TONES[i % TONES.length],
} as CSSProperties
}
title={p.desc} title={p.desc}
> >
<span className="w-drive__port" aria-hidden="true" /> <span className="w-drive__port" aria-hidden="true" />
@ -364,7 +380,7 @@ export default function WebSite() {
})} })}
</div> </div>
</div> </div>
<div className="w-mt-metal-flush w-drawer__front"> <div className="w-chassis-2 w-drawer__front">
<i className="w-screw" aria-hidden="true" /> <i className="w-screw" aria-hidden="true" />
<span className="w-drawer__handle" aria-hidden="true" /> <span className="w-drawer__handle" aria-hidden="true" />
<i className="w-screw" aria-hidden="true" /> <i className="w-screw" aria-hidden="true" />
@ -376,18 +392,22 @@ export default function WebSite() {
{/* ------------------------------------------------------ hardware */} {/* ------------------------------------------------------ hardware */}
<section id="hardware" className="scroll-mt-28"> <section id="hardware" className="scroll-mt-28">
<Reveal> <Reveal>
<SectionHead index="05" label="machines" title="Tagged on the pegboard" /> <SectionHead index="05" label="machines" title="Pinned to the cork board" />
</Reveal> </Reveal>
<Reveal delay={60}> <Reveal delay={60}>
<div className="w-mt-metal w-pegboard"> <div className="w-pegboard">
<div className="w-pegboard__face"> <div className="w-pegboard__face">
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
{techProjects.map((t) => ( {techProjects.map((t, i) => (
<article key={t.title} className="w-mt-paper w-hangtag"> <article
<span className="w-hangtag__clip" aria-hidden="true" /> key={t.title}
<h3 className="w-display text-[1rem] text-[var(--ink)]">{t.title}</h3> className="w-paper w-hangtag"
<p className="w-prose mb-4 mt-2 text-[0.8rem] text-[var(--ink-soft)]">{t.desc}</p> style={{ "--tone": TONES[i % TONES.length] } as CSSProperties}
<span className="w-barcode w-hangtag__bar" aria-hidden="true" /> >
<span className="w-pin" aria-hidden="true" />
<h3 className="w-display text-[1.05rem] text-[var(--ink)]">{t.title}</h3>
<span className="w-hangtag__rule" aria-hidden="true" />
<p className="w-prose text-[0.84rem] text-[var(--ink-soft)]">{t.desc}</p>
</article> </article>
))} ))}
</div> </div>
@ -399,7 +419,7 @@ export default function WebSite() {
{/* -------------------------------------------------- activity feed */} {/* -------------------------------------------------- activity feed */}
<section id="gitdxd" className="scroll-mt-28"> <section id="gitdxd" className="scroll-mt-28">
<Reveal> <Reveal>
<SectionHead index="06" label="gitdxd" title="The printer spits out the forge" /> <SectionHead index="06" label="gitdxd" title="The receipt for this year's commits" />
</Reveal> </Reveal>
<Reveal delay={60}> <Reveal delay={60}>
<Report /> <Report />
@ -418,11 +438,16 @@ export default function WebSite() {
href={f.realUrl || f.url} href={f.realUrl || f.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-mt-paper w-bizcard" className="w-bizcard"
style={{ "--tilt": `${i === 1 ? 0.9 : i === 2 ? -1 : -1.4}deg` } as CSSProperties} style={
{
"--tilt": `${i === 1 ? 0.9 : i === 2 ? -1 : -1.4}deg`,
"--tone": TONES[(i + 1) % TONES.length],
} as CSSProperties
}
> >
<p className="w-serif text-[1.15rem] text-[var(--ink)]">{f.name}</p> <p className="w-serif text-[1.2rem] text-[var(--ink)]">{f.name}</p>
<p className="w-mono mt-1 text-[0.66rem] text-[var(--ink-soft)]">{f.desc}</p> <p className="w-mono mt-1 text-[0.68rem] text-[var(--ink-soft)]">{f.desc}</p>
</a> </a>
</Reveal> </Reveal>
))} ))}
@ -432,29 +457,32 @@ export default function WebSite() {
{/* ----------------------------------------------------------- gym */} {/* ----------------------------------------------------------- gym */}
<section id="gym" className="scroll-mt-28"> <section id="gym" className="scroll-mt-28">
<Reveal> <Reveal>
<SectionHead index="08" label="strength" title="Numbers on the clipboard" /> <SectionHead index="08" label="strength" title="Numbers on the scoreboard" />
</Reveal> </Reveal>
<Reveal delay={60}> <Reveal delay={60}>
<div className="w-clipboard"> <div className="w-chassis w-score">
<div className="w-mt-metal w-clipboard__clip" aria-hidden="true" /> <div className="w-score__head">
<div className="w-mt-paper w-logsheet"> <Led kind="on" pulse slow />
<div className="w-sheet__head"> <span>TRAINING LOG · 73 KG BODYWEIGHT</span>
<span>training log</span> <span className="ml-auto hidden sm:inline">3 LIFTS</span>
<span className="ml-auto">73 kg bodyweight</span>
</div>
<div className="mt-3">
{LIFTS.map((l) => (
<div key={l.lift} className="w-logrow">
<span className="w-logrow__lift">{l.lift}</span>
<span className="w-logrow__weight">{l.weight} kg</span>
<span className="w-logrow__date">{l.date}</span>
</div>
))}
</div>
<p className="w-mono mt-4 text-[0.62rem] tracking-[0.14em] text-[var(--ink-soft)]">
BEST SBD TOTAL · 401 KG · 165 + 111 + 125
</p>
</div> </div>
<div className="w-score__tiles">
{LIFTS.map((l, i) => (
<div
key={l.lift}
className="w-score__tile"
style={{ "--tone": TONES[(i + 2) % TONES.length] } as CSSProperties}
>
<span className="w-score__num">
{l.weight}
<small>kg</small>
</span>
<span className="w-score__lift">{l.lift}</span>
<span className="w-score__date">{l.date}</span>
</div>
))}
</div>
<p className="w-score__foot">BEST SBD TOTAL · 401 KG · 165 + 111 + 125</p>
</div> </div>
</Reveal> </Reveal>
</section> </section>
@ -465,16 +493,23 @@ export default function WebSite() {
<SectionHead index="09" label="patch panel" title="Twelve ports, all live" /> <SectionHead index="09" label="patch panel" title="Twelve ports, all live" />
</Reveal> </Reveal>
<Reveal delay={60}> <Reveal delay={60}>
<div className="w-mt-metal w-patch"> <div className="w-chassis w-patch">
<div className="w-mt-metal-flush w-patch__face"> <div className="w-chassis-2 w-patch__face">
<div className="w-patch__head"> <div className="w-patch__head">
<i className="w-screw" aria-hidden="true" /> <i className="w-screw" aria-hidden="true" />
<span className="w-engrave text-[0.62rem] tracking-[0.2em]">PATCH 01 · SOCIALS</span> <span>PATCH 01 · SOCIALS</span>
<i className="w-screw ml-auto" aria-hidden="true" /> <i className="w-screw ml-auto" aria-hidden="true" />
</div> </div>
<div className="grid gap-2.5 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-2.5 sm:grid-cols-2 xl:grid-cols-3">
{socials.map((s) => ( {socials.map((s, i) => (
<a key={s.name} href={s.url} target="_blank" rel="noopener noreferrer" className="w-port"> <a
key={s.name}
href={s.url}
target="_blank"
rel="noopener noreferrer"
className="w-port"
style={{ "--tone": TONES[i % TONES.length] } as CSSProperties}
>
<span className="w-port__jack" aria-hidden="true"> <span className="w-port__jack" aria-hidden="true">
<span className="w-port__slot" /> <span className="w-port__slot" />
</span> </span>
@ -485,7 +520,7 @@ export default function WebSite() {
</a> </a>
))} ))}
</div> </div>
<p className="w-mono mt-5 text-[0.64rem] tracking-[0.06em] text-[var(--ftext-faint)]"> <p className="w-mono mt-5 text-[0.66rem] text-[var(--chassis-faint)]">
fastest reply is Discord or Instagram · mail{" "} fastest reply is Discord or Instagram · mail{" "}
<a className="w-link" href="mailto:dvorak@dvorakmortonxd.dev"> <a className="w-link" href="mailto:dvorak@dvorakmortonxd.dev">
dvorak@dvorakmortonxd.dev dvorak@dvorakmortonxd.dev
@ -501,11 +536,13 @@ export default function WebSite() {
{/* ------------------------------------------------------------ footer */} {/* ------------------------------------------------------------ footer */}
<footer className="mx-auto w-full max-w-[1120px] px-5 pb-16 sm:px-8"> <footer className="mx-auto w-full max-w-[1120px] px-5 pb-16 sm:px-8">
<Reveal> <Reveal>
<div className="w-mt-alu relative flex flex-col gap-5 px-6 py-5 sm:flex-row sm:items-center sm:justify-between"> <div className="w-plastic relative flex flex-col gap-5 px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0"> <div className="min-w-0">
<p className="w-mono text-[0.72rem] tracking-[0.2em]">DVORAKMORTONXD.LOCAL</p> <p className="w-display text-[0.8rem] tracking-[0.16em] text-[var(--plastic-ink)]">
<p className="w-mono mt-1.5 text-[0.62rem] opacity-80"> DVORAKMORTONXD.LOCAL
v3.3.0 · GPL-3.0-only · built with Next.js · textures CC0 via{" "} </p>
<p className="w-mono mt-1.5 text-[0.64rem] text-[var(--ink-soft)]">
v4.0.0 · GPL-3.0-only · built with Next.js · no textures, all CSS ·{" "}
<a className="w-link" href="/credits"> <a className="w-link" href="/credits">
credits credits
</a> </a>
@ -514,15 +551,15 @@ export default function WebSite() {
<button <button
type="button" type="button"
onClick={() => setTheme("skeuo")} onClick={() => setTheme("skeuo")}
className="w-keybtn desktop-toggle shrink-0 self-start sm:self-auto" className="w-keybtn w-btn-accent desktop-toggle shrink-0 self-start sm:self-auto"
title="Flip to Desktop mode" title="Flip to Desktop mode"
> >
<Led kind="amber" /> desktop mode <Led kind="on" /> desktop mode
</button> </button>
<i className="w-screw w-screw--alu w-corner-tr" aria-hidden="true" /> <i className="w-screw w-corner-tr" aria-hidden="true" />
<i className="w-screw w-screw--alu w-corner-bl" aria-hidden="true" /> <i className="w-screw w-corner-bl" aria-hidden="true" />
</div> </div>
<p className="w-mono mt-4 text-[0.62rem] text-[var(--ftext-faint)]"> <p className="w-mono mt-4 text-[0.64rem] text-[var(--ink-soft)]">
© 2025–2026 Martin Dvořák — follow the licence or don&rsquo;t, I don&rsquo;t mind © 2025–2026 Martin Dvořák — follow the licence or don&rsquo;t, I don&rsquo;t mind
</p> </p>
</Reveal> </Reveal>

View file

@ -79,22 +79,34 @@ export function Led({
); );
} }
export function Screws({ count = 2, alu = false, className = "" }: { count?: number; alu?: boolean; className?: string }) { export function Screws({ count = 2, className = "" }: { count?: number; className?: string }) {
return ( return (
<span className={`w-unit__screws ${className}`}> <span className={`w-unit__screws ${className}`}>
{Array.from({ length: count }).map((_, i) => ( {Array.from({ length: count }).map((_, i) => (
<i key={i} className={`w-screw${alu ? " w-screw--alu" : ""}`} /> <i key={i} className="w-screw" />
))} ))}
</span> </span>
); );
} }
/** Section heading: an etched aluminium tab on the desk + painted title. */ /** Section heading: a coloured chip on the desk + a painted title. */
export function SectionHead({ index, label, title }: { index: string; label: string; title: string }) { export function SectionHead({ index, label, title }: { index: string; label: string; title: string }) {
const tones = [
"var(--orange)",
"var(--teal)",
"var(--blue)",
"var(--lilac)",
"var(--mint)",
"var(--coral)",
"var(--yellow)",
];
const n = Number.parseInt(index, 10);
const tone = tones[Number.isFinite(n) ? (n - 1) % tones.length : 0];
return ( return (
<div className="w-sechead"> <div className="w-sechead">
<span className="w-mt-alu w-sechead__tab"> <span className="w-chip w-sechead__tab" style={{ "--tone": tone } as CSSProperties}>
<b>{index}</b> {label} {index} · {label}
</span> </span>
<h2 className="w-display w-paint w-sechead__title">{title}</h2> <h2 className="w-display w-paint w-sechead__title">{title}</h2>
</div> </div>
@ -107,9 +119,9 @@ export function Monitor({ children, className = "" }: { children: ReactNode; cla
return ( return (
<div className={`w-monitor ${className}`}> <div className={`w-monitor ${className}`}>
<div className="w-monitor__frame"> <div className="w-monitor__frame">
<div className="w-mt-glass w-monitor__glass">{children}</div> <div className="w-glass w-monitor__glass">{children}</div>
<div className="w-monitor__chin"> <div className="w-monitor__chin">
<Led kind="amber" /> <Led kind="on" />
<span className="w-monitor__brand">DXD</span> <span className="w-monitor__brand">DXD</span>
<span className="w-monitor__osd" aria-hidden="true"> <span className="w-monitor__osd" aria-hidden="true">
<i className="w-osdkey" /> <i className="w-osdkey" />
@ -127,6 +139,7 @@ export function Monitor({ children, className = "" }: { children: ReactNode; cla
} }
const KEY_ROWS = ["1234567890-=".split(""), "qwertyuiop[]".split(""), "asdfghjkl;'".split("")]; const KEY_ROWS = ["1234567890-=".split(""), "qwertyuiop[]".split(""), "asdfghjkl;'".split("")];
const CAP_TONES = ["var(--mint)", "var(--blue)", "var(--yellow)", "var(--coral)", "var(--lilac)", "var(--teal)"];
export function Keyboard() { export function Keyboard() {
return ( return (
@ -134,16 +147,20 @@ export function Keyboard() {
<div className="w-keyboard__deck"> <div className="w-keyboard__deck">
<div className="w-keyboard__row"> <div className="w-keyboard__row">
<span className="w-key w-key--accent">esc</span> <span className="w-key w-key--accent">esc</span>
{KEY_ROWS[0].map((k) => ( {KEY_ROWS[0].map((k, i) => (
<span key={k} className="w-key"> <span key={k} className="w-key" style={{ "--tone": CAP_TONES[i % CAP_TONES.length] } as CSSProperties}>
{k} {k}
</span> </span>
))} ))}
</div> </div>
{KEY_ROWS.slice(1).map((row, i) => ( {KEY_ROWS.slice(1).map((row, r) => (
<div key={i} className="w-keyboard__row"> <div key={r} className="w-keyboard__row">
{row.map((k) => ( {row.map((k, i) => (
<span key={k} className="w-key"> <span
key={k}
className="w-key"
style={{ "--tone": CAP_TONES[(i + r + 2) % CAP_TONES.length] } as CSSProperties}
>
{k} {k}
</span> </span>
))} ))}
@ -158,24 +175,27 @@ export function Keyboard() {
); );
} }
const RACK_TONES = ["var(--mint)", "var(--blue)", "var(--yellow)", "var(--coral)"];
/** A small rack that sits on the desk in the hero. */ /** A small rack that sits on the desk in the hero. */
export function MiniRack({ label = "DXD-01" }: { label?: string }) { export function MiniRack({ label = "DXD-01" }: { label?: string }) {
return ( return (
<div className="w-rack w-mt-metal" aria-hidden="true"> <div className="w-chassis w-rack" aria-hidden="true">
<div className="w-rack__top"> <div className="w-rack__top">
<Led kind="on" pulse /> <Led kind="on" pulse />
<span className="w-engrave text-[0.62rem] tracking-[0.2em]">{label}</span> <span className="w-display text-[0.68rem] tracking-[0.2em] text-[var(--chassis-ink)]">{label}</span>
<span className="w-rack__vent w-vents" /> <span className="w-rack__vent w-vents" />
</div> </div>
<div className="w-rack__grid"> <div className="w-rack__grid">
<span className="w-rails" /> <span className="w-rails" />
<div> <div>
{[0, 1, 2, 3].map((i) => ( {[0, 1, 2, 3].map((i) => (
<div key={i} className="w-unit !min-h-[46px] !gap-[10px] !py-2"> <div key={i} className="w-unit !min-h-[48px] !gap-[10px] !py-2" style={{ "--tone": RACK_TONES[i] } as CSSProperties}>
<span className="w-unit__grille !h-6 !w-9" /> <span className="w-unit__grip !h-7 !w-3" />
<span className="w-unit__grille !h-7 !w-10" />
<Led kind="on" pulse style={{ animationDelay: `${i * 0.5}s` }} /> <Led kind="on" pulse style={{ animationDelay: `${i * 0.5}s` }} />
<Led kind="amber" pulse slow style={{ animationDelay: `${i * 0.3}s` }} /> <Led kind="amber" pulse slow style={{ animationDelay: `${i * 0.3}s` }} />
<span className="ml-auto w-engrave text-[0.58rem] tracking-[0.14em]"> <span className="w-display ml-auto text-[0.6rem] tracking-[0.14em] text-[var(--chassis-dim)]">
{["pxe", "aio", "nas", "web"][i]} {["pxe", "aio", "nas", "web"][i]}
</span> </span>
</div> </div>
@ -187,13 +207,13 @@ export function MiniRack({ label = "DXD-01" }: { label?: string }) {
); );
} }
/** A machined aluminium plate with one number etched into it. */ /** A big coloured sticker with one number on it. */
export function Plaque({ value, label }: { value: string; label: string }) { export function Plaque({ value, label, tone = "var(--orange)" }: { value: string; label: string; tone?: string }) {
return ( return (
<div className="w-mt-alu w-plaque"> <div className="w-sticker w-plaque" style={{ "--tone": tone } as CSSProperties}>
<span className="w-plaque__value">{value}</span> <span className="w-plaque__value">{value}</span>
<span className="w-plaque__label">{label}</span> <span className="w-plaque__label">{label}</span>
<Led kind="amber" className="w-plaque__dot" /> <i className="w-plaque__dot" aria-hidden="true" />
</div> </div>
); );
} }
@ -209,6 +229,7 @@ export function ServiceUnit({
access, access,
checkUrl, checkUrl,
disabled, disabled,
tone = "var(--teal)",
}: { }: {
name: string; name: string;
desc: string; desc: string;
@ -216,6 +237,7 @@ export function ServiceUnit({
access?: ServiceAccess; access?: ServiceAccess;
checkUrl?: string; checkUrl?: string;
disabled?: boolean; disabled?: boolean;
tone?: string;
}) { }) {
const [status, setStatus] = useState<"pending" | "up" | "down">("pending"); const [status, setStatus] = useState<"pending" | "up" | "down">("pending");
@ -237,6 +259,7 @@ export function ServiceUnit({
const live = disabled ? "up" : status; const live = disabled ? "up" : status;
const kind = live === "up" ? "on" : live === "down" ? "off" : "amber"; const kind = live === "up" ? "on" : live === "down" ? "off" : "amber";
const stateClass = disabled ? "is-wait" : live === "up" ? "is-on" : live === "down" ? "is-off" : "is-wait";
const label = disabled ? "STANDBY" : live === "up" ? "ONLINE" : live === "down" ? "OFFLINE" : "POLLING"; const label = disabled ? "STANDBY" : live === "up" ? "ONLINE" : live === "down" ? "OFFLINE" : "POLLING";
const accessLabel = access === "public" ? "public" : access === "private" ? "private" : access === "on_request" ? "on request" : null; const accessLabel = access === "public" ? "public" : access === "private" ? "private" : access === "on_request" ? "on request" : null;
@ -253,8 +276,8 @@ export function ServiceUnit({
<span className="w-unit__grip" aria-hidden="true" /> <span className="w-unit__grip" aria-hidden="true" />
<span className="w-unit__grille w-grille" aria-hidden="true" /> <span className="w-unit__grille w-grille" aria-hidden="true" />
<span className="w-unit__id"> <span className="w-unit__id">
<span className="w-unit__name w-engrave">{name}</span> <span className="w-unit__name">{name}</span>
<span className="w-tape mt-1"> <span className="w-tape mt-1.5">
{host} {host}
{accessLabel ? ` · ${accessLabel}` : ""} {accessLabel ? ` · ${accessLabel}` : ""}
</span> </span>
@ -262,7 +285,7 @@ export function ServiceUnit({
</span> </span>
<span className="w-unit__state"> <span className="w-unit__state">
<Led kind={kind} pulse={live === "up"} /> <Led kind={kind} pulse={live === "up"} />
<span className="w-unit__status w-engrave">{label}</span> <span className={`w-unit__status ${stateClass}`}>{label}</span>
</span> </span>
<Screws /> <Screws />
</> </>
@ -270,13 +293,20 @@ export function ServiceUnit({
if (disabled) { if (disabled) {
return ( return (
<span className="w-unit" aria-disabled="true"> <span className="w-unit" style={{ "--tone": tone } as CSSProperties} aria-disabled="true">
{inner} {inner}
</span> </span>
); );
} }
return ( return (
<a href={url} target="_blank" rel="noopener noreferrer" className="w-unit w-unit--link" title={desc}> <a
href={url}
target="_blank"
rel="noopener noreferrer"
className="w-unit w-unit--link"
style={{ "--tone": tone } as CSSProperties}
title={desc}
>
{inner} {inner}
</a> </a>
); );
@ -295,7 +325,7 @@ interface Activity {
repo?: { full_name?: string }; repo?: { full_name?: string };
} }
const DOT_OPACITY = [0.07, 0.3, 0.48, 0.64, 0.82]; const DOT_OPACITY = [0.12, 0.35, 0.55, 0.75, 1];
export function Report() { export function Report() {
const [weeks, setWeeks] = useState<{ date: string; contribs: number; level: number }[][]>([]); const [weeks, setWeeks] = useState<{ date: string; contribs: number; level: number }[][]>([]);
@ -377,7 +407,7 @@ export function Report() {
return ( return (
<div className="w-printout"> <div className="w-printout">
<div className="w-mt-paper w-printout__paper"> <div className="w-printout__paper">
<p className="w-print__head"> <p className="w-print__head">
<span>GITDXD · ACTIVITY REPORT · 12 MONTHS</span> <span>GITDXD · ACTIVITY REPORT · 12 MONTHS</span>
<span className="ml-auto flex items-center gap-3"> <span className="ml-auto flex items-center gap-3">
@ -387,20 +417,20 @@ export function Report() {
</p> </p>
{state === "error" ? ( {state === "error" ? (
<p className="w-mono mt-5 text-[0.72rem] text-[#4a4133]">could not reach the forge.</p> <p className="w-mono mt-5 text-[0.74rem] text-[var(--ink-soft)]">could not reach the forge.</p>
) : ( ) : (
<> <>
<div className="mt-5 flex flex-wrap items-end gap-x-6 gap-y-2"> <div className="mt-5 flex flex-wrap items-end gap-x-6 gap-y-2">
<div> <div>
<p className="w-mono text-[0.62rem] tracking-[0.16em] text-[#4a4133]">TOTAL / 12 MONTHS</p> <p className="w-kicker">total / 12 months</p>
<p className="w-display text-[2.6rem] leading-none text-[var(--ink)]"> <p className="w-display mt-1 text-[2.6rem] leading-none text-[var(--ink)]">
{state === "loading" ? "—" : total} {state === "loading" ? "—" : total}
</p> </p>
</div> </div>
<p className="w-mono text-[0.62rem] leading-relaxed text-[#6f6450]"> <p className="w-mono text-[0.64rem] leading-relaxed text-[var(--ink-soft)]">
each dot = one day each square = one day
<br /> <br />
darkest = busiest strongest orange = busiest
</p> </p>
</div> </div>

View file

@ -4,34 +4,32 @@ import "../web.css";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "credits · dvorakmortonxd.dev", title: "credits · dvorakmortonxd.dev",
description: "Textures, type and other borrowed work used on dvorakmortonxd.dev.", description: "Type and other borrowed work used on dvorakmortonxd.dev.",
}; };
const TEXTURES = [
{ file: "desk.jpg", asset: "Wood 028", id: "Wood028", use: "the near-black desk" },
{ file: "metal.jpg", asset: "Metal 051 C", id: "Metal051C", use: "rack, monitor, panel hardware" },
{ file: "paper.jpg", asset: "Paper 003", id: "Paper003", use: "sheets, labels, tape, the report" },
{ file: "carbon.jpg", asset: "Fabric 004", id: "Fabric004", use: "the keyboard deck" },
{ file: "plastic.jpg", asset: "Plastic 012 A", id: "Plastic012A", use: "the drawer interior" },
];
const FONTS = [ const FONTS = [
{
name: "Nunito",
by: "Vernon Adams, Cyreal, Jacques Le Bailly",
use: "hardware labels, headings, buttons",
href: "https://fonts.google.com/specimen/Nunito",
},
{ {
name: "Newsreader", name: "Newsreader",
by: "Production Type", by: "Production Type",
use: "printed headlines, sheet copy, stamped numbers", use: "anything printed on paper",
href: "https://fonts.google.com/specimen/Newsreader", href: "https://fonts.google.com/specimen/Newsreader",
}, },
{ {
name: "IBM Plex Sans", name: "IBM Plex Sans",
by: "IBM", by: "IBM",
use: "body text on the desk", use: "body copy",
href: "https://fonts.google.com/specimen/IBM+Plex+Sans", href: "https://fonts.google.com/specimen/IBM+Plex+Sans",
}, },
{ {
name: "IBM Plex Mono", name: "IBM Plex Mono",
by: "IBM", by: "IBM",
use: "engraved labels, terminal text, index cards", use: "terminal text, receipt, technical labels",
href: "https://fonts.google.com/specimen/IBM+Plex+Mono", href: "https://fonts.google.com/specimen/IBM+Plex+Mono",
}, },
]; ];
@ -41,70 +39,34 @@ export default function Credits() {
<div className="w-page"> <div className="w-page">
<main className="mx-auto w-full max-w-[1120px] px-5 pt-24 pb-20 sm:px-8"> <main className="mx-auto w-full max-w-[1120px] px-5 pt-24 pb-20 sm:px-8">
<div className="w-sechead"> <div className="w-sechead">
<span className="w-mt-alu w-sechead__tab">credits</span> <span className="w-chip w-sechead__tab" style={{ "--tone": "var(--mint)" } as React.CSSProperties}>
credits
</span>
<h1 className="w-display w-paint w-sechead__title">Everything borrowed, credited.</h1> <h1 className="w-display w-paint w-sechead__title">Everything borrowed, credited.</h1>
</div> </div>
<p className="w-prose w-paint max-w-[64ch] text-[var(--ftext-dim)]"> <p className="w-prose w-paint max-w-[62ch] text-[var(--ink-soft)]">
The desk is built out of five CC0 textures and three open-licensed typefaces. Nothing is scraped The desk contains <strong>no images at all</strong> — every surface, screw, LED, paper sheet and coffee
or unlicensed — the authors are named even where the licence does not require it. mug is drawn in CSS. That means there are no photography or texture licences to track: the only
borrowed work is type.
</p> </p>
<section className="mt-14">
<div className="w-sechead !mb-6">
<span className="w-mt-alu w-sechead__tab">textures</span>
<h2 className="w-display w-paint w-sechead__title !text-[1.3rem]">ambientCG — CC0 1.0</h2>
</div>
<p className="w-prose w-paint max-w-[64ch] text-[var(--ftext-dim)]">
All five materials come from{" "}
<a className="w-link" href="https://ambientcg.com" target="_blank" rel="noopener noreferrer">
ambientCG
</a>{" "}
(Struffel Productions) under the{" "}
<a
className="w-link"
href="https://creativecommons.org/publicdomain/zero/1.0/"
target="_blank"
rel="noopener noreferrer"
>
CC0 1.0
</a>{" "}
public-domain dedication. I downloaded the 1K JPG colour maps, downscaled them to 512&nbsp;px and
tint them in CSS.
</p>
<ul className="mt-7 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
{TEXTURES.map((t) => (
<li key={t.id} className="w-mt-paper w-cardindex">
<p className="w-serif text-[1.05rem] text-[var(--ink)]">{t.asset}</p>
<p className="w-mono mt-2 text-[0.64rem] leading-relaxed text-[var(--ink-soft)]">
source:{" "}
<a
className="w-link"
href={`https://ambientcg.com/view?id=${t.id}`}
target="_blank"
rel="noopener noreferrer"
>
ambientCG / {t.id}
</a>
<br />
licence: CC0 1.0
<br />
used as <code>{`/textures/${t.file}`}</code> — {t.use}
</p>
</li>
))}
</ul>
</section>
<section className="mt-16"> <section className="mt-16">
<div className="w-sechead !mb-6"> <div className="w-sechead !mb-6">
<span className="w-mt-alu w-sechead__tab">type</span> <span className="w-chip w-sechead__tab" style={{ "--tone": "var(--teal)" } as React.CSSProperties}>
<h2 className="w-display w-paint w-sechead__title !text-[1.3rem]">Google Fonts — SIL OFL 1.1</h2> type
</span>
<h2 className="w-display w-paint w-sechead__title !text-[1.3rem]">
Google Fonts — SIL Open Font License 1.1
</h2>
</div> </div>
<ul className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"> <ul className="grid gap-5 sm:grid-cols-2 xl:grid-cols-4">
{FONTS.map((f) => ( {FONTS.map((f, i) => (
<li key={f.name} className="w-mt-paper w-cardindex w-cardindex--alt"> <li
<p className="w-serif text-[1.05rem] text-[var(--ink)]">{f.name}</p> key={f.name}
className="w-paper w-cardindex"
style={{ "--tone": ["var(--orange)", "var(--blue)", "var(--lilac)", "var(--mint)"][i % 4] } as React.CSSProperties}
>
<p className="w-display text-[1.05rem] text-[var(--ink)]">{f.name}</p>
<p className="w-mono mt-2 text-[0.64rem] leading-relaxed text-[var(--ink-soft)]"> <p className="w-mono mt-2 text-[0.64rem] leading-relaxed text-[var(--ink-soft)]">
by {f.by} by {f.by}
<br /> <br />
@ -122,13 +84,15 @@ export default function Credits() {
<section className="mt-16"> <section className="mt-16">
<div className="w-sechead !mb-6"> <div className="w-sechead !mb-6">
<span className="w-mt-alu w-sechead__tab">notes</span> <span className="w-chip w-sechead__tab" style={{ "--tone": "var(--lilac)" } as React.CSSProperties}>
notes
</span>
<h2 className="w-display w-paint w-sechead__title !text-[1.3rem]">Other marks</h2> <h2 className="w-display w-paint w-sechead__title !text-[1.3rem]">Other marks</h2>
</div> </div>
<p className="w-prose w-paint max-w-[64ch] text-[var(--ftext-dim)]"> <p className="w-prose w-paint max-w-[62ch] text-[var(--ink-soft)]">
The social ports use brand marks that remain the property of their owners and are used only to The patch panel uses brand marks that remain the property of their owners and are used only to
link to those accounts. Everything written on the paper is mine. The desktop mode (the KDE link to those accounts. Everything written on the paper — and every pixel of the desk itself — is
simulation) uses its own separate assets. mine. The desktop mode (the KDE simulation) uses its own separate assets.
</p> </p>
</section> </section>

View file

@ -37,15 +37,20 @@
--ease-cinematic: cubic-bezier(0.22, 1, 0.36, 1); --ease-cinematic: cubic-bezier(0.22, 1, 0.36, 1);
} }
/* Beats page.css's :root when the editorial theme is active. */ /* Beats page.css's :root when the web theme is active. The desk is a light,
colourful, cartoon-skeuomorphic surface — the dark gruvbox values in :root
are only the fallback for the desktop mode. */
html[data-theme="web"] { html[data-theme="web"] {
color-scheme: dark; color-scheme: light;
--accent: #fe8019; --bg: #f7e7c6;
--accent2: #d65d0e; --bg-raised: #fffdf6;
--accent3: #fabd2f; --bg-well: #eed9b0;
--green: #b8bb26; --accent: #f4823a;
--red: #fb4934; --accent2: #d4611c;
--gray: #a89984; --accent3: #f2c14e;
--green: #58bd72;
--red: #ea6f6a;
--gray: #7f7768;
} }
@theme inline { @theme inline {
@ -71,7 +76,7 @@ html[data-theme="web"] {
} }
html[data-theme="web"] ::selection { html[data-theme="web"] ::selection {
background: rgba(254, 128, 25, 0.28); background: rgba(244, 130, 58, 0.3);
color: var(--ink); color: var(--ink);
} }
@ -88,12 +93,12 @@ html[data-theme="web"] ::-webkit-scrollbar-track {
background: var(--bg-well); background: var(--bg-well);
} }
html[data-theme="web"] ::-webkit-scrollbar-thumb { html[data-theme="web"] ::-webkit-scrollbar-thumb {
background: #3c3836; background: #d9c49b;
border: 3px solid var(--bg-well); border: 3px solid var(--bg-well);
border-radius: 99px; border-radius: 99px;
} }
html[data-theme="web"] ::-webkit-scrollbar-thumb:hover { html[data-theme="web"] ::-webkit-scrollbar-thumb:hover {
background: #504945; background: #c9b184;
} }
/* Phones never get the desktop-mode button. */ /* Phones never get the desktop-mode button. */

View file

@ -1,10 +1,10 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Newsreader, IBM_Plex_Sans, IBM_Plex_Mono } from "next/font/google"; import { Newsreader, IBM_Plex_Sans, IBM_Plex_Mono, Nunito } from "next/font/google";
import "./globals.css"; import "./globals.css";
import ThemeScript from "./components/ThemeScript"; import ThemeScript from "./components/ThemeScript";
// Display serif for headlines, Plex for body/labels — same pairing as the // The web desk pairs a rounded sans (hardware labels, headings) with a printed
// reference editorial site. // serif (anything on paper) and a mono (terminal text + technical labels).
const news = Newsreader({ const news = Newsreader({
variable: "--font-news", variable: "--font-news",
subsets: ["latin"], subsets: ["latin"],
@ -24,6 +24,12 @@ const plexMono = IBM_Plex_Mono({
weight: ["400", "500"], weight: ["400", "500"],
display: "swap", display: "swap",
}); });
const round = Nunito({
variable: "--font-round",
subsets: ["latin"],
weight: ["600", "700", "800"],
display: "swap",
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "dvorakmortonxd.dev", title: "dvorakmortonxd.dev",
@ -51,7 +57,7 @@ export default function RootLayout({
lang="en" lang="en"
data-theme="web" data-theme="web"
suppressHydrationWarning suppressHydrationWarning
className={`${news.variable} ${plex.variable} ${plexMono.variable}`} className={`${news.variable} ${plex.variable} ${plexMono.variable} ${round.variable}`}
> >
<body className="antialiased"> <body className="antialiased">
<noscript> <noscript>

File diff suppressed because it is too large Load diff