diff --git a/icon.png b/icon.png index 81f6fa2..72d5a4d 100644 Binary files a/icon.png and b/icon.png differ diff --git a/images/containr.png b/images/containr.png index 0142dcf..5e5435a 100644 Binary files a/images/containr.png and b/images/containr.png differ diff --git a/images/logo-mark.png b/images/logo-mark.png new file mode 100644 index 0000000..7379997 Binary files /dev/null and b/images/logo-mark.png differ diff --git a/images/projects/bookra.png b/images/projects/bookra.png new file mode 100644 index 0000000..b32081a Binary files /dev/null and b/images/projects/bookra.png differ diff --git a/images/projects/cal.png b/images/projects/cal.png new file mode 100644 index 0000000..4e82cf3 Binary files /dev/null and b/images/projects/cal.png differ diff --git a/images/projects/nothing-modes.png b/images/projects/nothing-modes.png new file mode 100644 index 0000000..fcc4631 Binary files /dev/null and b/images/projects/nothing-modes.png differ diff --git a/images/projects/pulsemonitor.png b/images/projects/pulsemonitor.png new file mode 100644 index 0000000..af6c954 Binary files /dev/null and b/images/projects/pulsemonitor.png differ diff --git a/images/projects/timespent.png b/images/projects/timespent.png new file mode 100644 index 0000000..de713e5 Binary files /dev/null and b/images/projects/timespent.png differ diff --git a/images/projects/vibly.png b/images/projects/vibly.png new file mode 100644 index 0000000..be70b6e Binary files /dev/null and b/images/projects/vibly.png differ diff --git a/index.html b/index.html index 55e6ee2..a5183fd 100644 --- a/index.html +++ b/index.html @@ -3,875 +3,453 @@ - Tomas Dvorak - Portfolio - + Tomáš Dvořák — Full-Stack Developer & Systems Architect + - - - + + - - + - - - - + + + + - - - - + + + + - + -
- - - + + +
-
-
-
-
- Tomáš Dvořák -
-
- AI-Driven Product & Systems Specialist · CTO -

Tomáš Dvořák

-

- AI-Powered Product Builder & Systems Architect
- Co-Founder & CTO at SportCreative.
- I design and deliver AI-powered web systems from architecture to deployment. -

-
- - - info@tdvorak.dev -
-
+ +
+ +
+
Open to opportunities
+

Tomáš Dvořák

+

Full-Stack Developer & Systems Architect

+

+ Building scalable apps, SaaS platforms, automation tools and real-time systems. + Software Engineer & DevOps at MyBox (ELEXIM) — + Co-Founder & CTO at SportCreative and ReKlik. +

+ +

Uherské Hradiště, Czech Republic · info@tdvorak.dev

+
+
+ + +
+ -
-
-
-

About

-

- I work at the intersection of AI, web systems, and digital infrastructure, specializing in AI-assisted development from idea to deployment.

- - I understand modern web systems end-to-end and guide AI tools to produce reliable, maintainable solutions, turning complex ideas into working products quickly.

- - Core areas:
- • AI-assisted full-stack web systems (HTML, CSS, JS, Golang & Python)
- • System architecture & product thinking
- • Databases, APIs, authentication flows
- • Docker, Proxmox, Linux/Ubuntu, deployment & infrastructure
- • IT security awareness & penetration testing (Kali Linux)
- • Digital products, internal tools, MVPs

- - I also work with visual and creative tools — Photoshop, Lightroom, DaVinci Resolve — and I'm active in photography, cinematography, and content creation, bridging technical and creative worlds. -

- - - + + -
-
- - - + +

// Private & internal

+
+
+

Invico

Invoicing platform for the Czech market — issued invoices, zálohová, dobropis, ISDOC, SPD QR, PDF. The product behind Výdoz.

+ Go · React · Private +
+
+

Soika

Internal e-commerce audit, compliance-risk and outreach platform — operator-only consulting tool.

+ Go · React · Private +
+
+

MyBox Worktree

Internal development workspace for the MyBox EV charging platform at ELEXIM.

+ Work · Private +
+
+

SendMail

Self-hosted mail relay — powers this site's contact form.

+ Go · Private +
+
+

Flashscore Scraper

Sports data scraper service.

+ Go · Private
-
-
-
- -
-
- Trackeep -
-
- Open Source -

Trackeep

-
- -
- - -
-
- Containr -
-
- Open Source -

Containr

-
- -
- - -
-
- YouTube data scraper API -
-
- Selfhosted • Selfdeveloped -

YouTube Scraper API

-
- -
- - -
-
- PlexSync -
-
- Open Source -

PlexSync

-
- -
- - -
-
- EDI_DELFOR_Parser -
-
- Open Source -

EDI_DELFOR_Parser

-
- -
- - -
-
- PPve App 1 - PPve App 2 -
-
- Private Company App -

PPve

-
-
-
- - -
-
- MyClub -
-
- Selfhosted • Selfdeveloped -

MyClub

-
- -
- - -
-
- SportCreative -
-
- Agency Website -

SportCreative

-
- -
- - -
-
- ZEUS CZ -
-
- E‑shop -

ZEUS (CZ)

-
- -
- - -
-
- ZEUS EU -
-
- Selfhosted • Selfdeveloped -

ZEUS (EU)

-
- -
- - -
-
- Reklik landing -
-
- Landing Page -

Reklik

-
- -
- - -
-
- Reklik foto portfolio -
-
- Selfhosted • Selfdeveloped -

Reklik Foto

-
- -
- - -
-
- FAČR scraper -
-
- Selfhosted • Selfdeveloped -

FAČR Scraper

-
- -
- - -
-
- YouTube search scraper API -
-
- Selfhosted • Selfdeveloped -

YouTube Proxy API

-
- -
- - -
-
- SparkScreen -
-
- Selfhosted • Selfdeveloped -

SparkScreen

-
- -
- - -
-
- 1337x scraper -
-
- Selfhosted • Selfdeveloped -

1337x Scraper

-
- -
- - -
-
- SparkScreen API (GoAPI) -
-
- Selfhosted • Selfdeveloped -

GoAPI

-
- -
- - -
-
- Bizoni UH -
-
- Selfhosted • Selfdeveloped -

Bizoni UH

-
- -
- - -
-
- Personal portfolio -
-
- Selfdeveloped -

Personal Portfolio

-
- -
-
-
-
- -
-
- - - -

Programming Languages:

-
    -
  • - HTML5 - HTML5 -
  • -
  • - CSS3 - CSS3 -
  • -
  • - JavaScript - JavaScript -
  • -
  • - Go - GO -
  • -
  • - Typescript - Typescript -
  • -
  • - React.js - React.js -
  • -
  • - - - - - - Docker -
  • -
  • - Vite - Vite -
  • -
  • - Astro - Astro -
  • -
  • - Bun - Bun -
  • -
  • - Bun - Deno -
  • -
  • - PostgreSQL - PostgreSQL -
  • -

-

Softwares:

-
    -
  • - Windsurf - Windsurf -
  • -
  • - Github - Github -
  • -
  • - Gitea - Gitea -
  • -
  • - - supabase - - - - - - - - - - - - - - - - - Supabase -
  • -
  • - Appwrite - Appwrite -
  • -
  • - - Cloudflare-logo-vector-svg - - - - - Cloudflare -
  • -
  • - - - - ZeroTier -
  • -
  • - Proxmox - Proxmox -
  • -
  • - TOR - TOR -
  • -
  • - Metasploit - Metasploit -
  • -
  • - Nmap - Nmap -
  • -
  • - Davinci Resolve - Davinci Resolve -
  • -
  • - Photoshop - Photoshop -
  • -
  • - Lightroom - Lightroom -
  • -
  • - Adobe Illustrator - Adobe Illustrator -
  • -

-

Operating Systems:

-
    -
  • - Windows 10 - Windows 10 -
  • -
  • - Windows 11 - Windows 11 -
  • -
  • - Android - Android -
  • -
  • - Linux - Linux -
  • -
  • - Kali Linux - Kali Linux -
  • -
  • - Debian Linux - Debian Linux -
  • -
  • - Raspberry Pi OS - Raspberry Pi OS -
  • -
  • - - - - - Parrot OS -
  • -
  • - Ubuntu Linux - Ubuntu Linux -
  • -
  • - Proxmox - Proxmox -
  • -

