diff --git a/Dockerfile b/Dockerfile index 907b28f..8574c23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ RUN npm ci --ignore-scripts --no-audit --no-fund COPY webpack.common.js webpack.config.prod.js ./ COPY index.html 404.html favicon.ico icon.png icon.svg robots.txt site.webmanifest ./ +COPY commissions ./commissions COPY css ./css COPY img ./img COPY js ./js diff --git a/README.md b/README.md index 400d8f8..371f743 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # FIW Labs Homepage -The source for [fiwlabs.dev](https://fiwlabs.dev), the personal portfolio of Alex (`Fi3w0`). It presents systems and desktop projects including TideWM, Scorium, Flux, Moonlit Shell, and the FIW Minecraft tooling collection. +The source for [fiwlabs.dev](https://fiwlabs.dev), the personal portfolio of Alex (`Fi3w0`). It presents selected systems and desktop projects including TideWM, Scorium, Flux, Moonlit Shell, Prismarine, FiwNode, Nereus, and the FIW Minecraft tooling collection. The site is intentionally framework-light: semantic HTML, hand-written CSS, and vanilla JavaScript are compiled into a static production bundle. There is no analytics, telemetry, backend, database, or runtime dependency on third-party fonts, icons, or scripts. @@ -80,6 +80,7 @@ Keep secrets out of the frontend and out of Docker build arguments. Everything d ```text css/ Site styles +commissions/ Commission services page deploy/nginx.conf Static server and security headers img/ Optimized project artwork js/app.js Interaction and visual effects @@ -91,3 +92,11 @@ webpack.*.js Development and production builds ## License The website source is available under the [MIT License](LICENSE.txt). Product names, screenshots, logos, and third-party marks retain their respective rights. + +## Keeping the portfolio current + +The homepage is a curated portfolio, not a release feed. Project descriptions focus on what each tool does; versions, download totals, supported Minecraft versions, and release roadmaps belong in the linked repositories or Modrinth pages. Scorium links to its organization so its implementations can grow without adding more homepage cards. + +Keep the four illustrated projects as the main showcase. Use the compact project rows for other substantial work, and replace entries rather than continually adding sections. Small utilities belong in the short workbench list; archived prototypes remain discoverable through the all-repositories link. + +The GitHub avatar is a local snapshot in `img/avatar.png`, with a cache version on its image URL. Replace it and increment that version when changing the profile picture. The browser needs no GitHub API access, tokens, or scheduled refreshes to display the portfolio. Routine project releases do not require a website update. diff --git a/commissions/index.html b/commissions/index.html new file mode 100644 index 0000000..99c53bd --- /dev/null +++ b/commissions/index.html @@ -0,0 +1,102 @@ + + + + + + Commissions - Fi3w0 / Alex + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
queue openremote / worldwide
+

alex@fiw:~ $ solve --whatever-this-is

+

Your system is weird.
Good.

+

Linux, servers, Minecraft, automation, and small websites. Bring me the setup snag, broken deployment, or idea with a clear finish line. I inspect the problem before quoting the work.

+ +

€10smallest starting point

6main rabbit holes

0need to know the jargon

+
+ +
+
FIW_SCOPE_MONITOR
+ +
01

Minecraft servercrashes on startup

TRACE
02

Docker stackworks only on Tuesdays

INSPECT
03

Linux laptopWi-Fi has left the chat

FIX
+

INPUT: symptoms + context + logs OUTPUT: tested result + handover

+
+
+ + + +
+

01 / WHAT I TOUCH

Choose your chaos.

Pick the closest box. The links below show related work I have built and documented.
+
+
01 / daily driver

Linux

Desktop installs, packages, dev environments, and custom setups. Boot, driver, and performance problems start with a diagnosis.

Related work: Moonlit Shell
ArchGentooUbuntuKDEWayland
+
02 / lives in a rack

Homelab + servers

Docker stacks, reverse proxies, HTTPS, DNS, and monitoring for small servers. Migrations and backups include an agreed recovery plan and restore checks.

Related work: Nereus
ComposeTraefikForgejoPortainer
+
03 / block infrastructure

Minecraft

Server setups, modpack configuration, crash diagnosis, and focused custom mods. Version updates and loader ports are quoted after inspecting the code and dependencies.

Related work: FIW Bosses
FabricForgeNeoForgePaper
+
04 / do it once

Automation

Scripts, CLI tools, small bots, APIs, and CI jobs that solve one defined problem. Inputs, integrations, and failure handling are part of the scope.

Related work: AquaTTY
BashPythonGoCI/CD
+
05 / ship the page

Small web

Personal pages, landing pages, and documentation sites, plus domains, HTTPS, Docker deployment, and build repairs. Content and page count are agreed up front.

Related work: HomePage
StaticNginxDeployDNS
+
06 / before checkout

Hardware

PC and homelab parts lists for your budget and workload. I check compatibility, upgrade options, and the limits of advice given remotely.

PC buildsMini PCsServers
+
+
+ +
+

02 / STARTING PRICES

A number before the email.

Quoted in EUR / USD approximateThese ranges cover small, defined jobs. You get a fixed quote after I inspect the setup; larger jobs are quoted individually. Hosting, domains, and paid services are separate.
+
+
+
01

Help + Linux

5 jobs

General troubleshooting

€10–25$12–30

Linux help / setup session

€15–30$18–35

Linux desktop customization

€20–45$25–55

Developer environment setup

€20–40$25–45

Hardware consultation / parts list

€10–25$12–30
+
02

