832 lines
20 KiB
Vue
832 lines
20 KiB
Vue
<script setup lang="ts">
|
|
import { onBeforeUnmount, onMounted } from 'vue'
|
|
import cvPhoto from '@/assets/cv-photo.jpg'
|
|
import CvIcon from '@/components/CvIcon.vue'
|
|
|
|
// This is a public sub-page with no link from the rest of the app, and it
|
|
// shouldn't show up in search results. index.html is shared across every
|
|
// route, so the noindex meta tag has to be added/removed here rather than
|
|
// living statically in the HTML head.
|
|
let robotsMeta: HTMLMetaElement | null = null
|
|
|
|
onMounted(() => {
|
|
robotsMeta = document.createElement('meta')
|
|
robotsMeta.name = 'robots'
|
|
robotsMeta.content = 'noindex, nofollow'
|
|
document.head.appendChild(robotsMeta)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
robotsMeta?.remove()
|
|
})
|
|
|
|
interface Project {
|
|
title: string
|
|
repo: string
|
|
desc: string
|
|
url: string
|
|
}
|
|
|
|
interface ExperienceEntry {
|
|
company: string
|
|
location: string
|
|
title: string
|
|
dates: string
|
|
subtitle: string
|
|
bullets: string[]
|
|
}
|
|
|
|
interface EducationEntry {
|
|
school: string
|
|
dates: string
|
|
degree: string
|
|
}
|
|
|
|
interface Achievement {
|
|
title: string
|
|
desc: string
|
|
}
|
|
|
|
interface Language {
|
|
name: string
|
|
level: string
|
|
rating: number
|
|
}
|
|
|
|
interface Skill {
|
|
name: string
|
|
rating: number
|
|
}
|
|
|
|
interface AdditionalExperience {
|
|
title: string
|
|
desc: string
|
|
}
|
|
|
|
const SKILL_MAX = 8
|
|
const LANGUAGE_MAX = 5
|
|
|
|
const projects: Project[] = [
|
|
{
|
|
title: 'Frontend',
|
|
repo: 'dttmr-fe',
|
|
desc: 'Offline-first PWA for shared, syncable to-do lists and tracking workouts. Vue 3 + TypeScript.',
|
|
url: 'https://git.dittmar.dev/robin/dttmr-fe',
|
|
},
|
|
{
|
|
title: 'API',
|
|
repo: 'dttmr-api',
|
|
desc: 'Go backend powering auth and sync for the to-do list/workout tracking app.',
|
|
url: 'https://git.dittmar.dev/robin/dttmr-api',
|
|
},
|
|
]
|
|
|
|
const experience: ExperienceEntry[] = [
|
|
{
|
|
company: 'Greenbone AG',
|
|
location: 'Remote, Germany',
|
|
title: 'Senior C Software Engineer',
|
|
dates: '08/2025 - Present',
|
|
subtitle: 'Open-Source Vulnerability Management',
|
|
bullets: [
|
|
"Improved vulnerability management for customers and community users by working on open- and closed-source projects like 'gvmd', 'gsad', and 'gsa'.",
|
|
'Maintain old legacy systems in C, as well as building new components in Rust, which align with the ecosystem.',
|
|
'Improve GitHub Actions CI/CD pipelines.',
|
|
],
|
|
},
|
|
{
|
|
company: 'Aspen Technology',
|
|
location: 'Cologne, Germany',
|
|
title: 'Sr Software Developer',
|
|
dates: '04/2025 - 07/2025',
|
|
subtitle: 'Asset Optimization Software',
|
|
bullets: [
|
|
'Maintenance work and feature development for "inmation" Backend, a scalable solution for asset optimization.',
|
|
],
|
|
},
|
|
{
|
|
company: 'Ubisoft',
|
|
location: 'Düsseldorf, Germany',
|
|
title: 'Programmer',
|
|
dates: '10/2020 - 07/2024',
|
|
subtitle: 'AAA Game Developer/Publisher',
|
|
bullets: [
|
|
'Designed and implemented new backend services, resulting in less development time for business features.',
|
|
'Overseen the architecture and deployment of software solutions handling over 600k concurrent users.',
|
|
'Reduced processing time by 30% by optimizing database queries and scripts.',
|
|
'Improved system stability by 10%, resulting in increased user engagement and satisfaction.',
|
|
'Enhanced C++ Backend functionality, resulting in a 5% decrease in response times.',
|
|
],
|
|
},
|
|
{
|
|
company: '11880 Internet Services AG',
|
|
location: 'Essen, Germany',
|
|
title: 'C++ Developer',
|
|
dates: '06/2018 - 09/2020',
|
|
subtitle: '11880.com Information Portal',
|
|
bullets: [
|
|
'Maintained and developed new features of an in-memory search engine written in C++.',
|
|
'Lifting on-premises services to the cloud, resulting in 30% more uptime and reduced costs.',
|
|
],
|
|
},
|
|
{
|
|
company: 'Fluxatron Softec GmbH',
|
|
location: 'Oberhausen, Germany',
|
|
title: 'Software Developer Working Student',
|
|
dates: '03/2016 - 09/2017',
|
|
subtitle: 'Software Startup',
|
|
bullets: [
|
|
'Implement and design add-ons for Dynamics AX 365 for Business and Operations using X++.',
|
|
'Enabled operational efficiency by reducing feature deployment time by 20% through end-to-end feature implementation collaboration.',
|
|
],
|
|
},
|
|
]
|
|
|
|
const education: EducationEntry[] = [
|
|
{
|
|
school: 'Hochschule Ruhr West University of Applied Sciences',
|
|
dates: '10/2015 - 05/2018',
|
|
degree: 'Applied Informatics (no degree)',
|
|
},
|
|
{
|
|
school: 'Hans-Sachs Berufskolleg',
|
|
dates: '08/2012 - 06/2015',
|
|
degree: 'Fachabitur + Informationstechnischer Assistent',
|
|
},
|
|
]
|
|
|
|
const achievements: Achievement[] = [
|
|
{
|
|
title: 'Design New Services',
|
|
desc: 'Successfully implemented two new services within three months.',
|
|
},
|
|
{
|
|
title: 'Server Optimization Success',
|
|
desc: 'Reduced server downtime by 10% by optimizing code.',
|
|
},
|
|
{
|
|
title: 'Performance Enhancement Achievement',
|
|
desc: 'Improved application performance by 50% through code refactoring.',
|
|
},
|
|
]
|
|
|
|
const languages: Language[] = [
|
|
{ name: 'German', level: 'Native', rating: 5 },
|
|
{ name: 'English', level: 'Proficient', rating: 4 },
|
|
{ name: 'Spanish', level: 'Beginner', rating: 1 },
|
|
]
|
|
|
|
const interests: string[] = [
|
|
'Software Development',
|
|
'Linux',
|
|
'Open Source',
|
|
'Raspberry Pi',
|
|
'Rock climbing',
|
|
'Calisthenics',
|
|
]
|
|
|
|
const skills: Skill[] = [
|
|
{ name: 'Golang', rating: 5 },
|
|
{ name: 'C++17', rating: 8 },
|
|
{ name: 'C++20', rating: 5 },
|
|
{ name: 'C++ STL', rating: 7 },
|
|
{ name: 'C++ boost', rating: 4 },
|
|
{ name: 'C++ POCO', rating: 4 },
|
|
{ name: 'C (C89 to C11)', rating: 7 },
|
|
{ name: 'SQL', rating: 5 },
|
|
{ name: 'TypeScript', rating: 5 },
|
|
{ name: 'Docker', rating: 6 },
|
|
{ name: 'Linux', rating: 7 },
|
|
{ name: 'git', rating: 7 },
|
|
{ name: 'HTTP/REST', rating: 7 },
|
|
{ name: 'Amazon Web Services (AWS)', rating: 4 },
|
|
{ name: 'Node.js', rating: 4 },
|
|
{ name: 'Vue.js', rating: 5 },
|
|
{ name: 'CMake', rating: 5 },
|
|
{ name: 'MongoDB', rating: 4 },
|
|
{ name: 'Python', rating: 6 },
|
|
{ name: 'Rust', rating: 4 },
|
|
{ name: 'C#', rating: 3 },
|
|
{ name: 'ELK stack', rating: 3 },
|
|
{ name: 'DevOps', rating: 5 },
|
|
{ name: 'CI/CD with gitlab/jenkins', rating: 6 },
|
|
{ name: 'GitHub Actions', rating: 6 },
|
|
{ name: 'Jetbrains IDEs', rating: 7 },
|
|
{ name: 'VSCode', rating: 5 },
|
|
{ name: 'Visual Studio', rating: 5 },
|
|
{ name: 'vim/neovim', rating: 5 },
|
|
{ name: 'MS Windows', rating: 5 },
|
|
{ name: 'perf', rating: 5 },
|
|
{ name: 'valgrind', rating: 4 },
|
|
{ name: 'C++ SFML', rating: 6 },
|
|
{ name: 'OpenGL', rating: 4 },
|
|
{ name: 'Vulkan', rating: 2 },
|
|
{ name: 'DirectX', rating: 2 },
|
|
{ name: 'HTML5/CSS3', rating: 4 },
|
|
{ name: 'Claude Code', rating: 4 },
|
|
]
|
|
|
|
const additionalExperiences: AdditionalExperience[] = [
|
|
{
|
|
title: 'Minecraft Mod',
|
|
desc: 'Around 2012 I have developed and published the "Blood Mod" for Minecraft. The mod was pretty popular at the time, being downloaded thousands of times.',
|
|
},
|
|
{
|
|
title: 'Buy script generator',
|
|
desc: 'Around 2013, I have developed a tool to generate so-called "buy scripts" for Counter-Strike. This was my first experience creating C++ GUI applications, using the Win32 API.',
|
|
},
|
|
{
|
|
title: 'Small 2D games',
|
|
desc: 'Around 2014-2015 I became interested in game development and have created a few simple 2D games using C++ and SFML.',
|
|
},
|
|
{
|
|
title: 'Self-hosted services',
|
|
desc: 'For years, I have been hosting my own git and password manager at home, utilizing a Raspberry Pi with Docker. In addition, I have a VPS with nginx and WireGuard, for access from anywhere around the world.',
|
|
},
|
|
]
|
|
</script>
|
|
|
|
<template>
|
|
<main class="page cv-page">
|
|
<div class="profile-top">
|
|
<img class="avatar" :src="cvPhoto" alt="Portrait of Robin Dittmar" />
|
|
<div>
|
|
<h1>Robin Dittmar</h1>
|
|
<span class="role-badge">Software Developer</span>
|
|
</div>
|
|
</div>
|
|
<div class="ledger-rule" aria-hidden="true"></div>
|
|
|
|
<section class="card info-card contact-card">
|
|
<h4>Contacts</h4>
|
|
<p class="row">
|
|
<span class="row-label"><CvIcon name="phone" />Phone</span>
|
|
<a class="mono-num" href="tel:+4915901668799">+49 1590 1668799</a>
|
|
</p>
|
|
<p class="row">
|
|
<span class="row-label"><CvIcon name="email" />Email</span>
|
|
<a href="mailto:robindittmar@gmail.com">robindittmar@gmail.com</a>
|
|
</p>
|
|
<p class="row">
|
|
<span class="row-label"><CvIcon name="location" />Location</span>
|
|
<strong>Düsseldorf</strong>
|
|
</p>
|
|
<p class="row">
|
|
<span class="row-label"><CvIcon name="linkedin" />LinkedIn</span>
|
|
<a href="https://www.linkedin.com/in/robin-dittmar-948424310/" target="_blank" rel="noopener"
|
|
>robindittmar</a
|
|
>
|
|
</p>
|
|
<p class="row">
|
|
<span class="row-label"><CvIcon name="github" />GitHub</span>
|
|
<a href="https://github.com/rdttmr" target="_blank" rel="noopener">rdttmr</a>
|
|
</p>
|
|
</section>
|
|
|
|
<h1 class="section-heading"><CvIcon name="code" />Check Out My Work</h1>
|
|
<ul class="cards">
|
|
<li v-for="project in projects" :key="project.repo">
|
|
<a class="card project-card" :href="project.url" target="_blank" rel="noopener">
|
|
<span class="project-icon"><CvIcon name="code" /></span>
|
|
<span class="project-body">
|
|
<span class="project-title">
|
|
{{ project.title }}
|
|
<span class="mono-num project-repo">{{ project.repo }}</span>
|
|
</span>
|
|
<span class="project-desc">{{ project.desc }}</span>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<h1 class="section-heading"><CvIcon name="summary" />Summary</h1>
|
|
<section class="card info-card">
|
|
<p class="summary-text">
|
|
Passionate Software Developer professional with over 8 years of experience in backend
|
|
development and software architecture. Expert in C++, strong TypeScript skills, seasoned
|
|
Python experience and aspiring Go professional. My key achievements include designing and
|
|
implementing new backend services resulting in reduced development time, and optimizing
|
|
systems to handle over 600k concurrent users.
|
|
</p>
|
|
</section>
|
|
|
|
<h1 class="section-heading"><CvIcon name="experience" />Experience</h1>
|
|
<ul class="cards timeline timeline-current">
|
|
<li v-for="job in experience" :key="job.company + job.dates">
|
|
<article class="card job-card">
|
|
<div class="job-card-head">
|
|
<h3>{{ job.company }}</h3>
|
|
<span class="mono-num job-dates">{{ job.dates }}</span>
|
|
</div>
|
|
<p class="job-title">{{ job.title }} · {{ job.location }}</p>
|
|
<p class="job-subtitle">{{ job.subtitle }}</p>
|
|
<ul class="job-bullets">
|
|
<li v-for="(bullet, idx) in job.bullets" :key="idx">{{ bullet }}</li>
|
|
</ul>
|
|
</article>
|
|
</li>
|
|
</ul>
|
|
|
|
<h1 class="section-heading"><CvIcon name="education" />Education</h1>
|
|
<ul class="cards timeline">
|
|
<li v-for="edu in education" :key="edu.school">
|
|
<article class="card job-card">
|
|
<div class="job-card-head">
|
|
<h3>{{ edu.school }}</h3>
|
|
<span class="mono-num job-dates">{{ edu.dates }}</span>
|
|
</div>
|
|
<p class="job-subtitle">{{ edu.degree }}</p>
|
|
</article>
|
|
</li>
|
|
</ul>
|
|
|
|
<h1 class="section-heading"><CvIcon name="achievements" />Key Achievements</h1>
|
|
<section class="card info-card">
|
|
<div v-for="item in achievements" :key="item.title" class="achievement-row">
|
|
<span class="achv-icon"><CvIcon name="verified" /></span>
|
|
<div>
|
|
<h4>{{ item.title }}</h4>
|
|
<p>{{ item.desc }}</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<h1 class="section-heading"><CvIcon name="languages" />Languages</h1>
|
|
<section class="card info-card">
|
|
<div v-for="lang in languages" :key="lang.name" class="rating-row">
|
|
<p class="row">
|
|
<span>{{ lang.name }}</span>
|
|
<span class="tag">{{ lang.level }}</span>
|
|
</p>
|
|
<div class="tally tally-lg">
|
|
<span
|
|
v-for="n in LANGUAGE_MAX"
|
|
:key="n"
|
|
class="tally-tick"
|
|
:class="{ filled: n <= lang.rating }"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<h1 class="section-heading"><CvIcon name="skills" />Industry Experience</h1>
|
|
<section class="card info-card">
|
|
<div class="skill-grid">
|
|
<div v-for="skill in skills" :key="skill.name" class="skill-row">
|
|
<span class="skill-name">{{ skill.name }}</span>
|
|
<div class="tally">
|
|
<span
|
|
v-for="n in SKILL_MAX"
|
|
:key="n"
|
|
class="tally-tick"
|
|
:class="{ filled: n <= skill.rating }"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<h1 class="section-heading"><CvIcon name="additional" />Additional Experience</h1>
|
|
<ul class="cards">
|
|
<li v-for="item in additionalExperiences" :key="item.title">
|
|
<article class="card job-card">
|
|
<h3>{{ item.title }}</h3>
|
|
<p class="job-subtitle">{{ item.desc }}</p>
|
|
</article>
|
|
</li>
|
|
</ul>
|
|
|
|
<h1 class="section-heading"><CvIcon name="interests" />Interests</h1>
|
|
<section class="card info-card">
|
|
<div class="tags">
|
|
<span v-for="interest in interests" :key="interest" class="tag">{{ interest }}</span>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.cv-page {
|
|
padding-bottom: 2.5rem;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.cv-page {
|
|
max-width: 780px;
|
|
}
|
|
}
|
|
|
|
.cv-page > h1:first-child {
|
|
font-size: 1.35rem;
|
|
}
|
|
|
|
.profile-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.profile-top h1 {
|
|
font-size: 1.4rem;
|
|
margin-bottom: 0.45rem;
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
.avatar {
|
|
display: block;
|
|
width: 76px;
|
|
height: 76px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
border: 1px solid var(--c-border);
|
|
box-shadow: var(--shadow-sm);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.role-badge {
|
|
display: inline-flex;
|
|
padding: 0.22rem 0.65rem;
|
|
border-radius: 3px;
|
|
background: var(--c-accent-bg);
|
|
color: var(--c-accent-strong);
|
|
font-size: 0.78rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.ledger-rule {
|
|
height: 4px;
|
|
margin-bottom: 1.5rem;
|
|
border-top: 1.5px solid var(--c-border-hover);
|
|
border-bottom: 1px dashed var(--c-border);
|
|
}
|
|
|
|
.section-heading {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.55rem;
|
|
font-size: 1.08rem;
|
|
font-weight: 600;
|
|
margin: 2rem 0 0.85rem;
|
|
}
|
|
|
|
.section-heading::after {
|
|
content: '';
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--c-border);
|
|
margin-left: 0.35rem;
|
|
}
|
|
|
|
.section-heading .cv-icon {
|
|
font-size: 0.85rem;
|
|
color: var(--c-accent-strong);
|
|
}
|
|
|
|
.card {
|
|
border-radius: 4px;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.info-card {
|
|
padding: 1rem 1.1rem;
|
|
}
|
|
|
|
.info-card h4 {
|
|
font-size: 0.85rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.contact-card {
|
|
border-color: var(--c-accent-soft);
|
|
box-shadow: 0 10px 28px rgba(61, 114, 180, 0.22);
|
|
}
|
|
|
|
.row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
font-size: 0.85rem;
|
|
margin-bottom: 0.4rem;
|
|
color: var(--c-text);
|
|
}
|
|
|
|
.row:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.info-card > .row {
|
|
padding-bottom: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
border-bottom: 1px dashed var(--c-border);
|
|
}
|
|
|
|
.info-card > .row:last-child {
|
|
padding-bottom: 0;
|
|
margin-bottom: 0;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.row strong {
|
|
color: var(--c-heading);
|
|
}
|
|
|
|
.row-label {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.row-label .cv-icon {
|
|
color: var(--c-text-soft);
|
|
}
|
|
|
|
.row a {
|
|
color: var(--c-accent-strong);
|
|
}
|
|
|
|
.summary-text {
|
|
font-size: 0.88rem;
|
|
color: var(--c-text);
|
|
}
|
|
|
|
.achievement-row {
|
|
display: flex;
|
|
gap: 0.7rem;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.achievement-row + .achievement-row {
|
|
margin-top: 0.85rem;
|
|
padding-top: 0.85rem;
|
|
border-top: 1px dashed var(--c-border);
|
|
}
|
|
|
|
.achv-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 1.6rem;
|
|
height: 1.6rem;
|
|
flex-shrink: 0;
|
|
margin-top: 0.05rem;
|
|
border-radius: 50%;
|
|
background: var(--c-accent-bg);
|
|
color: var(--c-accent-strong);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.achievement-row h4 {
|
|
font-size: 0.85rem;
|
|
color: var(--c-heading);
|
|
margin-bottom: 0.15rem;
|
|
}
|
|
|
|
.achievement-row p {
|
|
font-size: 0.82rem;
|
|
color: var(--c-text-soft);
|
|
}
|
|
|
|
.rating-row + .rating-row {
|
|
margin-top: 0.85rem;
|
|
}
|
|
|
|
/* rating tallies */
|
|
.tally {
|
|
display: flex;
|
|
gap: 3px;
|
|
}
|
|
|
|
.tally-tick {
|
|
flex: 1;
|
|
height: 6px;
|
|
border-radius: 1px;
|
|
background: var(--c-bg-mute);
|
|
}
|
|
|
|
.tally-tick.filled {
|
|
background: var(--c-accent);
|
|
}
|
|
|
|
.tally-lg {
|
|
max-width: 82%;
|
|
}
|
|
|
|
.tally-lg .tally-tick {
|
|
height: 7px;
|
|
}
|
|
|
|
/* cards list (experience / education / additional experience) */
|
|
.cards {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.6rem;
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
/* timeline (experience / education): a real chronological record, so the
|
|
rail + nodes carry meaning rather than decorate */
|
|
.timeline {
|
|
position: relative;
|
|
gap: 0.9rem;
|
|
padding-left: 1.1rem;
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 3px;
|
|
top: 0.6rem;
|
|
bottom: 0.6rem;
|
|
width: 1px;
|
|
background: var(--c-border-hover);
|
|
}
|
|
|
|
.timeline > li {
|
|
position: relative;
|
|
}
|
|
|
|
.timeline > li::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -1.1rem;
|
|
top: 1.1rem;
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--c-bg);
|
|
border: 1.5px solid var(--c-text-soft);
|
|
}
|
|
|
|
.timeline-current > li:first-child::before {
|
|
border-color: var(--c-accent-strong);
|
|
background: var(--c-accent-strong);
|
|
box-shadow: 0 0 0 3px var(--c-accent-bg);
|
|
}
|
|
|
|
.job-card {
|
|
padding: 0.9rem 1.1rem;
|
|
}
|
|
|
|
.job-card-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.job-card-head h3 {
|
|
font-size: 1.02rem;
|
|
}
|
|
|
|
.job-dates {
|
|
font-size: 0.75rem;
|
|
color: var(--c-text-soft);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.job-title {
|
|
font-size: 0.85rem;
|
|
color: var(--c-text);
|
|
margin-top: 0.15rem;
|
|
}
|
|
|
|
.job-subtitle {
|
|
font-size: 0.82rem;
|
|
font-style: italic;
|
|
color: var(--c-text-soft);
|
|
margin-top: 0.3rem;
|
|
}
|
|
|
|
.job-bullets {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
margin-top: 0.6rem;
|
|
font-size: 0.85rem;
|
|
color: var(--c-text);
|
|
}
|
|
|
|
.job-bullets li {
|
|
position: relative;
|
|
padding-left: 0.9rem;
|
|
}
|
|
|
|
.job-bullets li::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0.55em;
|
|
width: 5px;
|
|
height: 5px;
|
|
border-radius: 50%;
|
|
background: var(--c-accent);
|
|
}
|
|
|
|
/* project links */
|
|
.project-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.9rem;
|
|
padding: 0.9rem 1.1rem;
|
|
border: 1px solid var(--c-accent-soft);
|
|
transition:
|
|
transform 0.15s ease-in-out,
|
|
border-color 0.15s ease-in-out,
|
|
box-shadow 0.15s ease-in-out;
|
|
}
|
|
|
|
.project-card:hover,
|
|
.project-card:focus-visible {
|
|
border-color: var(--c-accent-strong);
|
|
box-shadow: var(--shadow-md);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.project-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 2.2rem;
|
|
height: 2.2rem;
|
|
flex-shrink: 0;
|
|
border-radius: var(--radius-md);
|
|
background: var(--c-accent-bg);
|
|
color: var(--c-accent-strong);
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.project-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.2rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.project-title {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.5rem;
|
|
font-weight: 600;
|
|
color: var(--c-heading);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.project-repo {
|
|
font-size: 0.75rem;
|
|
color: var(--c-accent-strong);
|
|
}
|
|
|
|
.project-desc {
|
|
font-size: 0.82rem;
|
|
color: var(--c-text-soft);
|
|
}
|
|
|
|
/* skills */
|
|
.skill-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 0.7rem;
|
|
}
|
|
|
|
@media (min-width: 560px) {
|
|
.skill-grid {
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0.7rem 1.5rem;
|
|
}
|
|
}
|
|
|
|
.skill-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
}
|
|
|
|
.skill-name {
|
|
font-size: 0.82rem;
|
|
color: var(--c-text);
|
|
}
|
|
|
|
/* tags */
|
|
.tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.tag {
|
|
display: inline-flex;
|
|
font-size: 0.72rem;
|
|
font-weight: 500;
|
|
padding: 0.2rem 0.6rem;
|
|
border-radius: 999px;
|
|
background-color: var(--c-bg-mute);
|
|
color: var(--c-text-soft);
|
|
}
|
|
</style>
|