-

In Progress:

-
    -
  • - Java - Java -
  • -
  • - Spring Boot - Spring Boot -
  • -
  • - Flutter - Flutter -
  • -
  • - Dart - Dart -
  • -
  • - Python - Python -
  • -
  • - Rust - Rust -
  • -
-
-

Work Experience:

-
    -
  • -
    -

    IT / Web Development Intern → Full-Stack Developer / AI-Assisted Systems Specialist — Poppe + Potthoff s.r.o.

    - Internship → Contract · May 2025 – Present · · Czech Republic - Started as intern building internal web portal (Go + Tailwind)
    Assisted with virtualization and infrastructure setup (Proxmox)
    Progressed to full-stack development role
    AI-assisted backend and system logic implementation
    Infrastructure and virtualization using Proxmox
    Linux-based systems administration and IT support
    Git-based workflows and deployment coordination
    Participation in IT security and internal system hardening
    - Tech: Go, HTML, Tailwind CSS, Git, Linux, Proxmox, Cloudflare -
    -
  • - -
  • -
    -

    Co-Founder & CTO — SportCreative

    - Hybrid · Apr 2025 – Present · - Leading technical and digital strategy for sports-focused marketing agency
    Designing and delivering AI-assisted web systems, websites, and internal tools
    Overseeing automation, infrastructure, and system architecture
    Connecting development, content, design, and analytics into unified solutions
    Supporting sports clubs with scalable digital platforms and content workflows
    - Tech: Full-stack Development, AI Systems, Digital Strategy, Infrastructure -
    -
  • - - - - -
  • -
    -

    Co-Founder & CTO — ReKlik

    - On-site · Jun 2025 – Present · - Responsible for technical development and digital workflows
    Designing end-to-end production pipelines (from capture to delivery)
    Ensuring visual quality, system efficiency, and automation
    Supporting real-estate and commercial photography operations with tech solutions
    - Tech: Photography Systems, Workflow Automation, Digital Asset Management -
    -
  • - -
  • -
    -

    Web Developer & Digital Marketing Lead (Freelance) — Sports & E-commerce Brands

    - Remote / Hybrid · Sep 2024 – Present · - Web development and maintenance (HTML, CSS, Tailwind, JS)
    AI-assisted content systems and automation
    Digital marketing strategy, brand growth, and campaign support
    UX improvements, e-commerce coordination, and visual identity
    Social media systems, short-form video, and content pipelines
    (Multiple long-term freelance collaborations merged into one role for clarity and seniority.)
    - Brands: ZEUS, Masita, Winnersport, RévviSport, FC Bizoni -
    -
  • - -
-

Education & Continuous Learning

-
    -
  • -
    -

    - Information Technology – Secondary Technical School
    - SGaSOŠ (Private Grammar & Secondary Vocational School) -

    - 2024 – 2027 · Czech Republic - Focus on information technology, programming fundamentals, and digital systems
    Practical exposure to web technologies, databases, and IT infrastructure
    Strong emphasis on self-directed learning and real-world projects
    -
    -
  • - -
  • -
    -

    - Self-Taught AI, Programming & Systems Development
    - Ongoing -

    - Continuous Learning - I am primarily self-taught, learning through real projects, experimentation, and continuous practice, supported by high-quality online education and industry creators. -
    -
  • - -
  • -
    -

    - Zero To Mastery (ZTM) Bootcamps -

    - Key Learning Source - Programming fundamentals (JavaScript, Python, Go concepts)
    Databases & backend logic
    Full-stack development principles
    Practical, project-based learning approach
    -
    -
  • - -
  • -
    -

    - NetworkChuck (YouTube & Courses) -

    - Major Influence - Networking fundamentals
    Linux systems and servers
    Cybersecurity concepts
    Infrastructure, cloud, and real-world IT workflows

    NetworkChuck has been a major influence in shaping my understanding of modern IT, infrastructure, and security.
    -
    -
  • - -
-
-
-
-
-

Contact Info

-
    -
  • -
    - -
    -
    -

    Email Me

    - info@tdvorak.dev -
    -
  • -
  • -
    - -
    -
    -

    Tax ID

    - 24330621 -
    -
  • -
  • -
    - -
    -
    -

    Data Box

    - zhecn8k -
    -
  • -
-

Social Info

- -
- - -
-
- - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
-
-
-
-
+ + + +
+ +
+ + +
+
+

// Experience

+

Where I've worked

+

Full history on LinkedIn.

+ +
    +
  1. +
    Sep 2026 — Present
    +
    +

    Head of Product Development · Výdoz

    +

    Freelance · Kvasice · Hybrid

    +

    Leading product development and technology direction.

    +
    +
  2. +
  3. +
    Jun 2026 — Present
    +
    +

    Software Engineer, DevOps · MyBox Charging Stations — ELEXIM a.s.

    +

    Contract · Kroměříž · Hybrid

    +

    Software solutions for the EV charging industry — internal applications and tools, DevOps workflow improvements, automation of development and deployment. Development and testing of charging-station software including ISO 15118 features.

    +
    +
  4. +
  5. +
    Jun 2025 — Present
    +
    +

    Founder · Vibly

    +

    Uherské Hradiště · Hybrid

    +

    AI-assisted full-stack web solutions and digital systems — product logic, architecture and automation workflows.

    +
    +
  6. +
  7. +
    Apr 2025 — Present
    +
    +

    Co-Founder & CTO · SportCreative

    +

    Uherské Hradiště · Hybrid

    +

    Technical and creative strategy of a marketing agency for sports clubs — websites, social systems, content and partner communication.

    +
    +
  8. +
  9. +
    Jun 2025 — Present
    +
    +

    Co-Founder & CTO · ReKlik

    +

    Uherské Hradiště · On-site

    +

    Technological development and visual quality for modern, affordable real-estate photography — from shoot to delivery.

    +
    +
  10. +
  11. +
    Oct 2024 — Present
    +
    +

    Full-Stack Developer · FC Bizoni UH

    +

    Freelance · Remote

    +

    Club website, UX and fan engagement — plus YouTube/TikTok channels, graphics and match photography.

    +
    +
  12. +
  13. +
    May 2025 — Dec 2025
    +
    +

    IT Assistant → Full-Stack Developer · Poppe + Potthoff

    +

    Internship → Contract · Kunovice · On-site

    +

    Internal web portal and systems in Go, HTML5 and Tailwind. IT infrastructure, networking, Proxmox virtualization, Cloudflare security and performance.

    +
    +
  14. +
  15. +
    May 2025 — Dec 2025
    +
    +

    Web Developer & Marketing Lead · Zeus Sport

    +

    Freelance · Remote

    +

    Custom websites and e-commerce (HTML5, Tailwind, JS) during Czech market entry — plus marketing strategy, social media and content.

    +
    +
  16. +
  17. +
    2024 — 2025
    +
    +

    Marketing roles · Révvi Sport, Winnersport, Masita

    +

    Freelance · Remote

    +

    Brand identity, websites, social media, graphic design and campaigns for sports brands.

    +
    +
  18. +
+ + +
+

// Stack

+
+
+
Languages
+

Go · TypeScript · JavaScript · Kotlin · Python · Rust · SQL · HTML/CSS

+
+
+
Frameworks
+

React · Next.js · Tailwind CSS · Vite · Astro · Node.js · Tauri

+
+
+
Infra & DevOps
+

Docker · Proxmox · Linux · Cloudflare · PostgreSQL · Redis · SQLite · GitHub Actions · Vercel · Railway

+
+
+
Tools & Creative
+

Git · GitHub · Gitea · AI-assisted development (Devin, Windsurf) · Photoshop · Lightroom · Illustrator · DaVinci Resolve

+
+
+
Domains
+

EV charging (ISO 15118) · EDI systems · Web scraping & automation · Real-time systems · Self-hosting

+
+
+
+ + +
+

// Education

+
+
+

SGaSOŠ Kunovice — Information Technology

Secondary technical school · 2024 — 2027 · Year 4

+
+
+

Zero To Mastery Academy

Computer & information sciences · 2023 — present

+
+
+

freeCodeCamp

