Redesign the resume site as a 3D-editor viewport
Deploy to GitHub Pages / build (push) Canceled after 0s
Deploy to GitHub Pages / deploy (push) Canceled after 0s

New design: studio backdrop with a perspective floor and X/Z axis lines, an
orientation gizmo that orbits on scroll, Bricolage Grotesque and Instrument
Sans (bundled, no build-time network). Projects are laid out as plates by a
row planner that keeps every row full for any count; a demo image from the
repo root is shown when present, otherwise a generated voxel sculpture.
Text and projects still come live from Gitea.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XtcZtnG4GQA1d6mW5PNrS9
This commit is contained in:
Emil
2026-09-20 21:37:35 +03:00
co-authored by Claude Sonnet 5
parent 32c57a5ada
commit b5ffc67a82
23 changed files with 1440 additions and 374 deletions
Binary file not shown.
Binary file not shown.
+37 -139
View File
@@ -1,144 +1,42 @@
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Manrope:wght@400;500;600;700&display=swap');
/* Design tokens: a studio-lit 3D viewport. Axis colours (X red, Y green, Z blue) are marks and lines, not text. */
:root {
--studio: #e6e9ed;
--studio-deep: #cdd3db;
--paper: #f4f6f8;
--ink: #15181d;
--muted: #5b6571;
--line: rgba(21, 24, 29, 0.14);
--x: #e0384b;
--y: #5aa300;
--z: #2c63e0;
--z-text: #2455c9;
:root { --ink:#f3f6f0; --paper:#0b0e0d; --surface:#121715; --surface-raised:#171d1a; --acid:#c7ff46; --muted:#96a099; --line:rgba(222,237,220,.14); --dark:#070908; --glow:rgba(199,255,70,.14); }
* { box-sizing:border-box; }
html { scroll-behavior:smooth; background:var(--paper); }
body { margin:0; overflow-x:hidden; background:var(--paper); color:var(--ink); font-family:'Manrope',sans-serif; }
main { overflow:hidden; position:relative; background-image:linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px); background-size:48px 48px; }
a { color:inherit; text-decoration:none; }
a:focus-visible { outline:2px solid var(--acid); outline-offset:5px; }
::selection { background:var(--acid); color:#0b0e0d; }
.link-arrow { width:14px; height:14px; flex:none; fill:none; stroke:currentColor; stroke-width:1.35; stroke-linecap:round; stroke-linejoin:round; }
.shell { width:min(1180px, calc(100% - 48px)); margin-inline:auto; }
.ambient { position:absolute; width:680px; height:680px; border-radius:50%; pointer-events:none; filter:blur(4px); opacity:.55; }
.ambient-one { top:-350px; right:-220px; background:radial-gradient(circle, var(--glow), transparent 67%); }
.ambient-two { top:1200px; left:-430px; background:radial-gradient(circle, rgba(67,146,130,.13), transparent 66%); }
.nav { height:86px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--line); position:relative; z-index:1; }
.mark { font:600 17px 'DM Mono',monospace; letter-spacing:-.04em; color:var(--acid); }
.mark span { color:var(--muted); font-weight:300; }
.nav nav { display:flex; gap:34px; font:400 12px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.08em; }
.nav nav a { position:relative; transition:color .2s; }.nav nav a::after { content:''; position:absolute; left:0; right:0; bottom:-8px; height:1px; background:var(--acid); transform:scaleX(0); transform-origin:left; transition:transform .2s; }.nav nav a:hover { color:var(--acid); }.nav nav a:hover::after { transform:scaleX(1); }
.hero { min-height:760px; padding:64px 0 70px; position:relative; border-bottom:1px solid var(--line); }
.hero-kicker { max-width:290px; font:400 11px/1.5 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); display:flex; gap:10px; }
.pulse { width:8px; height:8px; margin-top:4px; background:var(--acid); border-radius:50%; box-shadow:0 0 0 5px var(--glow),0 0 18px var(--acid); flex:none; }
.hero-orbit { position:absolute; width:230px; height:230px; border:1px solid rgba(199,255,70,.24); right:-60px; top:90px; border-radius:50%; opacity:.55; animation:spin 20s linear infinite; z-index:0; pointer-events:none; }
.hero-orbit::before,.hero-orbit::after { content:''; position:absolute; border-radius:50%; background:var(--acid); box-shadow:0 0 18px var(--acid); }.hero-orbit::before { width:7px; height:7px; top:34px; left:34px; }.hero-orbit::after { width:4px; height:4px; bottom:29px; right:56px; }
.hero-orbit span { position:absolute; inset:32px; border:1px dashed rgba(231,250,224,.18); border-radius:50%; display:grid; place-items:center; font:500 13px 'DM Mono',monospace; color:var(--acid); animation:spin-reverse 20s linear infinite; }.hero-orbit i { position:absolute; width:100%; height:1px; top:50%; background:linear-gradient(90deg, transparent,rgba(199,255,70,.6),transparent); }
.eyebrow { font:400 11px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.15em; color:var(--muted); }
.hero h1 { font-size:clamp(70px,11.5vw,164px); line-height:.82; letter-spacing:-.085em; margin:56px 0 26px -8px; font-weight:600; position:relative; z-index:1; text-shadow:0 0 55px rgba(199,255,70,.07); }
.hero h2 { max-width:760px; font-size:clamp(28px,4.2vw,58px); line-height:1.03; letter-spacing:-.05em; margin:0 0 32px; font-weight:500; text-wrap:balance; }
.hero-copy { width:min(650px, calc(100% - 220px)); margin-left:26%; color:#bac4bb; font-size:16px; line-height:1.75; }
.hero-actions { margin:42px 0 0 26%; display:flex; gap:10px; }
.button { padding:14px 18px; border:1px solid rgba(243,246,240,.36); background:rgba(15,20,18,.55); font:500 11px 'DM Mono',monospace; text-transform:uppercase; display:flex; gap:26px; justify-content:space-between; transition:transform .2s,background .2s,border .2s; }
.button.primary { background:var(--acid); border-color:var(--acid); color:#11150f; box-shadow:0 12px 30px rgba(199,255,70,.16); }.button:hover { transform:translateY(-3px); background:var(--acid); border-color:var(--acid); color:#11150f; }
.hero-facts { display:flex; gap:38px; margin:58px 0 0 26%; font:400 10px 'DM Mono',monospace; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }.hero-facts b { color:var(--acid); font-size:17px; font-weight:400; margin-right:6px; }
.hero-index { position:absolute; right:0; bottom:70px; font:300 11px 'DM Mono',monospace; color:var(--muted); pointer-events:none; }
@media (max-width:1100px) {
.hero-orbit { display:none; }
.hero-index { display:none; }
}
.section-heading { display:flex; justify-content:space-between; align-items:flex-end; padding:23px 0; border-bottom:1px solid rgba(243,246,240,.65); }
.section-heading p { margin:0; font:500 12px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.11em; }
.section-heading span { font:300 11px 'DM Mono',monospace; color:var(--muted); }
.capabilities,.work,.tools,.about { padding-top:100px; }
.cap-grid { display:grid; grid-template-columns:repeat(3,1fr); }
.cap-card { padding:42px 30px 36px 0; min-height:320px; border-right:1px solid var(--line); transition:background .25s,padding .25s; }
.cap-card + .cap-card { padding-left:30px; }
.cap-card:last-child { border-right:0; }
.cap-number { font:300 11px 'DM Mono',monospace; color:var(--muted); }
.cap-card:hover { background:linear-gradient(135deg,rgba(199,255,70,.09),transparent 68%); }.cap-card h3 { font-size:24px; margin:54px 0 16px; letter-spacing:-.035em; }.cap-card p { color:#b0bab2; font-size:14px; line-height:1.7; min-height:74px; }.cap-card:hover .cap-number { color:var(--acid); }
.tag-list { display:flex; flex-wrap:wrap; gap:7px; margin-top:24px; }
.tag-list span { font:400 10px 'DM Mono',monospace; border:1px solid var(--line); background:rgba(255,255,255,.025); padding:6px 8px; color:#b8c2bb; }
.project { display:grid; grid-template-columns:160px minmax(0,1fr) 150px; gap:34px; padding:52px 0; border-bottom:1px solid var(--line); transition:background .25s; }.project:hover { background:linear-gradient(90deg,rgba(199,255,70,.075),transparent 72%); }.project:hover .project-meta span { color:var(--acid); }
.project-meta span { font:300 11px 'DM Mono',monospace; color:var(--muted); }
.project-meta p { font:400 10px/1.5 'DM Mono',monospace; text-transform:uppercase; color:var(--muted); margin-top:14px; max-width:150px; }
.project-body h3 { margin:0 0 18px; font-size:clamp(30px,4vw,52px); letter-spacing:-.055em; font-weight:500; }
.project-body > p { max-width:720px; color:#b0bab2; line-height:1.75; margin:0; }
.project-body ul { display:grid; grid-template-columns:1fr 1fr; padding:0; margin:26px 0 0; list-style:none; gap:9px 20px; }
.project-body li { font:400 11px 'DM Mono',monospace; }
.project-body li::before { content:'—'; color:var(--muted); margin-right:9px; }
.project-links { display:flex; align-items:flex-end; justify-content:flex-end; flex-direction:column; gap:10px; }
.project-links a { width:100%; display:flex; justify-content:space-between; align-items:center; padding:11px 0; border-bottom:1px solid rgba(243,246,240,.55); font:400 10px 'DM Mono',monospace; text-transform:uppercase; }.project-links a:hover { color:var(--acid); border-color:var(--acid); }.project-links a:hover .link-arrow,.tool-grid a:hover .link-arrow,.contact-grid a:hover .link-arrow { transform:translate(2px,-2px); }.link-arrow { transition:transform .2s; }
.tool-grid { display:grid; grid-template-columns:1fr 1fr; }
.tool-grid a { min-height:130px; border-bottom:1px solid var(--line); padding:26px 0; display:flex; justify-content:space-between; gap:30px; transition:color .2s,transform .2s; }
.tool-grid a:nth-child(odd) { padding-right:30px; border-right:1px solid var(--line); }
.tool-grid a:nth-child(even) { padding-left:30px; }
.tool-grid a:last-child:nth-child(odd) { border-right:0; padding-right:0; }
.tool-grid strong { font-size:18px; }
.tool-grid p { color:var(--muted); font-size:12px; margin:9px 0 0; }
.tool-grid a:hover { color:var(--acid); transform:translateX(8px); }.tool-grid a:hover strong { box-shadow:inset 0 -8px rgba(199,255,70,.28); }
.education { display:grid; grid-template-columns:160px 1fr; gap:34px; padding:30px 0; border-bottom:1px solid var(--line); }.education-list { padding:18px 0 70px; }.education-list .education:last-child { border-bottom:0; }
.education > span { font:300 11px 'DM Mono',monospace; color:var(--muted); }
.education h3 { margin:0 0 8px; font-size:22px; }
.education p { margin:0; color:#b0bab2; line-height:1.6; }
.education-note { font-size:13px; }
.footer { background:radial-gradient(circle at 85% 15%,rgba(199,255,70,.13),transparent 26%),var(--dark); color:#f3f6f0; width:100%; max-width:none; padding:90px max(24px,calc((100% - 1180px)/2)) 32px; margin-top:30px; border-top:1px solid var(--line); }
.footer .eyebrow { color:#a4a69c; }
.footer h2 { font-size:clamp(45px,7vw,96px); line-height:.98; letter-spacing:-.065em; font-weight:500; margin:48px 0 80px; }
.footer h2 em { color:var(--acid); font-style:normal; }
.contact-grid { border-top:1px solid #484b42; }
.contact-grid a { display:grid; grid-template-columns:120px 1fr 24px; padding:22px 0; border-bottom:1px solid #484b42; align-items:center; transition:padding .2s,color .2s; }.contact-grid a:hover { padding-inline:10px; }
.contact-grid span { font:400 10px 'DM Mono',monospace; text-transform:uppercase; color:#92958a; }
.contact-grid strong { font-weight:400; }
.contact-grid a:hover strong { color:var(--acid); }
.footer-bottom { display:flex; justify-content:space-between; padding-top:60px; color:#787b71; font:300 9px 'DM Mono',monospace; text-transform:uppercase; }
@keyframes spin { to { transform:rotate(360deg); } } @keyframes spin-reverse { to { transform:rotate(-360deg); } }
@media (prefers-reduced-motion:reduce) { *,*::before,*::after { scroll-behavior:auto!important; animation-duration:.01ms!important; animation-iteration-count:1!important; transition-duration:.01ms!important; } }
@media (max-width:980px) and (min-width:761px) {
.hero h1 { font-size:clamp(72px, 11vw, 120px); }
.hero-orbit { right:2%; width:250px; height:250px; }
.project { grid-template-columns:120px minmax(0,1fr); gap:26px; }
.project-links { grid-column:2; flex-direction:row; justify-content:flex-start; }
.project-links a { width:150px; }
}
@media (max-width:760px) {
.shell { width:min(100% - 28px,1180px); }
.nav { height:70px; }.nav nav { gap:15px; font-size:10px; }.nav nav a::after { bottom:-6px; }
.hero { min-height:auto; padding:55px 0 60px; }.hero-kicker { margin-bottom:34px; }.hero h1 { margin-top:0; font-size:19vw; }.hero h2 { font-size:36px; }
.hero-copy,.hero-actions { width:100%; margin-left:0; }.hero-copy { margin-top:30px; }.hero-actions { flex-direction:column; }.hero-facts { margin-left:0; gap:16px; flex-direction:column; }.hero-index { display:none; }
.section-heading span { display:none; }.capabilities,.work,.tools,.about { padding-top:70px; }
.cap-grid { grid-template-columns:1fr; }.cap-card,.cap-card + .cap-card { border-right:0; border-bottom:1px solid var(--line); padding:34px 0; min-height:0; }.cap-card h3 { margin-top:28px; }.cap-card p { min-height:0; }
.project { grid-template-columns:1fr; gap:20px; padding:38px 0; }.project:hover { padding-left:0; }.project-meta { display:flex; justify-content:space-between; }.project-meta p { margin:0; max-width:none; }.project-body ul { grid-template-columns:1fr; }.project-links { align-items:flex-start; }.project-links a { width:150px; }
.tool-grid { grid-template-columns:1fr; }.tool-grid a:nth-child(n) { border-right:0; padding:24px 0; }
.education { grid-template-columns:1fr; gap:20px; padding-bottom:70px; }.contact-grid a { grid-template-columns:80px 1fr 20px; }.contact-grid strong { font-size:12px; overflow-wrap:anywhere; }.footer-bottom { gap:20px; }.footer-bottom span:last-child { text-align:right; }
--radius-plate: 18px;
--radius-control: 10px;
--shell: min(1240px, calc(100% - 48px));
--gutter: clamp(20px, 3vw, 32px);
--section: clamp(84px, 11vw, 148px);
--font-body: var(--font-text), 'Segoe UI', system-ui, sans-serif;
--font-head: var(--font-display), 'Segoe UI', system-ui, sans-serif;
}
/* Resume site: skills, experience, live project list, side info. */
.skill-grid { display:grid; grid-template-columns:repeat(3,1fr); }
.skill-card { padding:34px 28px 34px 0; border-right:1px solid var(--line); border-bottom:1px solid var(--line); transition:background .25s; }
.skill-card:nth-child(3n+2), .skill-card:nth-child(3n) { padding-left:28px; }
.skill-card:nth-child(3n) { border-right:0; }
.skill-card:hover { background:linear-gradient(135deg,rgba(199,255,70,.09),transparent 68%); }
.skill-card h3 { font-size:22px; margin:30px 0 12px; letter-spacing:-.035em; }
.skill-card p { margin:0; color:#b0bab2; font-size:14px; line-height:1.65; }
.exp-item { display:grid; grid-template-columns:200px 1fr; gap:34px; padding:34px 0; border-bottom:1px solid var(--line); }
.exp-item > span { font:300 11px/1.6 'DM Mono',monospace; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
.exp-item h3 { margin:0 0 16px; font-size:22px; letter-spacing:-.03em; }
.exp-item ul { margin:0; padding:0; list-style:none; display:grid; gap:9px; color:#b0bab2; line-height:1.6; }
.exp-item li::before { content:'—'; color:var(--muted); margin-right:10px; }
.status-note { margin:0; padding:44px 0; border-bottom:1px solid var(--line); color:var(--muted); font:400 12px/1.8 'DM Mono',monospace; }
.status-note a { color:var(--acid); border-bottom:1px solid rgba(199,255,70,.4); }
.status-note b { color:var(--ink); font-weight:500; }
.project-updated { margin:20px 0 0; color:var(--muted); font:300 10px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.05em; }
.about-grid { display:grid; grid-template-columns:1.25fr 1fr; gap:64px; padding-bottom:70px; }
.about-grid .education-list { padding-bottom:0; }
.side-info { padding-top:18px; }
.mini-heading { margin:0 0 14px; padding-bottom:10px; border-bottom:1px solid rgba(243,246,240,.35); font:500 11px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.11em; }
.side-info .mini-heading:not(:first-child) { margin-top:40px; }
.lang-list { list-style:none; margin:0; padding:0; }
.lang-list li { display:flex; justify-content:space-between; padding:10px 0; border-bottom:1px solid var(--line); }
.lang-list b { font:400 11px 'DM Mono',monospace; color:var(--acid); font-weight:400; }
.side-info .tag-list { margin-top:0; }
.contact-note { color:#92958a; font-style:normal; font-size:12px; }
@media (max-width:980px) {
.skill-grid { grid-template-columns:repeat(2,1fr); }
.skill-card:nth-child(n) { padding:34px 24px 34px 0; border-right:1px solid var(--line); }
.skill-card:nth-child(2n) { padding-left:24px; border-right:0; }
.about-grid { grid-template-columns:1fr; gap:40px; }
* { box-sizing: border-box; }
html { background: var(--paper); scroll-behavior: smooth; scroll-padding-top: 88px; }
body {
margin: 0;
color: var(--ink);
font: 400 17px/1.6 var(--font-body);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
@media (max-width:760px) {
.skill-grid { grid-template-columns:1fr; }
.skill-card:nth-child(n) { padding:30px 0; border-right:0; }
.exp-item { grid-template-columns:1fr; gap:14px; padding:28px 0; }
.about-grid { padding-bottom:50px; }
a { color: inherit; }
h1, h2, h3, p, ul, dl, dd, figure { margin: 0; }
ul { padding: 0; list-style: none; }
::selection { background: var(--z); color: #fff; }
:focus-visible { outline: 3px solid var(--z); outline-offset: 3px; border-radius: 4px; }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation-duration: 0.01ms !important; animation-delay: 0ms !important; transition-duration: 0.01ms !important; }
}
+6 -1
View File
@@ -1,6 +1,11 @@
import type { Metadata } from 'next'
import localFont from 'next/font/local'
import './globals.css'
// Both fonts are bundled (SIL Open Font License), so building never depends on the network.
const display = localFont({ src: './fonts/BricolageGrotesque-Variable.woff2', variable: '--font-display', weight: '200 800', display: 'swap' })
const text = localFont({ src: './fonts/InstrumentSans-Variable.woff2', variable: '--font-text', weight: '400 700', display: 'swap' })
export const metadata: Metadata = {
title: 'Emil Shanayev — Resume',
description: 'Resume of Emil Shanayev: backend, DevOps, AI systems, scientific software and game technology.',
@@ -9,7 +14,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<html lang="en" className={`${display.variable} ${text.variable}`}>
<body>{children}</body>
</html>
)
+1 -1
View File
@@ -1,6 +1,6 @@
import Site from '@/components/Site'
import resume from '@/data/resume.json'
import type { Resume } from '@/lib/types'
import Site from './site'
export default function Home() {
return <Site initial={resume as unknown as Resume} />
-212
View File
@@ -1,212 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
import type { Live, Project, Resume } from '@/lib/types'
// Deployed on shanaty.ru the build points at the same-origin file; elsewhere (dev, GitHub Pages) it uses the public URL.
const DATA_URL = process.env.NEXT_PUBLIC_DATA_URL ?? 'https://shanaty.ru/data/resume.json'
const GITEA_PROFILE = 'https://git.shanaty.ru/Emil_Shanaty'
const pad = (n: number) => String(n).padStart(2, '0')
const monthYear = (iso?: string) => (iso ? new Date(iso).toLocaleDateString('en', { month: 'short', year: 'numeric' }) : '')
const ArrowIcon = () => (
<svg className="link-arrow" viewBox="0 0 16 16" aria-hidden="true">
<path d="M3 13 13 3M6 3h7v7" />
</svg>
)
type Status = 'loading' | 'live' | 'offline'
export default function Site({ initial }: Readonly<{ initial: Resume }>) {
const [resume, setResume] = useState<Resume>(initial)
const [projects, setProjects] = useState<Project[]>([])
const [generatedAt, setGeneratedAt] = useState<string>()
const [status, setStatus] = useState<Status>('loading')
useEffect(() => {
const controller = new AbortController()
fetch(DATA_URL, { cache: 'no-store', signal: controller.signal })
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
.then((data: Live) => {
if (!data?.resume || !Array.isArray(data.projects)) throw new Error('unexpected data')
setResume(data.resume)
setProjects(data.projects)
setGeneratedAt(data.generated_at)
setStatus('live')
})
.catch((error: Error) => {
if (error.name !== 'AbortError') setStatus('offline')
})
return () => controller.abort()
}, [])
// Once the list has loaded and is empty (no repo carries the `resume` topic yet), the section is hidden entirely.
const showProjects = status !== 'live' || projects.length > 0
const primary = resume.contacts[0]
const github = resume.contacts.find((contact) => contact.label === 'GitHub')
return (
<main>
<div className="ambient ambient-one" aria-hidden="true" />
<div className="ambient ambient-two" aria-hidden="true" />
<header className="nav shell">
<a className="mark" href="#top" aria-label="Home">ES<span>/26</span></a>
<nav aria-label="Main navigation">
<a href="#skills">Skills</a>
<a href="#experience">Experience</a>
{showProjects && <a href="#projects">Projects</a>}
<a href="#contact">Contact</a>
</nav>
</header>
<section className="hero shell" id="top">
<div className="hero-kicker"><span className="pulse" /> {resume.availability}</div>
<div className="hero-orbit" aria-hidden="true"><span>AI</span><i /></div>
<p className="eyebrow">{resume.eyebrow}</p>
<h1>{resume.name}</h1>
<h2>{resume.role}</h2>
<p className="hero-copy">{resume.bio}</p>
<div className="hero-actions">
{showProjects && <a className="button primary" href="#projects">Selected work</a>}
<a className={showProjects ? 'button' : 'button primary'} href={primary.href}>Get in touch <ArrowIcon /></a>
{github && <a className="button" href={github.href} target="_blank" rel="noreferrer">GitHub <ArrowIcon /></a>}
</div>
<div className="hero-facts">
{status === 'live' && projects.length > 0 && <span><b>{pad(projects.length)}</b> projects, live from Gitea</span>}
<span><b>{pad(resume.skills.length)}</b> areas of practice</span>
</div>
{status === 'live' && projects.length > 0 && <div className="hero-index" aria-hidden="true">01—{pad(projects.length)}</div>}
</section>
<section className="capabilities shell" id="skills">
<div className="section-heading">
<p>Skills / stack</p>
<span>Where I work</span>
</div>
<div className="skill-grid">
{resume.skills.map((skill, index) => (
<article className="skill-card" key={skill.title}>
<span className="cap-number">{pad(index + 1)}</span>
<h3>{skill.title}</h3>
<p>{skill.text}</p>
<div className="tag-list">{skill.stack.map((tag) => <span key={tag}>{tag}</span>)}</div>
</article>
))}
</div>
</section>
<section className="about shell" id="experience">
<div className="section-heading">
<p>Experience</p>
<span>Building, running, leading</span>
</div>
<div className="exp-list">
{resume.experience.map((entry) => (
<div className="exp-item" key={entry.title}>
<span>{entry.meta}</span>
<div>
<h3>{entry.title}</h3>
<ul>{entry.points.map((point) => <li key={point}>{point}</li>)}</ul>
</div>
</div>
))}
</div>
</section>
{showProjects && (
<section className="work shell" id="projects">
<div className="section-heading">
<p>Selected work</p>
<span>
{status === 'live' && `Live from Gitea · updated ${monthYear(generatedAt)}`}
{status === 'loading' && 'Loading from Gitea…'}
{status === 'offline' && 'Live list unavailable'}
</span>
</div>
{status === 'loading' && (
<p className="status-note">
Loading projects…
<noscript> The project list is loaded live from git.shanaty.ru and needs JavaScript. Repositories: <a href={GITEA_PROFILE}>{GITEA_PROFILE.replace('https://', '')}</a>.</noscript>
</p>
)}
{status === 'offline' && (
<p className="status-note">
The live project list could not be loaded right now. Browse the repositories directly at{' '}
<a href={GITEA_PROFILE}>{GITEA_PROFILE.replace('https://', '')}</a>
{github && <> or <a href={github.href}>github.com/{github.value}</a></>}.
</p>
)}
<div className="project-list">
{projects.map((project, index) => (
<article className="project" key={project.name}>
<div className="project-meta">
<span>{pad(index + 1)}</span>
<p>{project.kind ?? project.language ?? 'Repository'}</p>
</div>
<div className="project-body">
<h3>{project.title ?? project.name}</h3>
{project.description && <p>{project.description}</p>}
{project.highlights && project.highlights.length > 0 && (
<ul>{project.highlights.map((highlight) => <li key={highlight}>{highlight}</li>)}</ul>
)}
{project.tech.length > 0 && <div className="tag-list">{project.tech.map((tag) => <span key={tag}>{tag}</span>)}</div>}
{project.updated && <p className="project-updated">Updated {monthYear(project.updated)}</p>}
</div>
<div className="project-links">
<a href={project.url} target="_blank" rel="noreferrer">Repository <ArrowIcon /></a>
{project.homepage && <a href={project.homepage} target="_blank" rel="noreferrer">Live site <ArrowIcon /></a>}
{project.links?.map((link) => (
<a href={link.href} target="_blank" rel="noreferrer" key={link.href}>{link.label} <ArrowIcon /></a>
))}
</div>
</article>
))}
</div>
</section>
)}
<section className="about shell" id="education">
<div className="section-heading">
<p>Education & more</p>
<span>Learning by building</span>
</div>
<div className="about-grid">
<div className="education-list">
{resume.education.map((entry) => (
<div className="education" key={entry.institution}>
<span>{entry.period}</span>
<div><h3>{entry.institution}</h3><p>{entry.program}</p></div>
</div>
))}
</div>
<div className="side-info">
<h3 className="mini-heading">Languages</h3>
<ul className="lang-list">
{resume.languages.map((language) => (
<li key={language.name}><span>{language.name}</span><b>{language.level}</b></li>
))}
</ul>
<h3 className="mini-heading">Interests</h3>
<div className="tag-list">{resume.interests.map((interest) => <span key={interest}>{interest}</span>)}</div>
</div>
</div>
</section>
<footer className="footer shell" id="contact">
<p className="eyebrow">Have a difficult technical problem?</p>
<h2>Let’s build something<br /><em>that actually works.</em></h2>
<div className="contact-grid">
{resume.contacts.map((contact) => (
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer" key={contact.value}>
<span>{contact.label}</span>
<strong>{contact.value}{contact.note && <em className="contact-note"> · {contact.note}</em>}</strong>
<ArrowIcon />
</a>
))}
</div>
<div className="footer-bottom"><span>© 2026 Emil Shanayev</span><span>Content and project list are read live from Gitea</span></div>
</footer>
</main>
)
}