Servers + infrastructure

8 jobs

Server / PC performance diagnosis

€15–35$18–40

Dockerize an existing app

€25–60$30–70

Deploy a self-hosted service

€25–60$30–70

Basic homelab / server setup

€40–90$45–105

Full Docker Compose stack

€50–100$60–115

Reverse proxy + domain + HTTPS

€25–55$30–65

Backup setup + restore check

€25–60$30–70

CI/CD setup

€30–70$35–80
+
03

Automation + tools

2 jobs

Small automation script

€15–50$18–60

Small Discord / utility bot

€30–80$35–95
+
04

Minecraft

8 jobs

Basic server setup

€20–40$25–45

Modded server setup

€35–75$40–90

Server crash diagnosis / targeted fix

€15–45$18–55

Performance review / targeted tuning

€20–50$25–60

Modpack setup / configuration

€25–60$30–70

Simple custom mod

€40–120$45–140

Small mod port / version update

€25–80$30–95

Permissions / proxy configuration

€20–50$25–60
+
05

Web + strange ideas

4 jobs

One-page website

€50–90$60–105

One-page website + deployment / domain

€70–120$80–140

Website / server deployment only

€20–40$25–45

Weird / custom project

Tell me the ideaquoted individually
+
+

27 starting points visible

+
+ +

03 / JOB SIZE

How much mess?

No mystery package names. Just three useful shapes for the work.
FIW-001QUICK FIX

One sharp problem.

One config issue, small script, setup snag, or diagnosis session. We agree on the time or task limit and the result you will receive.

often starts around €10–30
FIW-002SETUP / BUILD

Make the whole thing work.

One agreed server, Docker stack, site, modded setup, or automation. Delivered with checks and notes for running it yourself.

priced from the catalog above
FIW-003CUSTOM

The odd one.

Several systems, an unusual idea, or unfamiliar compatibility work. I check feasibility first and quote the parts I can deliver.

quoted after I understand it
+ +

04 / HOW IT WORKS

Four steps. No ceremony.

  1. 01

    Show me the thing

    Send the goal, symptoms, setup, versions, budget, deadline, and useful logs. Tell me what a successful result looks like.

    hello
  2. 02

    I trace the edges

    I inspect the setup and define the deliverable, access, dependencies, and checks. Troubleshooting may be scoped as diagnosis before a fix is agreed.

    scope
  3. 03

    You get a quote

    A price and plan before work begins, including revisions and any support period. Extra features or changed requirements get a separate quote first.

    agree
  4. 04

    I make it behave

    Build, check the agreed result, and hand over the files and setup notes. Follow-up fixes and ongoing maintenance follow the terms in your quote.

    ship

I use AI tools for parts of development. I own the design, review, testing, and integration, and agree on checks for your setup before delivery.

+ +

05 / PAYMENT

Pick the boring money pipe.

We choose after the quote. Three routes are live; Stripe joins them when its checkout is connected.
+ +

06 / YOUR TURN

Show me
the logs.

Or the screenshot. Or the half-finished idea. You do not need to know whether the villain is DNS, Java, Docker, Linux, permissions, or networking.

Emailalex@fiwlabs.dev→Telegram@fi3w0↗