Computer & information sciences · 2023 — present

+
+
+

OAUH — Econometrics & Quantitative Economics

2023 — 2024

+
+
+
+
+
+ + +
+
+

// About

+

+ I work at the intersection of web systems, AI-assisted development and digital infrastructure — + taking products from idea to deployment. I understand modern systems end-to-end and use AI tools + to ship reliable, maintainable software fast. Off the keyboard I'm behind a camera — + photography, cinematography and content for sports clubs. +

+
+
+ + +
+
+

// Contact

+

Get in touch

+ +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

+
+
+
+
- -
- + + diff --git a/script.js b/script.js index dae8ac1..f88431c 100644 --- a/script.js +++ b/script.js @@ -1,226 +1,65 @@ "use strict"; -const $HTML = document.documentElement; +document.documentElement.classList.add("js"); -if (sessionStorage.getItem("theme")) { - $HTML.dataset.theme = sessionStorage.getItem("theme"); -} else { - $HTML.dataset.theme = "dark"; // Set the default theme to dark -} +// Theme: persisted in sessionStorage, dark default. +const root = document.documentElement; +root.dataset.theme = sessionStorage.getItem("theme") || "dark"; -// ===== Resume timeline duration auto-update ===== -function parseYearMonth(value) { - // value format: 'YYYY-MM' or 'present' - if (!value || value === 'present') return null; - const [y, m] = value.split('-').map(Number); - return { year: y, month: m }; -} +document.querySelector("[data-theme-btn]")?.addEventListener("click", () => { + root.dataset.theme = root.dataset.theme === "dark" ? "light" : "dark"; + sessionStorage.setItem("theme", root.dataset.theme); +}); -function monthDiff(start, end) { - // start/end: {year, month} - return (end.year - start.year) * 12 + (end.month - start.month); -} +// Footer year +document.getElementById("year").textContent = new Date().getFullYear(); -function toYearMonth(date) { - return { year: date.getFullYear(), month: date.getMonth() + 1 }; -} - -function humanizeMonthsEN(totalMonths) { - if (totalMonths < 0) return ''; - const years = Math.floor(totalMonths / 12); - const months = totalMonths % 12; - - const yearStr = years === 0 ? '' : years === 1 ? '1 year' : `${years} years`; - const monthStr = months === 0 ? '' : months === 1 ? '1 month' : `${months} months`; - - if (yearStr && monthStr) return `${yearStr} ${monthStr}`; - return yearStr || monthStr || '0 months'; -} - -function updateDurations() { - const nowYM = toYearMonth(new Date()); - document.querySelectorAll('.duration').forEach(el => { - const startAttr = el.getAttribute('data-start'); - const endAttr = el.getAttribute('data-end'); - const startYM = parseYearMonth(startAttr); - const endYM = endAttr === 'present' ? nowYM : parseYearMonth(endAttr); - if (!startYM || !endYM) return; - const diff = monthDiff(startYM, endYM); - el.textContent = humanizeMonthsEN(diff); +// Card spotlight: track cursor into --mx/--my +for (const card of document.querySelectorAll(".card")) { + card.addEventListener("mousemove", (e) => { + const r = card.getBoundingClientRect(); + card.style.setProperty("--mx", `${e.clientX - r.left}px`); + card.style.setProperty("--my", `${e.clientY - r.top}px`); }); } -// Update footer copyright with current year -function updateFooterCopyright() { - const currentYear = new Date().getFullYear(); - const footerText = document.querySelector('.footer p.body-medium'); - if (footerText) { - footerText.innerHTML = `Copyright ${currentYear}. All rights reserved by TDvorak.`; +// Staggered reveal on scroll +const reveal = new IntersectionObserver((entries) => { + for (const en of entries) { + if (en.isIntersecting) { + const cards = [...en.target.parentElement.children]; + en.target.style.transitionDelay = `${cards.indexOf(en.target) * 60}ms`; + en.target.classList.add("in"); + reveal.unobserve(en.target); + } } -} +}, { threshold: 0.15 }); +document.querySelectorAll(".card").forEach((c) => reveal.observe(c)); -document.addEventListener('DOMContentLoaded', () => { - updateDurations(); - updateFooterCopyright(); - // refresh once a day to keep counters accurate as months roll - setInterval(updateDurations, 24 * 60 * 60 * 1000); -}); +// Contact form -> sendmail.tdvorak.dev +const form = document.getElementById("contact-form"); +const status = document.getElementById("form-status"); -const changeTheme = () => { - $HTML.dataset.theme = sessionStorage.getItem("theme") === "dark"; - sessionStorage.setItem("theme", $HTML.dataset.theme); -}; +form?.addEventListener("submit", async (e) => { + e.preventDefault(); + const btn = form.querySelector('button[type="submit"]'); + btn.disabled = true; + btn.textContent = "Sending…"; + status.textContent = ""; + status.className = "form-status mono"; -const $tabBtn = document.querySelectorAll("[data-tab-btn]"); -let [lastActiveTab] = document.querySelectorAll("[data-tab-content]"); -let [lastActiveTabBtn] = $tabBtn; - -$tabBtn.forEach(item => { - item.addEventListener("click", function () { - lastActiveTab.classList.remove("active"); - lastActiveTabBtn.classList.remove("active"); - - const $tabContent = document.querySelector(`[data-tab-content="${item.dataset.tabBtn}"]`); - $tabContent.classList.add("active"); - this.classList.add("active"); - - lastActiveTab = $tabContent; - lastActiveTabBtn = this; - }); -}); - -const cursor = document.getElementById('cursor'); -cursor.style.position = 'fixed'; // Add 'position: fixed' to ensure absolute positioning - -let cursorX = 0, - cursorY = 0; - -const isTouchDevice = () => { try { - document.createEvent('TouchEvent'); - return true; - } catch (e) { - return false; - } -}; - -const move = (e) => { - if (!isTouchDevice()) { - cursorX = e.clientX; - cursorY = e.clientY; - cursor.style.left = `${cursorX}px`; - cursor.style.top = `${cursorY}px`; - } else { - // Disable custom cursor on touch devices - cursor.style.display = "none"; - return; - } - - cursor.style.zIndex = "9999"; - cursor.style.display = "block"; -}; - -document.addEventListener('mouseenter', (e) => { - cursor.style.zIndex = "9999"; - move(e); -}); - -document.addEventListener('mouseleave', () => { - if (isTouchDevice()) { - cursor.style.display = "none"; - } -}); - -document.addEventListener('mousemove', (e) => { - move(e); -}); - -document.addEventListener('touchmove', (e) => { - move(e); - cursor.style.display = "none"; -}); - -document.addEventListener('touchend', () => { - cursor.style.display = "none"; -}); - -document.addEventListener('touchstart', (e) => { - cursor.style.display = "none"; // Hide the cursor on touchstart - const scrollX = e.touches[0].pageX; - const scrollY = e.touches[0].pageY; - move({ clientX: scrollX, clientY: scrollY }); -}); - -// animate border -const borderAnimation = () => { - requestAnimationFrame(borderAnimation); -}; - -requestAnimationFrame(borderAnimation); - -// handle scroll event -document.addEventListener('scroll', (e) => { - const scrollX = e.pageX; - const scrollY = e.pageY; - move({ clientX: scrollX, clientY: scrollY }); -}); - -function sendEmail() { - emailjs.send('service_j9prncb', 'template_drg2vil', { - name: document.getElementById('name').value, - email: document.getElementById('email').value, - subject: document.getElementById('subject').value, - message: document.getElementById('message').value - }) - .then(function(response) { - console.log('SUCCESS!', response.status, response.text); - }, function(error) { - console.log('FAILED...', error); - }); -} - -// ===== Contact form submission ===== -document.addEventListener('DOMContentLoaded', () => { - - // Contact form submission - const form = document.getElementById('contact-form'); - if (form) { - form.addEventListener('submit', async (e) => { - e.preventDefault(); - - const submitButton = form.querySelector('button[type="submit"]'); - const originalButtonText = submitButton.innerHTML; - - try { - if (submitButton) { - submitButton.disabled = true; - submitButton.innerHTML = 'Odesílám...'; - } - - const data = Object.fromEntries(new FormData(form)); - const params = new URLSearchParams(data); - // Preserve existing query string too - const existing = window.location.search.replace(/^\?/, ''); - const url = 'https://sendmail.tdvorak.dev/send' + (existing ? ('?' + existing + '&' + params.toString()) : ('?' + params.toString())); - - const res = await fetch(url, { method: 'GET' }); - const text = await res.text(); - - if (res.ok) { - alert('Děkujeme za vaši zprávu! Brzy se vám ozveme zpět.'); - form.reset(); - } else { - throw new Error(text || 'Něco se pokazilo. Zkuste to prosím znovu.'); - } - } catch (error) { - console.error('Error:', error); - alert(error.message || 'Při odesílání zprávy došlo k chybě. Zkuste to prosím znovu později.'); - } finally { - if (submitButton) { - submitButton.disabled = false; - submitButton.innerHTML = originalButtonText; - } - } - }); + const params = new URLSearchParams(Object.fromEntries(new FormData(form))); + const res = await fetch(`https://sendmail.tdvorak.dev/send?${params}`, { method: "GET" }); + if (!res.ok) throw new Error(await res.text() || "Send failed"); + status.textContent = "Message sent. I'll get back to you soon."; + status.classList.add("ok"); + form.reset(); + } catch (err) { + status.textContent = "Something went wrong — try again or email me directly."; + status.classList.add("err"); + } finally { + btn.disabled = false; + btn.textContent = "Send message"; } }); diff --git a/style.css b/style.css index 6ed9e84..68adbed 100644 --- a/style.css +++ b/style.css @@ -1,1233 +1,330 @@ - +/* Tomáš Dvořák — portfolio. Vercel-style: monochrome, hairline borders, Geist. */ :root { - --background-dark: #111111; - --on-background-dark: hsl(0, 0%, 100%); - --surface-dark: #111111; - --on-surface-dark: hsl(0, 0%, 100%); - --on-surface-variant-dark: hsl(0, 0%, 100%); - --surface-container-low-dark: #1d1d1d; - --surface-container-dark: rgb(19, 19, 19); - --surface-bright-dark: hsl(0, 85%, 3%); - --primary-dark: hsl(0, 0%, 100%); - --on-primary-dark: #202020; - --outline-dark: hsl(0, 0%, 100%); - --outline-variant-dark: hsl(0, 0%, 73%); - - --font-primary: 'Open Sans', sans-serif; - --font-secondary: 'Noto Sans', sans-serif; - - --base-font-size: 62.5%; - --display-large: 5.7rem; - --display-medium: 4.5rem; - --display-small: 3.6rem; - --headline-large: 3.2rem; - --headline-medium: 2.8rem; - --headline-small: 2.4rem; - --title-large: 2rem; - --title-medium: 1.6rem; - --title-small: 1.4rem; - --label-large: 1.4rem; - --label-medium: 1.2rem; - --label-small: 1.1rem; - --body-large: 1.6rem; - --body-medium: 1.4rem; - --body-small: 1.2rem; - - --weight-regular: 400; - --weight-medium: 500; - - --radius-extra-small: 4px; - --radius-small: 8px; - --radius-medium: 12px; - --radius-large: 16px; - --radius-full: 500px; - --radius-circle: 50%; - - --elevation-1: 0px 1px 3px 1px #00000026, 0px 1px 2px 0px #0000004d; - - --section-padding: 24px; - --top-bar-height: 64px; - + --font-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: "Geist Mono", ui-monospace, "SF Mono", monospace; + --radius: 8px; + --nav-h: 64px; + --max-w: 1024px; } [data-theme="dark"] { - - color-scheme: dark; - - --background: var(--background-dark); - --on-background: var(--on-background-dark); - --surface: var(--surface-dark); - --on-surface: var(--on-surface-dark); - --on-surface-variant: var(--on-surface-variant-dark); - --surface-container-low: var(--surface-container-low-dark); - --surface-container: var(--surface-container-dark); - --surface-bright: var(--surface-bright-dark); - --primary: var(--primary-dark); - --on-primary: var(--on-primary-dark); - --outline: var(--outline-dark); - --outline-variant: var(--outline-variant-dark); - + --bg: #000; + --bg-raised: #0a0a0a; + --fg: #ededed; + --fg-dim: #a1a1a1; + --fg-faint: #666; + --border: #2e2e2e; + --border-strong: #454545; + --btn-primary-bg: #ededed; + --btn-primary-fg: #000; + --grid-line: rgba(255, 255, 255, 0.06); + --accent: #3b82f6; } -@media (prefers-reduced-motion: no-preference) { - - :root { - --motion-duration-short-1: 100ms; - --motion-easing-linear: cubic-bezier(0, 0, 1, 1); - } - +[data-theme="light"] { + --bg: #fff; + --bg-raised: #fafafa; + --fg: #171717; + --fg-dim: #666; + --fg-faint: #999; + --border: #eaeaea; + --border-strong: #d4d4d4; + --btn-primary-bg: #171717; + --btn-primary-fg: #fff; + --grid-line: rgba(0, 0, 0, 0.06); + --accent: #0070f3; } -*, -*::before, -*::after { - margin: 0; - padding: 0; -} +* { box-sizing: border-box; margin: 0; padding: 0; } -li { list-style: none; } - -a, -img, -span, -input, -button, -textarea { display: block; } - -a { - color: inherit; - text-decoration: none; -} - -img { height: auto; } - -input, -button, -textarea { - background: none; - border: none; - font: inherit; -} - -input, -textarea { width: 100%; } - -button { cursor: pointer; } - -textarea { resize: none; } - -html { - font-family: var(--font-secondary); - font-size: var(--base-font-size); - scroll-behavior: smooth; -} +html { scroll-behavior: smooth; scroll-padding-top: calc(var(--nav-h) + 16px); } body { - background-color: var(--background); - color: var(--on-background); - font-size: var(--body-large); + background: var(--bg); + color: var(--fg); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + transition: background 0.2s, color 0.2s; } -.display-large { - font-family: var(--font-primary); - font-size: var(--display-large); - font-weight: var(--weight-regular); - line-height: 64px; - letter-spacing: -0.25px; +.mono { font-family: var(--font-mono); } +img { display: block; max-width: 100%; } +a { color: inherit; text-decoration: none; } +.container { max-width: var(--max-w); margin: 0 auto; padding: 0 24px; } + +/* ============ Nav ============ */ +.nav { + position: sticky; top: 0; z-index: 50; + height: var(--nav-h); + border-bottom: 1px solid var(--border); + background: color-mix(in srgb, var(--bg) 80%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); } - -.display-medium { - font-family: var(--font-primary); - font-size: var(--display-medium); - font-weight: var(--weight-regular); - line-height: 52px; +.nav-inner { + max-width: var(--max-w); height: 100%; margin: 0 auto; padding: 0 24px; + display: flex; align-items: center; gap: 32px; } - -.display-small { - font-family: var(--font-primary); - font-size: var(--display-small); - font-weight: var(--weight-regular); - line-height: 44px; -} - -.headline-large { - font-family: var(--font-primary); - font-size: var(--headline-large); - font-weight: var(--weight-regular); - line-height: 40px; -} - -.headline-medium { - font-family: var(--font-primary); - font-size: var(--headline-medium); - font-weight: var(--weight-regular); - line-height: 36px; -} - -.headline-small { - font-family: var(--font-primary); - font-size: var(--headline-small); - font-weight: var(--weight-regular); - line-height: 32px; -} - -.title-large { - font-family: var(--font-primary); - font-size: var(--title-large); - font-weight: var(--weight-regular); - line-height: 28px; -} - -.title-medium { - font-family: var(--font-primary); - font-size: var(--title-medium); - font-weight: var(--weight-medium); - line-height: 24px; - letter-spacing: 0.15px; -} - -.title-small { - font-family: var(--font-primary); - font-size: var(--title-small); - font-weight: var(--weight-medium); - line-height: 20px; - letter-spacing: 0.1px; -} - -.body-large { - font-size: var(--body-large); - font-weight: var(--weight-regular); - line-height: 24px; - letter-spacing: 0.5px; -} - -.body-medium { - font-size: var(--body-medium); - font-weight: var(--weight-regular); - line-height: 20px; - letter-spacing: 0.25px; -} - -.body-small { - font-size: var(--body-small); - font-weight: var(--weight-regular); - line-height: 16px; - letter-spacing: 0.4px; -} - -.label-large { - font-size: var(--label-large); - font-weight: var(--weight-medium); - line-height: 20px; - letter-spacing: 0.1px; -} - -.label-medium { - font-size: var(--label-medium); - font-weight: var(--weight-medium); - line-height: 16px; - letter-spacing: 0.5px; -} - -.label-small { - font-size: var(--label-small); - font-weight: var(--weight-medium); - line-height: 16px; - letter-spacing: 0.5px; -} - -.material-symbols-outlined { - width: 1em; - height: 1em; - display: block; - font-variation-settings: - 'FILL' 0, - 'wght' 400, - 'GRAD' 0, - 'opsz' 24; -} - -:has(> .state-layer) { - position: relative; - overflow: hidden; - z-index: 1; -} - -.state-layer { - position: absolute; - inset: 0; - background-color: var(--state-layer-bg); - transition: var(--motion-duration-short-1) var(--motion-easing-linear); - opacity: 0; -} - -:has(> .state-layer):hover .state-layer { opacity: 0.08; } - -:has(> .state-layer):is(:focus, :focus-visible, :active) .state-layer { - opacity: 0.12; -} - -.section { padding-block: var(--section-padding); } - -.container { padding-inline: 16px; } - -.img-holder { - aspect-ratio: var(--width) / var(--height); - background-color: transparent; - overflow: hidden; -} - -.img-cover { - width: 100%; - height: 100%; - object-fit: cover; -} - -.tab-content:not(.active) { display: none; } - -.section-title { margin-block-end: 16px; font-size: medium;} - -.icon-btn { - width: 40px; - height: 40px; - display: grid; - place-items: center; -} - -.btn { - height: 40px; - display: grid; - place-items: center; - padding-inline: 24px; - border-radius: var(--radius-full); - background-color: var(--primary); - color: var(--on-primary); - font-weight: var(--weight-medium); - overflow: hidden; - --state-layer-bg: var(--on-primary); -} - -.btn:hover:not(:focus) { box-shadow: var(--elevation-1); } - -.primary-tabs { - background-color: var(--background); - margin-block-start: 24px; - margin-inline: 16px; - display: grid; - grid-template-columns: repeat(3, 1fr); - justify-content: center; - border-block-end: 1px solid var(--outline-variant); -} - - -.tab-btn { - height: 48px; - width: 100%; - display: grid; - place-content: center; - background-color: var(--surface); - color: var(--on-surface-variant); - --state-layer-bg: var(--on-surface-variant); -} - -.tab-btn:first-child{ - border-top-left-radius: 12px; -} - -.tab-btn:last-child{ - border-top-right-radius: 12px; -} - -.tab-btn.active { - color: var(--primary); - --state-layer-bg: var(--primary); -} - -.tab-text { - position: relative; - height: 48px; - line-height: 48px; -} - -.tab-text::after { - content: ""; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 3px; - border-radius: 3px 3px 0 0; - background-color: var(--primary); - opacity: 0; -} - -.tab-btn.active .tab-text::after { opacity: 1; } - -.chip { - height: 32px; - max-width: max-content; - display: flex; - justify-content: center; - align-items: center; - gap: 8px; - padding-inline: 16px; - border: 1px solid var(--outline); - border-radius: var(--radius-small); - background-color: var(--surface); - color: var(--on-surface-variant); - --state-layer-bg: var(--on-surface-variant); -} - -.chip .material-symbols-outlined { font-size: 1.8rem; } - -.card { - background-color: #1d1d1d; - border-radius: var(--radius-medium); - padding: 16px; - display: grid; - gap: 16px; - --state-layer-bg: var(--on-surface); - position: relative; - border: 2px solid transparent; - transition: all 0.3s ease; -} - -.card:hover { - border-style: dashed solid; - border-color: var(--outline-variant); -} - -.card-banner { border-radius: var(--radius-medium); } - -/* Year badge styling for project cards */ -.card::before { - content: attr(data-year); - position: absolute; - top: 8px; - left: 50%; - transform: translateX(-50%); - background-color: white; - color: #333; - padding: 4px 8px; - border-radius: var(--radius-full); - font-size: 0.75rem; - font-weight: 600; - z-index: 2; - opacity: 0; - transition: all 0.3s ease; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - -.card:hover::before { - opacity: 1; -} - -.card-content { - display: grid; - gap: 4px; -} - -.card-subtitle { color: var(--on-surface-variant); } - -.card-title { color: var(--on-surface); } - -.input-field, -.textarea { - color: var(--on-surface); - padding: 13px 16px; - box-shadow: inset 0 0 0 1px var(--outline); - border-radius: var(--radius-extra-small); - outline: none; - transition: box-shadow var(--motion-duration-short-1) var(--motion-easing-linear); -} - -.textarea { - height: 120px; - padding: 16px; -} - -:where(.input-field, .textarea)::placeholder { color: var(--on-surface-variant); } - -:where(.input-field, .textarea):focus { box-shadow: inset 0 0 0 2px var(--primary); } - -.topbar { - background-color: var(--background); - position: fixed; - top: 0; - left: 0; - width: 100%; - height: var(--top-bar-height); - display: flex; - justify-content: space-between; - align-items: center; - padding-inline: 16px; - z-index: 4; -} - -[data-theme="dark"] .logo .light, -[data-theme="light"] .logo .dark { display: none; } - -[data-theme="dark"] .logo .dark, -[data-theme="light"] .logo .light { display: block; } - +.nav-logo img { height: 20px; width: auto; } +[data-theme="dark"] .nav-logo img { filter: invert(1); } +.nav-links { display: flex; gap: 24px; flex: 1; } +.nav-links a { font-size: 14px; color: var(--fg-dim); transition: color 0.15s; } +.nav-links a:hover { color: var(--fg); } +.nav-actions { display: flex; align-items: center; gap: 12px; } .theme-btn { - color: var(--on-surface-variant); - --state-layer-bg: var(--on-surface-variant); - border-radius: var(--radius-full); - overflow: hidden; + display: grid; place-items: center; + width: 32px; height: 32px; + background: none; border: 1px solid var(--border); border-radius: var(--radius); + color: var(--fg-dim); cursor: pointer; transition: color 0.15s, border-color 0.15s; } +.theme-btn:hover { color: var(--fg); border-color: var(--border-strong); } +[data-theme="dark"] .icon-moon { display: none; } +[data-theme="light"] .icon-sun { display: none; } -[data-theme="dark"] .theme-btn .dark, -[data-theme="light"] .theme-btn .light { display: none; } - -.hero { padding-block-start: calc(var(--top-bar-height) + 24px); } - -.hero .container { - display: grid; - align-items: center; - gap: 24px; +/* ============ Buttons ============ */ +.btn { + display: inline-flex; align-items: center; gap: 6px; + padding: 0 16px; height: 40px; + font-family: var(--font-sans); font-size: 14px; font-weight: 500; + color: var(--fg); background: var(--bg); + border: 1px solid var(--border-strong); border-radius: var(--radius); + cursor: pointer; transition: border-color 0.15s, background 0.15s; } +.btn:hover { border-color: var(--fg-dim); background: var(--bg-raised); } +.btn .arrow { transition: transform 0.15s; } +.btn:hover .arrow { transform: translate(1px, -1px); } +.btn-sm { height: 32px; padding: 0 12px; font-size: 13px; } +.btn-primary { background: var(--btn-primary-bg); color: var(--btn-primary-fg); border-color: var(--btn-primary-bg); } +.btn-primary:hover { background: var(--btn-primary-bg); opacity: 0.88; border-color: var(--btn-primary-bg); } -.hero-banner { - width: 240px; - border-radius: var(--radius-circle); +/* ============ Hero ============ */ +.hero { position: relative; padding: 128px 0 96px; overflow: hidden; } +.grid-bg { + position: absolute; inset: 0; pointer-events: none; + background-image: + linear-gradient(var(--grid-line) 1px, transparent 1px), + linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); + background-size: 64px 64px; + mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 75%); + -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 75%); } - -.hero-content { max-width: 500px; } - -.hero .section-subtitle { color: var(--on-surface-variant); } - -.hero .section-text { - margin-block: 5px 16px; - color: var(--on-surface-variant); +.hero .container { position: relative; } +.status-pill { + display: inline-flex; align-items: center; gap: 8px; + padding: 6px 12px; margin-bottom: 32px; + font-size: 13px; color: var(--fg-dim); + border: 1px solid var(--border); border-radius: 999px; + background: var(--bg-raised); } - -.hero .btn-wrapper { - display: flex; - flex-wrap: wrap; - gap: 8px; +.status-dot { + width: 7px; height: 7px; border-radius: 50%; + background: #22c55e; + box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18); } - -.about-card { - background-color: var(--surface-container-low); - color: var(--on-surface-variant); - margin-inline: 16px; - padding: 16px; - margin-block-start: 24px; - border-radius: var(--radius-large); +.hero-title { + font-size: clamp(44px, 8vw, 80px); + font-weight: 700; letter-spacing: -0.04em; line-height: 1.05; } - -.about-card .card-title { color: var(--on-surface); } - -.about-card .card-text { - margin-block: 12px; - padding-block: 12px; - border-block: 1px solid var(--outline-variant); +.hero-sub { + margin-top: 16px; + font-size: clamp(18px, 3vw, 24px); font-weight: 500; + color: var(--fg-dim); letter-spacing: -0.02em; } +.hero-desc { margin-top: 24px; max-width: 560px; color: var(--fg-dim); font-size: 16px; } +.hero-desc strong { color: var(--fg); font-weight: 500; } +.hero-cta { display: flex; gap: 12px; margin-top: 32px; flex-wrap: wrap; } +.hero-meta { margin-top: 40px; font-size: 13px; color: var(--fg-faint); } -.about-list { - display: grid; - gap: 12px; +/* ============ Sections ============ */ +.section { padding: 80px 0; border-top: 1px solid var(--border); } +.label { font-size: 12px; color: var(--fg-faint); text-transform: uppercase; letter-spacing: 0.08em; } +.section-title { margin-top: 12px; font-size: 32px; font-weight: 600; letter-spacing: -0.03em; } +.section-desc { margin-top: 8px; color: var(--fg-dim); font-size: 15px; } +.inline-link { color: var(--fg); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--border-strong); } +.inline-link:hover { text-decoration-color: var(--fg); } + +/* ============ Cards ============ */ +.card-grid { + display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; + margin-top: 40px; } - -.list-item { - display: flex; - align-items: center; - gap: 4px; -} - -.project{ - border-top-left-radius: 10px; -} - -.project-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 12px; -} - -.resume-tab .list { - margin-block-end: 28px; -} - -.resume-card { - display: grid; - gap: 4px; -} - -.resume-card .card-subtitle { color: var(--primary); } - -.resume-card .card-text { color: var(--on-surface-variant); } - -.resume-bottom-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 12px; -} - -.resume-bottom-item { - background-color: var(--surface-container); - display: flex; - align-items: center; - gap: 8px; - padding: 12px; - border-radius: var(--radius-small); +.card { + --glow: rgba(255, 255, 255, 0.35); position: relative; - border: 2px solid transparent; - transition: all 0.3s ease; + display: flex; flex-direction: column; + border: 1px solid var(--border); border-radius: var(--radius); + background: var(--bg-raised); overflow: hidden; + transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; } - -.resume-bottom-item:hover { - border-style: dashed solid; - border-color: var(--outline-variant); +.js .card { opacity: 0; transform: translateY(14px); } +[data-theme="light"] .card { --glow: rgba(0, 0, 0, 0.16); } +.card.in { opacity: 1; transform: none; transition: opacity 0.5s ease, transform 0.5s ease, border-color 0.2s, box-shadow 0.2s; } +.card:hover { border-color: var(--border-strong); transform: translateY(-3px); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35); } +[data-theme="light"] .card:hover { box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1); } +/* mouse-tracked border glow */ +.card::before { + content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; + border-radius: var(--radius); padding: 1px; + background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%), var(--glow), transparent 70%); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; mask-composite: exclude; + opacity: 0; transition: opacity 0.3s; } - -.resume-bottom-item .icon { height: 28px; } - -/* Year badge styling for skill items */ -.resume-bottom-item::before { - content: attr(data-year); - position: absolute; - top: -10px; - right: 8px; - background-color: white; - color: #333; - padding: 4px 8px; - border-radius: var(--radius-full); - font-size: 0.75rem; - font-weight: 600; - z-index: 2; - opacity: 0; - transform: translateY(5px); - transition: all 0.3s ease; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +/* soft inner spotlight */ +.card::after { + content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none; + background: radial-gradient(420px circle at var(--mx, 50%) var(--my, 50%), rgba(255, 255, 255, 0.05), transparent 70%); + opacity: 0; transition: opacity 0.3s; } - -.resume-bottom-item:hover::before { - opacity: 1; - transform: translateY(0); +[data-theme="light"] .card::after { background: radial-gradient(420px circle at var(--mx, 50%) var(--my, 50%), rgba(0, 0, 0, 0.04), transparent 70%); } +.card:hover::before, .card:hover::after { opacity: 1; } +.card > * { position: relative; z-index: 1; } +.card figure { position: relative; aspect-ratio: 16/9; overflow: hidden; border-bottom: 1px solid var(--border); } +.card figure img { + width: 100%; height: 100%; object-fit: cover; object-position: top; + filter: saturate(0.9); transition: transform 0.5s ease, filter 0.5s ease; } - -/* Hide hover effects for In Progress section */ -.resume-tab h2:nth-of-type(4) + .resume-bottom-list .resume-bottom-item { - border: 2px solid transparent !important; +.card:hover figure img { transform: scale(1.03); filter: saturate(1.05); } +/* ↗ badge over the image */ +.card figure::after { + content: "↗"; position: absolute; top: 10px; right: 10px; + display: grid; place-items: center; width: 26px; height: 26px; + font-family: var(--font-mono); font-size: 13px; color: var(--fg); + background: color-mix(in srgb, var(--bg) 75%, transparent); + border: 1px solid var(--border-strong); border-radius: 999px; + backdrop-filter: blur(8px); + opacity: 0; transform: translate(-6px, 6px); + transition: opacity 0.2s, transform 0.2s; } - -.resume-tab h2:nth-of-type(4) + .resume-bottom-list .resume-bottom-item::before { - display: none !important; -} - -/* Resume Timeline */ -.resume-tab .container { align-items: center; } - -.resume-header { - display: flex; - justify-content: center; - margin: 8px 0 20px; -} - -.resume-linkedin-btn { - border: 1px solid var(--outline); - padding: 10px 18px; - border-radius: var(--radius-full); - background: var(--surface); - color: var(--on-surface); -} - -.timeline { - position: relative; - max-width: 780px; - width: 100%; - margin-inline: auto; -} - -.timeline::before { - content: ""; - position: absolute; - right: 0; - top: 0; - bottom: 0; - width: 2px; - background: var(--outline-variant); - opacity: 0.6; -} - -.timeline::after { - content: ""; - display: table; - clear: both; -} - -.timeline-item { - position: relative; - margin: 0 0 28px 0; - padding-right: 44px; /* space for dots/line on the right */ -} - -.timeline-item::before { - content: ""; - position: absolute; - right: -6px; - top: 18px; - width: 12px; - height: 12px; - border-radius: 50%; - background: var(--primary); - box-shadow: 0 0 0 3px rgba(255,255,255,0.08); - transform: translate(0, -50%); - z-index: 2; -} - -.timeline .resume-card { background: transparent; padding: 14px 0; border-radius: 0; margin: 0; max-width: none; } -.timeline .resume-card .body-large { font-weight: 600; } -.timeline .resume-card .card-subtitle { color: var(--on-surface-variant); opacity: 0.9; } -.timeline .resume-card .card-text { color: var(--on-surface-variant); opacity: 0.9; } - -/* Single-column layout on all sizes; no alternating */ - -/* Narrow screens fine-tuning */ -@media (max-width: 480px) { - .timeline-item { padding-right: 28px; } - .timeline-item::before { right: -5px; top: 16px; } -} - -.contact-tab { padding-block-end: 0; } - -.contact-tab .container { padding-block-end: 24px; } - -.contact-info, -.contact-info-list { - display: grid; - gap: 12px; -} - -.contact-info { margin-block-end: 24px; } - -.contact-info-list { - padding-block-end: 12px; - border-block-end: 1px solid var(--outline-variant); -} - -.contact-info-item { - display: flex; - align-items: center; - gap: 12px; -} - -.contact-info .icon-box { - width: 48px; - height: 48px; +.card:hover figure::after { opacity: 1; transform: none; } +.card-body { padding: 20px; } +.card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.card-head h3 { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; } +.tag { flex-shrink: 0; - display: grid; - place-items: center; - color: var(--on-surface-variant); + font-family: var(--font-mono); font-size: 11px; color: var(--fg-dim); + padding: 3px 8px; border: 1px solid var(--border); border-radius: 999px; +} +/* brand accent tags — color sampled from each product's own UI */ +.tag-lime { --acc: #a3e635; } /* containr */ +.tag-nothing { --acc: #f04e55; } /* nothing red */ +.tag-cal { --acc: #7da88b; } /* sage */ +.tag-pulse { --acc: #5eb3ff; } /* monitor blue */ +.tag-bookra { --acc: #d8b98a; } /* warm tan */ +.tag-vibly { --acc: #b8ff2e; } /* chartreuse */ +.tag-silver { --acc: #c9cdd3; } /* timespent mono */ +.tag-sportcreative { --acc: #9a7ae0; } /* purple */ +.tag-zeus { --acc: #e05a52; } /* red */ +.tag-reklik { --acc: #58aed8; } /* blue */ +.tag-bizoni { --acc: #c05b50; } /* maroon */ +.tag[class*="tag-"] { color: var(--acc); border-color: color-mix(in srgb, var(--acc) 35%, transparent); } +[data-theme="light"] .tag[class*="tag-"] { color: color-mix(in srgb, var(--acc) 45%, #000); border-color: color-mix(in srgb, var(--acc) 50%, transparent); } +.card-body > p { margin-top: 8px; font-size: 14px; color: var(--fg-dim); line-height: 1.55; } + +/* ============ Row lists ============ */ +.row-list { margin-top: 40px; border-top: 1px solid var(--border); } +.row { + display: flex; align-items: center; gap: 16px; + padding: 16px 8px; + border-bottom: 1px solid var(--border); + transition: background 0.15s; +} +.row:not(.row-static):hover { background: var(--bg-raised); } +.row-main { flex: 1; min-width: 0; } +.row-main h4 { font-size: 15px; font-weight: 500; display: inline; } +.row-main p { font-size: 14px; color: var(--fg-dim); margin-top: 2px; } +.row-tag { font-size: 11px; color: var(--fg-faint); white-space: nowrap; } +.row .arrow { color: var(--fg-faint); flex-shrink: 0; transition: transform 0.15s, color 0.15s; } +.row:hover .arrow { transform: translate(1px, -1px); color: var(--fg); } +.row-static { cursor: default; } +.sub-label { margin-top: 48px; } +.row-list-tight { margin-top: 16px; } + +/* ============ Timeline ============ */ +.timeline { margin-top: 40px; list-style: none; } +.tl-item { + display: grid; grid-template-columns: 180px 1fr; gap: 24px; + padding: 24px 0; border-bottom: 1px solid var(--border); +} +.tl-item:first-child { padding-top: 0; } +.tl-when { font-size: 13px; color: var(--fg-faint); padding-top: 3px; } +.tl-body h4 { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; } +.tl-meta { font-size: 13px; color: var(--fg-faint); margin-top: 2px; } +.tl-body > p:last-child { margin-top: 8px; font-size: 14px; color: var(--fg-dim); } + +/* ============ Stack ============ */ +.stack { margin-top: 64px; } +.stack-grid { + display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px 24px; + margin-top: 24px; +} +.stack-grid h5 { font-size: 12px; color: var(--fg-faint); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500; } +.stack-grid p { margin-top: 8px; font-size: 14px; color: var(--fg-dim); line-height: 1.7; } + +.edu { margin-top: 64px; } + +/* ============ About ============ */ +.about-text { + margin-top: 20px; max-width: 720px; + font-size: clamp(18px, 2.5vw, 22px); line-height: 1.6; + color: var(--fg-dim); letter-spacing: -0.01em; } -.info-content { - display: grid; - gap: 4px; +/* ============ Contact ============ */ +.contact-grid { + display: grid; grid-template-columns: 1fr 1fr; gap: 48px; + margin-top: 40px; +} +.contact-info { border-top: 1px solid var(--border); } +.contact-row { + display: flex; justify-content: space-between; align-items: baseline; gap: 16px; + padding: 14px 0; border-bottom: 1px solid var(--border); +} +a.contact-row:hover .contact-val { color: var(--fg); } +.contact-key { font-size: 12px; color: var(--fg-faint); text-transform: uppercase; letter-spacing: 0.08em; } +.contact-val { font-size: 14px; color: var(--fg-dim); text-align: right; transition: color 0.15s; } + +.contact-form { display: flex; flex-direction: column; gap: 16px; } +.field label { display: block; font-size: 12px; color: var(--fg-faint); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px; } +.field input, .field textarea { + width: 100%; padding: 10px 12px; + font-family: var(--font-sans); font-size: 14px; + color: var(--fg); background: var(--bg-raised); + border: 1px solid var(--border); border-radius: var(--radius); + transition: border-color 0.15s; +} +.field input:focus, .field textarea:focus { outline: none; border-color: var(--fg-dim); } +.field input::placeholder, .field textarea::placeholder { color: var(--fg-faint); } +.field textarea { resize: vertical; } +.contact-form .btn-primary { align-self: flex-start; } +.form-status { font-size: 13px; color: var(--fg-dim); min-height: 18px; } +.form-status.ok { color: #22c55e; } +.form-status.err { color: #ef4444; } + +/* ============ Footer ============ */ +.footer { border-top: 1px solid var(--border); padding: 32px 0; } +.footer-inner { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; } +.footer p { font-size: 13px; color: var(--fg-faint); } + +/* ============ Responsive ============ */ +@media (max-width: 900px) { + .card-grid { grid-template-columns: repeat(2, 1fr); } + .stack-grid { grid-template-columns: repeat(2, 1fr); } + .contact-grid { grid-template-columns: 1fr; gap: 40px; } +} +@media (max-width: 640px) { + .nav-links { display: none; } + .hero { padding: 88px 0 72px; } + .card-grid { grid-template-columns: 1fr; } + .tl-item { grid-template-columns: 1fr; gap: 6px; } + .section { padding: 56px 0; } + .stack-grid { grid-template-columns: 1fr; gap: 24px; } } -.info-title { - color: var(--on-surface-variant); - opacity: 0.6; -} - -.info-text { color: var(--on-surface-variant); } - -.social-list { - display: flex; - gap: 12px; -} - -.social-item { padding: 12px; } - -.input-wrapper { - margin-block-start: 0px; - display: grid; - gap: 10px; -} - -/* Only add padding to the divs with inputs inside the contact form */ -#contact-form > div { - padding: 0px; -} - -.contact-form { - background-color: var(--surface-container); - padding: 0; - margin-block-end: 16px; - border-radius: var(--radius-medium); -} - -.footer .container { - text-align: center; - padding: 16px; - border-block-start: 1px solid var(--outline-variant); -} - -@media (min-width: 768px) { - .article { - position: relative; - display: grid; - grid-template-columns: 0.5fr 1fr; - align-items: flex-start; - } - - .about-card, - .primary-tabs { - position: sticky; - top: 64px; - } - - .primary-tabs { z-index: 2; } - - .hero .container { grid-template-columns: 0.2fr 1fr; } - -} - -@media (min-width: 1200px) { - - .container, - .article { - max-width: 1128px; - margin-inline: auto; - } - - .about-card { margin-inline: 8px; } - - .contact-tab .container { - display: grid; - align-items: flex-start; - grid-template-columns: 0.6fr 1fr; - gap: 24px; - } -} - -#cursor { - position: absolute; - background-color: #fff; - height: 10px; - width: 10px; - border-radius: 50%; - transform: translate(-50%, -50%); - pointer-events: none; - transition: background-color 0.3s ease; -} - -a:hover { - color: rgba(255, 255, 255, 0.616); - font-weight: bold; - transform: scale(1.02); - cursor: pointer; - transition: .8s; -} - -.send-btn { - background-color: #fff; - color: #000; -} -.send-btn:active { - background-color: #333; - color: #fff; -} - -/* Make submit button full-width by default for better mobile UX */ -.btn.send-btn { width: 100%; } - -/* Contact form wrapper responsive sizing */ -.contact-form-wrapper { - width: 100%; - max-width: 740px; - margin: 0px; - padding: 0; - background-color: transparent; - overflow: hidden; /* clip inner focus rings/buttons to the rounded card */ -} - -/* Contact form visual tweaks (dark theme friendly) */ -.contact-form-wrapper .label-medium { - color: var(--on-surface-variant); -} - -.contact-form-wrapper .input-field, -.contact-form-wrapper .textarea { - background-color: var(--surface-container); - box-shadow: none; - border: 1px solid rgba(255, 255, 255, 0.18); - transition: border-color 160ms ease, box-shadow 160ms ease, background-color 160ms ease; - width: 100%; - box-sizing: border-box; -} - -.contact-form-wrapper .input-field::placeholder, -.contact-form-wrapper .textarea::placeholder { - color: rgba(255, 255, 255, 0.6); -} - -.contact-form-wrapper .input-field:focus, -.contact-form-wrapper .textarea:focus { - border-color: var(--primary); - box-shadow: 0 0 0 2px rgba(255,255,255,0.08), inset 0 0 0 1px var(--primary); -} - -.contact-form-wrapper .btn.send-btn { - background-color: #fff; - color: #000; - border-radius: 9999px; - max-width: 100%; - box-sizing: border-box; -} - -.flags { - margin: 10px auto 20px; -} - -.flags img { - cursor: pointer; - display: block; - margin-left: auto; - margin-right: auto; - width: 6%; -} - -.home__button { - width: 90%; - border: 2px white solid; - color: white; - font-size: 14px; - text-align: center; -} - - -.button { - display: inline-block; - color: white; - padding: 1rem 2rem; - border-radius: .5rem; - transition: .8s; - text-align: center; -} - -.button:hover { - background-color: white; - color: black; - font-weight: bolder; - font-size: medium; -} - -.cv{ - margin: auto; - width: 50%; - padding: 10px 0px 40px; -} - -/* Enhanced Modern Resume Styling */ -.resume-tab .section-title { - font-weight: 600; - letter-spacing: -0.02em; - margin-bottom: 32px; - position: relative; - padding-bottom: 12px; -} - -.resume-tab .section-title::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 60px; - height: 3px; - background: linear-gradient(135deg, var(--primary), var(--primary-dark)); - border-radius: 2px; - opacity: 0.8; -} - -.timeline { - position: relative; - max-width: 900px; - width: 100%; - margin-inline: auto; -} - -.timeline::before { - content: ""; - position: absolute; - right: 0; - top: 0; - width: 2px; - height: 100%; - background: linear-gradient(180deg, var(--primary) 0%, transparent 100%); - opacity: 0.3; -} - -.timeline-item { - position: relative; - margin: 0 0 32px 0; - padding-right: 44px; - transition: transform 0.2s ease; -} - -.timeline-item:hover { - transform: translateX(-2px); -} - -.timeline-item::before { - content: ""; - position: absolute; - right: -6px; - top: 8px; - width: 14px; - height: 14px; - background: var(--surface-container); - border: 3px solid var(--primary); - border-radius: 50%; - z-index: 2; - transition: all 0.3s ease; -} - -.timeline-item:hover::before { - transform: scale(1.2); - box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.1); -} - -.timeline .resume-card { - background: var(--surface-container-low); - border: 1px solid var(--outline-variant); - border-radius: var(--radius-medium); - padding: 24px; - margin: 0; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.timeline .resume-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: linear-gradient(180deg, var(--primary), transparent); - opacity: 0; - transition: opacity 0.3s ease; -} - -.timeline .resume-card:hover::before { - opacity: 1; -} - -.timeline .resume-card:hover { - background: var(--surface-container); - border-color: var(--primary); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); - transform: translateY(-2px); -} - -.timeline .resume-card .body-large { - font-weight: 600; - color: var(--on-surface); - margin-bottom: 8px; - line-height: 1.3; -} - -.timeline .resume-card .card-subtitle { - color: var(--primary); - opacity: 0.9; - font-weight: 500; - margin-bottom: 12px; - display: flex; - align-items: center; - gap: 8px; -} - -.timeline .resume-card .card-text { - color: var(--on-surface-variant); - line-height: 1.6; - margin-bottom: 12px; -} - -.timeline .resume-card .card-text br { - content: ''; - display: block; - margin-bottom: 8px; -} - -/* Education Section Enhancements - Different from Work Experience */ -.resume-tab .list:not(.timeline) { - display: grid; - gap: 16px; - max-width: 900px; - margin: 0 auto 32px; -} - -.resume-tab .list:not(.timeline) .resume-item { - background-color: var(--surface-container); - border: none; - border-radius: var(--radius-small); - padding: 20px; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.resume-tab .list:not(.timeline) .resume-item:hover { - background-color: var(--surface-container-low); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - transform: translateY(-1px) scale(1.01); -} - -.resume-tab .list:not(.timeline) .resume-card { - background: transparent; - padding: 0; - border-radius: 0; - margin: 0; - position: relative; - z-index: 1; -} - -.resume-tab .list:not(.timeline) .resume-card .body-large { - font-weight: 700; - color: var(--on-surface); - margin-bottom: 6px; - line-height: 1.2; - font-size: 1.7rem; -} - -.resume-tab .list:not(.timeline) .resume-card .card-subtitle { - color: var(--primary); - opacity: 1; - font-weight: 600; - margin-bottom: 10px; - font-size: 1.1rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.resume-tab .list:not(.timeline) .resume-card .card-text { - color: var(--on-surface-variant); - line-height: 1.5; - font-size: 1.3rem; -} - -.resume-tab .list:not(.timeline) .resume-card .card-text br { - content: ''; - display: block; - margin-bottom: 6px; -} - -/* Responsive improvements */ -.timeline .resume-card { - background: var(--surface-container-low); - border: 1px solid var(--outline-variant); - border-radius: var(--radius-medium); - padding: 24px; - margin: 0; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.timeline .resume-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: linear-gradient(180deg, var(--primary), transparent); - opacity: 0; - transition: opacity 0.3s ease; -} - -.timeline .resume-card:hover::before { - opacity: 1; -} - -.timeline .resume-card:hover { - background: var(--surface-container); - border-color: var(--primary); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); - transform: translateY(-2px); -} - -.timeline .resume-card .body-large { - font-weight: 600; - color: var(--on-surface); - margin-bottom: 8px; - line-height: 1.3; -} - -.timeline .resume-card .card-subtitle { - color: var(--primary); - opacity: 0.9; - font-weight: 500; - margin-bottom: 12px; - display: flex; - align-items: center; - gap: 8px; -} - -.timeline .resume-card .card-text { - color: var(--on-surface-variant); - line-height: 1.6; - margin-bottom: 12px; -} - -.timeline .resume-card .card-text br { - content: ''; - display: block; - margin-bottom: 8px; -} - -/* Responsive improvements */ -@media (max-width: 768px) { - .timeline { - max-width: 100%; - padding: 0 16px; - } - - .timeline-item { - padding-right: 32px; - margin-bottom: 24px; - } - - .timeline .resume-card { - padding: 20px; - } - - .resume-tab .list:not(.timeline) { - padding: 0 16px; - } - - .resume-tab .list:not(.timeline) .resume-item { - padding: 20px; - } -} - -@media (max-width: 480px) { - .timeline-item { - padding-right: 28px; - margin-bottom: 20px; - } - - .timeline-item::before { - right: -5px; - top: 16px; - } - - .timeline .resume-card { - padding: 16px; - } - - .resume-tab .list:not(.timeline) .resume-item { - padding: 16px; - } - - .resume-tab .section-title { - font-size: 1.8rem; - margin-bottom: 24px; - } +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + * { transition: none !important; } }