+
+ + + + + + diff --git a/css/commissions.css b/css/commissions.css new file mode 100644 index 0000000..22a0088 --- /dev/null +++ b/css/commissions.css @@ -0,0 +1,902 @@ +:root { + --commission-pad: clamp(1.25rem, 4vw, 4.5rem); + --commission-width: 96rem; +} + +.commissions-page { + --mx: 72%; + --my: 22%; + background: #0b1014; +} + +.commissions-page::before, +.commissions-page .page-grid { + display: none; +} + +.commission-grid { + position: fixed; + z-index: -2; + inset: 0; + opacity: 0.34; + background-image: + linear-gradient(rgba(143, 212, 208, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(143, 212, 208, 0.035) 1px, transparent 1px); + background-size: 2rem 2rem; + pointer-events: none; + mask-image: radial-gradient(circle at var(--mx) var(--my), black, transparent 55%); +} + +.commission-shell { + width: min(100%, var(--commission-width)); + margin: 0 auto; + padding-right: var(--commission-pad); + padding-left: var(--commission-pad); +} + +.commission-nav-header { + border-bottom-color: rgba(143, 212, 208, 0.2); + background: rgba(11, 16, 20, 0.9); + backdrop-filter: blur(16px); +} + +.commission-nav-header .brand > span:last-child { + color: var(--cyan); +} + +.commission-nav-header .nav-pill { + color: var(--coral); +} + +.commission-hero-new { + --hx: 72%; + --hy: 24%; + position: relative; + display: grid; + width: min(100%, var(--commission-width)); + min-height: 100svh; + margin: 0 auto; + padding: clamp(8.5rem, 13vh, 10rem) var(--commission-pad) clamp(4rem, 8vh, 7rem); + grid-template-columns: minmax(0, 7fr) minmax(24rem, 5fr); + align-items: center; + gap: clamp(2.5rem, 6vw, 7rem); +} + +.commission-glow { + position: absolute; + z-index: -1; + inset: 0; + background: + radial-gradient(circle at var(--hx) var(--hy), rgba(143, 212, 208, 0.16), transparent 18rem), + radial-gradient(circle at 15% 82%, rgba(240, 153, 110, 0.1), transparent 24rem); + pointer-events: none; +} + +.commission-hero-copy { + min-width: 0; +} + +.commission-availability { + display: flex; + max-width: 46rem; + justify-content: space-between; + margin-bottom: 2rem; + padding-bottom: 0.65rem; + border-bottom: 1px dashed var(--line); + color: var(--muted); + font-size: 0.62rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.commission-availability span:first-child { + color: var(--lime); +} + +.commission-availability i { + display: inline-block; + width: 0.45rem; + height: 0.45rem; + margin-right: 0.45rem; + border-radius: 50%; + background: var(--lime); + box-shadow: 0 0 0 0 rgba(200, 238, 128, 0.4); + animation: commission-pulse 2s ease-out infinite; +} + +.commission-command { + margin-bottom: 1.3rem; + color: var(--cyan); + font-size: 0.72rem; + letter-spacing: 0.05em; +} + +.commission-hero-new h1 { + max-width: 9ch; + margin-bottom: 2rem; + font-size: clamp(4.4rem, 8.5vw, 9rem); + letter-spacing: -0.08em; + line-height: 0.82; +} + +.commission-word { + position: relative; + display: inline-block; + color: var(--coral); +} + +.commission-word.is-changing { + animation: word-swap 260ms steps(2, end); +} + +.commission-lede { + max-width: 48rem; + margin-bottom: 2.2rem; + color: var(--muted); + font-size: clamp(0.95rem, 1.35vw, 1.14rem); +} + +.commission-actions { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + margin-bottom: 3rem; +} + +.commission-actions a { + display: inline-flex; + min-height: 3rem; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + padding: 0.7rem 1rem; + border: 1px solid var(--line); + color: var(--text); + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + transition: background 160ms ease, color 160ms ease, transform 160ms ease; +} + +.commission-actions a:first-child { + border-color: var(--coral); + background: var(--coral); + color: var(--ink); +} + +.commission-actions a:hover { + transform: translateY(-0.25rem); + background: var(--lime); + color: var(--ink); +} + +.commission-facts { + display: grid; + max-width: 46rem; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); + grid-template-columns: repeat(3, 1fr); +} + +.commission-facts p { + display: grid; + min-height: 5.5rem; + margin: 0; + padding: 0.75rem; + align-content: center; +} + +.commission-facts p + p { + border-left: 1px solid var(--line); +} + +.commission-facts strong, +.commission-facts span { + display: block; +} + +.commission-facts strong { + color: var(--coral); + font-family: var(--display); + font-size: 1.75rem; + line-height: 1; +} + +.commission-facts span { + margin-top: 0.35rem; + color: var(--muted); + font-size: 0.54rem; + text-transform: uppercase; +} + +.scope-console { + position: relative; + min-width: 0; + border: 2px solid rgba(143, 212, 208, 0.4); + background: rgba(13, 22, 28, 0.9); + box-shadow: 1rem 1rem 0 rgba(240, 153, 110, 0.75); +} + +.scope-console::before { + position: absolute; + z-index: -1; + inset: -1.4rem 1.4rem 1.4rem -1.4rem; + border: 1px dashed rgba(143, 212, 208, 0.28); + content: ""; +} + +.scope-console-top { + display: flex; + min-height: 3rem; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + border-bottom: 1px solid rgba(143, 212, 208, 0.25); + color: var(--cyan); + font-size: 0.58rem; + letter-spacing: 0.08em; +} + +.scope-clock { + color: var(--lime); +} + +.scope-radar { + position: relative; + display: grid; + width: min(72%, 19rem); + aspect-ratio: 1; + margin: 2rem auto; + overflow: hidden; + place-items: center; + border: 1px solid rgba(143, 212, 208, 0.3); + border-radius: 50%; + background: + linear-gradient(rgba(143, 212, 208, 0.16) 1px, transparent 1px), + linear-gradient(90deg, rgba(143, 212, 208, 0.16) 1px, transparent 1px), + radial-gradient(circle, rgba(143, 212, 208, 0.08), transparent 68%); + background-size: 2rem 2rem, 2rem 2rem, auto; +} + +.scope-radar::before, +.scope-radar::after { + position: absolute; + background: rgba(143, 212, 208, 0.28); + content: ""; +} + +.scope-radar::before { width: 100%; height: 1px; } +.scope-radar::after { width: 1px; height: 100%; } + +.radar-ring { + position: absolute; + border: 1px solid rgba(143, 212, 208, 0.28); + border-radius: 50%; +} + +.radar-ring-one { inset: 18%; } +.radar-ring-two { inset: 36%; } + +.radar-sweep { + position: absolute; + inset: 50% 50% 0 0; + transform-origin: 100% 0; + background: linear-gradient(35deg, transparent 45%, rgba(200, 238, 128, 0.45)); + animation: radar-spin 4.5s linear infinite; +} + +.radar-blip { + position: absolute; + width: 0.55rem; + height: 0.55rem; + border: 2px solid var(--coral); + border-radius: 50%; + box-shadow: 0 0 0.9rem var(--coral); + animation: radar-blip 1.8s ease-in-out infinite; +} + +.radar-blip-one { top: 23%; left: 62%; } +.radar-blip-two { top: 67%; left: 27%; animation-delay: 0.55s; } +.radar-blip-three { top: 72%; left: 74%; animation-delay: 1.1s; } + +.scope-radar strong { + position: relative; + z-index: 2; + padding: 0.45rem; + background: rgba(11, 16, 20, 0.75); + color: var(--lime); + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.08em; + line-height: 1.3; + text-align: center; +} + +.scope-queue { + border-top: 1px solid rgba(143, 212, 208, 0.25); +} + +.scope-queue > div { + display: grid; + min-height: 4.4rem; + padding: 0.7rem 1rem; + border-bottom: 1px dashed var(--line); + grid-template-columns: 2rem minmax(0, 1fr) auto; + align-items: center; + gap: 0.7rem; + transition: background 150ms ease, padding 150ms ease; +} + +.scope-queue > div:hover { + padding-left: 1.35rem; + background: rgba(143, 212, 208, 0.08); +} + +.scope-queue > div > span, +.scope-queue em { + color: var(--coral); + font-size: 0.55rem; + font-style: normal; +} + +.scope-queue p { margin: 0; } +.scope-queue b, +.scope-queue small { display: block; } +.scope-queue b { font-size: 0.72rem; font-weight: 500; } +.scope-queue small { color: var(--muted); font-size: 0.58rem; } + +.scope-console-foot { + display: flex; + min-height: 3rem; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin: 0; + color: var(--muted); + font-size: 0.5rem; + text-transform: uppercase; +} + +.scope-console-foot span { color: var(--cyan); } +.scope-console-foot i { width: 1.2rem; height: 1px; background: var(--line); } + +.commission-tape { + overflow: hidden; + border-top: 3px solid var(--ink); + border-bottom: 3px solid var(--ink); + background: var(--lime); + color: var(--ink); +} + +.commission-tape-track { + display: flex; + width: max-content; + min-height: 3.6rem; + align-items: center; + animation: commission-tape 24s linear infinite; +} + +.commission-tape-set { + display: flex; + min-width: 100vw; + flex-shrink: 0; + align-items: center; + justify-content: space-around; + gap: 1.3rem; + padding: 0 1.3rem; +} + +.commission-tape span { + font-family: var(--display); + font-size: 1rem; + letter-spacing: 0.08em; +} + +.commission-tape i { + width: 0.55rem; + height: 0.55rem; + transform: rotate(45deg); + background: var(--ink); +} + +.capability-section, +.job-section, +.payment-section-new { + padding-top: clamp(5rem, 9vw, 9rem); + padding-bottom: clamp(5rem, 9vw, 9rem); +} + +.commission-section-head { + display: grid; + margin-bottom: clamp(3rem, 6vw, 5rem); + padding-top: 1.2rem; + border-top: 4px solid var(--text); + grid-template-columns: 2fr 7fr 3fr; + gap: 1.5rem; + align-items: start; +} + +.commission-section-head p, +.price-head p, +.process-head p, +.commission-cta > .commission-shell > p { + color: var(--coral); + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.09em; +} + +.commission-section-head h2, +.price-head h2, +.process-head h2 { + margin: 0; + font-size: clamp(3.2rem, 6.8vw, 7rem); + letter-spacing: -0.07em; + line-height: 0.88; +} + +.commission-section-head > span { + color: var(--muted); + font-size: 0.78rem; +} + +.capability-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(12, minmax(0, 1fr)); +} + +.capability-card { + position: relative; + display: flex; + min-height: 27rem; + flex-direction: column; + padding: clamp(1.4rem, 2.5vw, 2.3rem); + overflow: hidden; + border: 2px solid var(--line); + background: rgba(17, 25, 31, 0.86); + transition: transform 180ms ease, border-color 180ms ease; +} + +.capability-card:hover { + transform: translateY(-0.4rem); + border-color: currentColor; +} + +.capability-card > span { + font-size: 0.58rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.capability-card h3 { + margin: auto 0 1.5rem; + font-size: clamp(2.2rem, 4.5vw, 5rem); + letter-spacing: -0.07em; +} + +.capability-card p { + max-width: 42rem; + color: var(--muted); + font-size: 0.78rem; +} + +.capability-card > div { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 1rem; +} + +.capability-card b { + padding: 0.2rem 0.5rem; + border: 1px solid currentColor; + font-size: 0.54rem; + font-weight: 500; +} + +.capability-linux { grid-column: span 4; color: var(--coral); } +.capability-server { grid-column: span 8; color: var(--cyan); } +.capability-minecraft { grid-column: span 7; color: var(--lime); } +.capability-automation { grid-column: span 5; color: var(--coral); } +.capability-web { grid-column: span 5; color: var(--cyan); } +.capability-hardware { grid-column: span 7; color: var(--lime); } + +.capability-server, +.capability-minecraft, +.capability-hardware { + min-height: 31rem; +} + +.capability-card::after { + position: absolute; + right: -0.08em; + bottom: -0.18em; + color: currentColor; + content: "#"; + font-family: var(--display); + font-size: 13rem; + line-height: 0.8; + opacity: 0.035; + pointer-events: none; +} + +.price-section, +.process-section-new, +.commission-cta { + box-shadow: 0 0 0 100vmax currentColor; + clip-path: inset(0 -100vmax); +} + +.price-section { + padding: clamp(5rem, 9vw, 9rem) 0; + background: var(--paper); + box-shadow: 0 0 0 100vmax var(--paper); + color: var(--ink); +} + +.price-head { + display: grid; + margin-bottom: 3rem; + grid-template-columns: minmax(0, 8fr) minmax(18rem, 4fr); + gap: 3rem; + align-items: end; +} + +.price-head h2 { max-width: 10ch; } + +.price-head-note { + padding: 1rem; + border: 2px solid var(--ink); + background: var(--yellow); + transform: rotate(1deg); +} + +.price-head-note strong, +.price-head-note span { display: block; } +.price-head-note strong { margin-bottom: 0.5rem; font-size: 0.68rem; text-transform: uppercase; } +.price-head-note span { color: #3f4848; font-size: 0.7rem; } + +.price-filter { + display: flex; + flex-wrap: wrap; + margin-bottom: 2.5rem; + border-top: 4px solid var(--ink); + border-bottom: 2px solid var(--ink); +} + +.price-filter button { + flex: 1; + min-width: max-content; + padding: 0.9rem 0.8rem; + border: 0; + border-right: 1px solid var(--ink); + background: transparent; + color: var(--ink); + cursor: pointer; + font-size: 0.62rem; + font-weight: 600; + text-transform: uppercase; + transition: background 150ms ease, color 150ms ease; +} + +.price-filter button:last-child { border-right: 0; } +.price-filter button:hover, +.price-filter button.is-active { background: var(--ink); color: var(--paper); } +.price-filter button span { margin-left: 0.35rem; color: #a74b2c; } +.price-filter button.is-active span { color: var(--coral); } + +.price-group { + display: grid; + border-top: 2px solid var(--ink); + grid-template-columns: minmax(14rem, 4fr) minmax(0, 8fr); +} + +.price-group[hidden] { display: none; } + +.price-group-label { + position: sticky; + top: 5.5rem; + align-self: start; + padding: 1.5rem 2rem 1.5rem 0; +} + +.price-group-label span, +.price-group-label small { color: #a74b2c; font-size: 0.6rem; text-transform: uppercase; } +.price-group-label h3 { max-width: 9ch; margin: 2rem 0 1rem; font-size: clamp(2rem, 4vw, 4rem); letter-spacing: -0.06em; } + +.price-list { border-left: 2px solid var(--ink); } + +.price-list > div { + display: grid; + min-height: 5.4rem; + padding: 0.9rem 0 0.9rem 1.5rem; + border-bottom: 1px solid rgba(17, 23, 26, 0.28); + grid-template-columns: minmax(0, 1fr) 8rem 5rem; + align-items: center; + gap: 1rem; + transition: padding 150ms ease, background 150ms ease; +} + +.price-list > div:hover { + padding-right: 0.8rem; + padding-left: 2rem; + background: var(--lime); +} + +.price-list p { margin: 0; font-size: 0.8rem; } +.price-list strong { font-family: var(--display); font-size: 1.45rem; letter-spacing: -0.04em; text-align: right; } +.price-list small { color: #5c6665; font-size: 0.62rem; text-align: right; } +.price-custom { background: var(--coral); } +.price-custom strong { font-family: var(--mono); font-size: 0.7rem; text-transform: uppercase; } + +.price-count { + margin: 1.5rem 0 0; + color: #5c6665; + font-size: 0.65rem; + text-align: right; + text-transform: uppercase; +} +.price-count span { color: #a74b2c; font-weight: 700; } + +.job-tickets { + display: grid; + gap: 1rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.job-ticket { + display: flex; + min-height: 28rem; + flex-direction: column; + padding: 1.5rem; + border: 2px dashed var(--line); + background: rgba(17, 25, 31, 0.72); + transition: transform 180ms ease; +} +.job-ticket:hover { transform: rotate(-0.6deg) translateY(-0.3rem); } +.job-ticket-main { border: 0; background: var(--lime); color: var(--ink); transform: rotate(1deg); } +.job-ticket-main:hover { transform: rotate(0.3deg) translateY(-0.3rem); } +.job-ticket > div { display: flex; justify-content: space-between; font-size: 0.58rem; color: var(--coral); } +.job-ticket-main > div { color: #6c4b2f; } +.job-ticket h3 { margin: auto 0 1.3rem; font-size: clamp(2.2rem, 3.7vw, 4rem); letter-spacing: -0.065em; } +.job-ticket p { color: var(--muted); font-size: 0.78rem; } +.job-ticket-main p { color: #3f4848; } +.job-ticket small { margin-top: 1rem; padding-top: 0.7rem; border-top: 1px dashed currentColor; font-size: 0.58rem; text-transform: uppercase; } + +.process-section-new { + padding: clamp(5rem, 9vw, 9rem) 0; + background: var(--cyan); + box-shadow: 0 0 0 100vmax var(--cyan); + color: var(--ink); +} + +.process-head { + display: grid; + margin-bottom: 3.5rem; + padding-top: 1.2rem; + border-top: 4px solid var(--ink); + grid-template-columns: 2fr 10fr; + gap: 1.5rem; +} + +.process-head h2 { max-width: 9ch; } + +.process-steps { + margin: 0; + padding: 0; + border-top: 2px solid var(--ink); + border-bottom: 2px solid var(--ink); + list-style: none; +} + +.process-steps li { + display: grid; + min-height: 8rem; + padding: 1.2rem 0; + border-bottom: 1px solid rgba(17, 23, 26, 0.28); + grid-template-columns: 5rem minmax(0, 1fr) auto; + align-items: center; + gap: 1.5rem; + transition: padding 150ms ease, background 150ms ease; +} +.process-steps li:last-child { border-bottom: 0; } +.process-steps li:hover { padding-right: 1rem; padding-left: 1rem; background: rgba(233, 229, 218, 0.35); } +.process-steps > li > span { color: #a74b2c; font-size: 0.7rem; } +.process-steps h3 { margin: 0 0 0.35rem; font-family: var(--mono); font-size: clamp(1.1rem, 2vw, 1.65rem); } +.process-steps p { margin: 0; color: #3f4848; font-size: 0.72rem; } +.process-steps b { padding: 0.2rem 0.6rem; border: 1px solid var(--ink); font-size: 0.55rem; font-weight: 500; text-transform: uppercase; } + +.payment-rail { + display: grid; + border-top: 4px solid var(--text); + border-bottom: 2px solid var(--text); + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.payment-rail > * { + display: block; + min-height: 18rem; + padding: 1.3rem; + border-right: 1px solid var(--line); + transition: background 160ms ease, color 160ms ease; +} +.payment-rail > *:last-child { border-right: 0; } +.payment-rail > *:hover { background: var(--coral); color: var(--ink); } +.payment-rail span { color: var(--coral); font-size: 0.58rem; } +.payment-rail > *:hover span { color: var(--ink); } +.payment-rail h3 { margin: 6rem 0 0.8rem; font-size: clamp(1.8rem, 3vw, 3rem); letter-spacing: -0.055em; } +.payment-rail p { color: var(--muted); font-size: 0.67rem; text-transform: uppercase; } +.payment-rail > *:hover p { color: var(--ink); } + +.commission-cta { + padding: clamp(5rem, 9vw, 9rem) 0; + background: var(--coral); + box-shadow: 0 0 0 100vmax var(--coral); + color: var(--ink); +} + +.commission-cta h2 { + max-width: 8ch; + margin-bottom: 2rem; + font-size: clamp(5rem, 14vw, 13rem); + letter-spacing: -0.09em; + line-height: 0.72; +} + +.commission-cta-copy { + max-width: 48rem; + margin-bottom: 3rem; + font-size: clamp(0.9rem, 1.5vw, 1.1rem); +} + +.commission-contact-grid { + display: grid; + border-top: 4px solid var(--ink); + border-bottom: 2px solid var(--ink); + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.commission-contact-grid > * { + display: grid; + min-width: 0; + min-height: 7.5rem; + padding: 1rem; + border: 0; + border-right: 1px solid var(--ink); + background: transparent; + color: var(--ink); + cursor: pointer; + text-align: left; + grid-template-columns: 1fr auto; + transition: background 150ms ease, padding 150ms ease; +} +.commission-contact-grid > *:last-child { border-right: 0; } +.commission-contact-grid > *:hover { padding-left: 1.4rem; background: var(--yellow); } +.commission-contact-grid span { font-family: var(--display); font-size: clamp(1.4rem, 2.4vw, 2.3rem); letter-spacing: -0.04em; } +.commission-contact-grid small { align-self: end; color: #5b4235; font-size: 0.55rem; } +.commission-contact-grid b { font-size: 1.2rem; } + +.commission-footer { + display: grid; + width: min(100%, var(--commission-width)); + min-height: 7rem; + margin: 0 auto; + padding: 1.5rem var(--commission-pad); + grid-template-columns: 1fr auto auto; + align-items: center; + gap: 2rem; +} +.commission-footer p { margin: 0; color: var(--muted); font-size: 0.6rem; } + +@keyframes radar-spin { to { transform: rotate(360deg); } } +@keyframes radar-blip { 0%, 100% { opacity: 0.35; transform: scale(0.7); } 45% { opacity: 1; transform: scale(1.3); } } +@keyframes commission-tape { to { transform: translateX(-50%); } } +@keyframes commission-pulse { 70%, 100% { box-shadow: 0 0 0 0.5rem rgba(200, 238, 128, 0); } } +@keyframes word-swap { 0% { transform: translate(0); } 25% { transform: translate(-0.05em, 0.04em); text-shadow: 0.06em 0 var(--cyan); } 50% { transform: translate(0.05em, -0.03em); text-shadow: -0.06em 0 var(--lime); } 100% { transform: translate(0); } } + +@media (max-width: 74rem) { + .commission-hero-new { min-height: auto; grid-template-columns: 1fr; } + .scope-console { width: min(100%, 48rem); margin: 1rem auto 0; } + .capability-linux, + .capability-automation, + .capability-web { grid-column: span 6; } + .capability-server, + .capability-minecraft, + .capability-hardware { grid-column: span 6; min-height: 27rem; } + .commission-section-head { grid-template-columns: 2fr 7fr; } + .commission-section-head > span { grid-column: 2; } + .payment-rail { grid-template-columns: repeat(2, 1fr); } + .payment-rail > *:nth-child(2) { border-right: 0; } + .payment-rail > *:nth-child(-n + 2) { border-bottom: 1px solid var(--line); } + .commission-contact-grid { grid-template-columns: repeat(2, 1fr); } + .commission-contact-grid > *:nth-child(2) { border-right: 0; } + .commission-contact-grid > *:nth-child(-n + 2) { border-bottom: 1px solid var(--ink); } +} + +@media (max-width: 52rem) { + .commission-nav-header .brand > span:last-child { font-size: 0; } + .commission-nav-header .brand > span:last-child::before { content: "fiw / work"; font-size: 0.75rem; } + .commission-hero-new { padding-top: 7.8rem; } + .commission-hero-new h1 { font-size: clamp(3.8rem, 15vw, 6rem); } + .commission-section-head, + .price-head, + .process-head { grid-template-columns: 1fr; } + .commission-section-head > span { grid-column: 1; max-width: 36rem; } + .capability-grid { grid-template-columns: 1fr; } + .capability-linux, + .capability-server, + .capability-minecraft, + .capability-automation, + .capability-web, + .capability-hardware { min-height: 24rem; grid-column: auto; } + .price-group { grid-template-columns: 1fr; } + .price-group-label { position: static; padding-right: 0; } + .price-list { border-top: 2px solid var(--ink); border-left: 0; } + .job-tickets { grid-template-columns: 1fr; } + .job-ticket { min-height: 23rem; } + .job-ticket-main { transform: none; } + .process-head { gap: 0; } +} + +@media (max-width: 36rem) { + .commission-availability { display: grid; gap: 0.4rem; } + .commission-command { font-size: 0.62rem; } + .commission-actions { display: grid; } + .commission-actions a { width: 100%; } + .commission-facts { grid-template-columns: 1fr; } + .commission-facts p { min-height: 4.5rem; grid-template-columns: 4rem 1fr; align-items: center; } + .commission-facts p + p { border-top: 1px solid var(--line); border-left: 0; } + .commission-facts span { margin-top: 0; } + .scope-console { box-shadow: 0.55rem 0.55rem 0 rgba(240, 153, 110, 0.75); } + .scope-console::before { display: none; } + .scope-console-foot { display: none; } + .price-filter { display: grid; grid-template-columns: repeat(2, 1fr); } + .price-filter button { min-width: 0; border-right: 1px solid var(--ink); border-bottom: 1px solid var(--ink); } + .price-filter button:nth-child(even) { border-right: 0; } + .price-filter button:nth-last-child(-n + 2) { border-bottom: 0; } + .price-list > div { min-height: 6rem; padding-left: 0; grid-template-columns: minmax(0, 1fr) auto; gap: 0.4rem; } + .price-list > div:hover { padding-left: 0.5rem; } + .price-list small { grid-column: 2; } + .price-list strong { font-size: 1.2rem; } + .process-steps li { grid-template-columns: 2.2rem 1fr; gap: 0.7rem; } + .process-steps b { display: none; } + .payment-rail, + .commission-contact-grid { grid-template-columns: 1fr; } + .payment-rail > *, + .payment-rail > *:nth-child(2) { min-height: 13rem; border-right: 0; border-bottom: 1px solid var(--line); } + .payment-rail > *:last-child { border-bottom: 0; } + .payment-rail h3 { margin-top: 3rem; } + .commission-contact-grid > *, + .commission-contact-grid > *:nth-child(2) { border-right: 0; border-bottom: 1px solid var(--ink); } + .commission-contact-grid > *:last-child { border-bottom: 0; } + .commission-footer { grid-template-columns: 1fr auto; } + .commission-footer > p:first-of-type { display: none; } +} + +@media (prefers-reduced-motion: reduce) { + .commission-availability i, + .radar-sweep, + .radar-blip, + .commission-tape-track, + .commission-word { animation: none !important; } +} + +/* Proof links sit with each service instead of adding another portfolio section. */ +.capability-proof { + align-self: flex-start; + font-size: 0.68rem; + text-underline-offset: 0.25em; + text-decoration: underline; +} + +.capability-proof:hover { + color: var(--text); +} + +.commission-work-note { + max-width: 68ch; + margin: 2rem 0 0; + color: #3f4848; + font-size: 0.78rem; +} diff --git a/css/editorial.css b/css/editorial.css index fff54a6..7a40f85 100644 --- a/css/editorial.css +++ b/css/editorial.css @@ -2664,3 +2664,22 @@ article[data-repo="Moonlit-shell"] .project-feature-copy { display: none; } } + +/* Keep the selected tools understandable on phones without expanding cards. */ +@media (max-width: 52rem) { + .work-section .project-row { + row-gap: 0.65rem; + align-items: start; + } + + .work-section .project-row > p { + display: block; + grid-column: 2 / 3; + grid-row: 2; + } + + .work-section .project-row > a { + grid-column: 3; + grid-row: 1; + } +} diff --git a/img/avatar.png b/img/avatar.png index 9f3be0c..b14ae42 100644 Binary files a/img/avatar.png and b/img/avatar.png differ diff --git a/index.html b/index.html index 50c7106..0d97662 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ Fi3w0 / Alex - Systems Builder - + @@ -17,7 +17,7 @@ - + @@ -31,7 +31,7 @@ -

Every mod above (plus Warp, Graves, and Tickwatch from the archive) ships through one Modrinth profile — same JSON-config philosophy, same hot reload, across Fabric, Forge, and NeoForge.

- -
-
7mods published
-
3loaders supported
-
1creator profile
-
+

Bosses, custom items, graves, warps, and admin tools for Minecraft servers. Installation, supported loaders, and release details live on Modrinth.

-

03 / Repositories

Other things I made.

+

03 / Repositories

More from the workbench.

All repositories
- Fiw WarpKotlin / server teleports↗ - TickwatchCrash-safe player stats↗ - Firefox PrivacyCross-platform hardening↗ - HomePageLive homelab dashboard↗ - Fiw Story ModSMP artifacts + lore↗ - Naoya ModFabric ability system↗ - Devourer PluginAsymmetric Paper PvP↗ - Fi3w0 HyprlandThe original daily dots↗ - Fiw ModChecklistWhitelist prototype↗ - VeeBot V4.2Python / Discord↗ - BrotaPaused mobile prototype↗ + AquaTTYGo / ASCII liquid physics↗ + HomePageSelf-hosted homelab dashboard↗ + FIW AntiCheatModpack verification↗ + FIW GravesPersistent loot recovery↗ + FIW Admin UtilitiesServer maintenance + moderation↗ + Firefox PrivacyCross-platform browser hardening↗
@@ -396,19 +386,19 @@

04 / Tools

What is usually open in my terminal.

-

Kitty on a MacBook Air M4 is the main console, SSH'd out to the Arch desktop, ThinkPad T14, and Ubuntu server for everything else.

+

Kitty on a MacBook Air M4 is the main console, SSH'd out to the Arch desktop, ThinkPad T14 running Gentoo or Arch, and Ubuntu server for everything else.

Home turf

Linux / systems

-
Arch LinuxBashUbuntu ServersystemdSSHNetworking
+
GentooArch LinuxBashUbuntu ServersystemdSSHNetworking

Ship + operate

Infrastructure

-
DockerComposeTraefikPortainerGitHub ActionsBackups
+
DockerComposeTraefikKubernetes / k3sAnsibleGitHub ActionsBackups

Build surface

@@ -416,9 +406,9 @@
GoLuaRust / AI-assistedJavaKotlinTypeScriptQMLJSON
-

Learning next

-

Kubernetes, Terraform, Ansible

-

I am learning these when a real problem gives me a reason, not just to collect more logos.

+

Deploy + recover

+

Observe it. Roll it back.

+

Prometheus, Grafana, OpenTelemetry, and Argo Rollouts: see what broke and keep a bad release away from real traffic.

diff --git a/js/commissions.js b/js/commissions.js new file mode 100644 index 0000000..9f421a2 --- /dev/null +++ b/js/commissions.js @@ -0,0 +1,101 @@ +const commissionPage = document.querySelector(".commissions-page"); + +if (commissionPage) { + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)").matches; + + if (finePointer && !reduceMotion) { + const hero = document.querySelector(".commission-hero-new"); + + window.addEventListener("pointermove", (event) => { + commissionPage.style.setProperty("--mx", `${event.clientX}px`); + commissionPage.style.setProperty("--my", `${event.clientY}px`); + + if (hero) { + const bounds = hero.getBoundingClientRect(); + hero.style.setProperty("--hx", `${event.clientX - bounds.left}px`); + hero.style.setProperty("--hy", `${event.clientY - bounds.top}px`); + } + }, { passive: true }); + } + + const clock = document.querySelector(".scope-clock"); + if (clock) { + const startedAt = Date.now(); + const updateClock = () => { + const seconds = Math.floor((Date.now() - startedAt) / 1000); + const hours = String(Math.floor(seconds / 3600)).padStart(2, "0"); + const minutes = String(Math.floor((seconds % 3600) / 60)).padStart(2, "0"); + const remainder = String(seconds % 60).padStart(2, "0"); + clock.textContent = `${hours}:${minutes}:${remainder}`; + }; + updateClock(); + window.setInterval(updateClock, 1000); + } + + const rotatingWord = document.querySelector(".commission-word"); + if (rotatingWord && !reduceMotion) { + const words = (rotatingWord.dataset.words || "weird").split(","); + let wordIndex = 0; + window.setInterval(() => { + rotatingWord.classList.add("is-changing"); + window.setTimeout(() => { + wordIndex = (wordIndex + 1) % words.length; + rotatingWord.textContent = words[wordIndex]; + }, 110); + window.setTimeout(() => rotatingWord.classList.remove("is-changing"), 280); + }, 2800); + } + + const filterButtons = document.querySelectorAll("[data-price-filter]"); + const priceGroups = document.querySelectorAll("[data-price-category]"); + const visibleCount = document.querySelector("#price-visible-count"); + + filterButtons.forEach((button) => { + button.addEventListener("click", () => { + const selected = button.dataset.priceFilter; + let count = 0; + + filterButtons.forEach((candidate) => { + const active = candidate === button; + candidate.classList.toggle("is-active", active); + candidate.setAttribute("aria-pressed", String(active)); + }); + + priceGroups.forEach((group) => { + const visible = selected === "all" || group.dataset.priceCategory === selected; + group.hidden = !visible; + if (visible) count += group.querySelectorAll(".price-list > div").length; + }); + + if (visibleCount) visibleCount.textContent = String(count); + }); + }); + + const copyBrief = document.querySelector("#copy-brief"); + const copyStatus = document.querySelector("#copy-status"); + const brief = [ + "Hi Alex — I have a commission request.", + "", + "What I want to build or fix:", + "Current system / versions:", + "What happens now:", + "What I want to happen:", + "What counts as done:", + "Budget range:", + "Deadline, if any:", + "Useful logs / screenshots:", + ].join("\n"); + + if (copyBrief && copyStatus) { + copyBrief.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(brief); + copyStatus.textContent = "Commission brief copied. Paste it into an email, Telegram, or Discord."; + } catch { + copyStatus.textContent = "Clipboard access was blocked. Email me the goal, setup, symptoms, and any logs."; + } + window.setTimeout(() => { copyStatus.textContent = ""; }, 4000); + }); + } +} diff --git a/webpack.common.js b/webpack.common.js index b502ea9..16c82fe 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -3,10 +3,11 @@ const path = require('path'); module.exports = { entry: { app: './js/app.js', + commissions: './js/commissions.js', }, output: { path: path.resolve(__dirname, 'dist'), clean: true, - filename: './js/app.js', + filename: './js/[name].js', }, }; diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 96b3fde..0946486 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -10,6 +10,11 @@ module.exports = merge(common, { template: './index.html', inject: false, }), + new HtmlWebpackPlugin({ + template: './commissions/index.html', + filename: 'commissions/index.html', + inject: false, + }), new CopyPlugin({ patterns: [ { from: 'img', to: 'img' },