From d208d425f58e5c8101eedc78e6ef4886493fe22c Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Fri, 12 Jun 2026 23:52:07 +0200 Subject: [PATCH 01/13] Replace photo hero with terminal-session home page The home page is now a single macOS-style terminal window: a typed 'cd davidbingmann/.' outputs the portrait at its natural 2:3 ratio next to a neofetch-style fact sheet, then 'cat about.txt' outputs the about prose, ending on a blinking prompt. The separate about/ section is absorbed into the terminal. Animation is skipped on small screens and for reduced motion, matching the previous behavior. Co-Authored-By: Claude Fable 5 --- .../specs/2026-06-12-terminal-hero-design.md | 48 +++ src/pages/Home.jsx | 294 ++++++++++-------- src/styles.css | 263 ++++++++++------ 3 files changed, 387 insertions(+), 218 deletions(-) create mode 100644 docs/superpowers/specs/2026-06-12-terminal-hero-design.md diff --git a/docs/superpowers/specs/2026-06-12-terminal-hero-design.md b/docs/superpowers/specs/2026-06-12-terminal-hero-design.md new file mode 100644 index 0000000..70dfa76 --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-terminal-hero-design.md @@ -0,0 +1,48 @@ +# Terminal Hero — Design + +## Goal + +Replace the cropped square photo hero on the home page with a macOS-style +terminal window that presents the portrait photo at its natural 2:3 ratio and +absorbs the `about/` section, so the home page is a single coherent element. + +## Concept + +The home page becomes one terminal session with two commands: + +1. `~ $ cd davidbingmann/.` is typed (reusing the existing typing-animation + idea), then the "output" fades in: photo (left, natural 2:3, ~220px wide) + and a neofetch-style fact sheet (right): name, role, `study`/`uni`/`work` + key-value lines (keys in accent yellow), social icon links. +2. `~/davidbingmann $ cat about.txt` is typed, then the existing about prose + fades in as command output, including the mail link. +3. A final empty prompt `~/davidbingmann $` with a blinking block cursor. + +The separate `about/` section below the hero is removed — its facts live in +the fact sheet, its prose in the `cat about.txt` output. + +## Visuals + +- Whole page stays in the existing dark theme (`#0b0b0b`, already hardcoded + via `data-theme="dark"`). +- Terminal window: slightly lighter surface (`#141414`), 1px border + (`#2a2c27`), 12px radius, title bar with macOS traffic lights and + `david@bingmann.de — zsh` title. On dark backgrounds the window separates + via surface + border (shadows don't read). +- Colors via `--term-*` CSS variables with a light-theme fallback so the + light palette keeps working if the theme is ever switched back. +- Prompt cwd in green, `$` muted, fact keys in yellow (`#febc2e`). + +## Behavior + +- Animation sequence: type cmd 1 → profile output fades in → type cmd 2 → + about output fades in → final blinking prompt. ~90ms per character, + short pauses between stages. +- On small screens (≤720px) and `prefers-reduced-motion`: everything renders + immediately (same policy as the current hero). +- Mobile layout: photo and fact sheet stack vertically inside the terminal. + +## Out of scope + +- Nav, footer, projects/resume/impressum pages stay unchanged. +- No new dependencies. diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 7fc5b96..ed5c241 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,155 +1,197 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; +import { FaEnvelope, FaGithub, FaLinkedinIn } from 'react-icons/fa'; +import { FaXTwitter } from 'react-icons/fa6'; import profilePicture800 from '../assets/profile_picture_800.jpeg'; import profilePicture1200 from '../assets/profile_picture_1200.jpeg'; import profilePictureFull from '../assets/profile_picture.jpeg'; -export default function Home() { - const fullPrompt = '~ cd'; - const fullPath = 'davidbingmann/.'; - const totalLength = fullPrompt.length + fullPath.length; - - const [reduceMotion, setReduceMotion] = useState(() => - typeof window !== 'undefined' && - window.matchMedia('(prefers-reduced-motion: reduce)').matches - ); - const [isSmallScreen, setIsSmallScreen] = useState(() => - typeof window !== 'undefined' && window.matchMedia('(max-width: 720px)').matches - ); - const disableHeroAnimation = reduceMotion || isSmallScreen; - - const [typedCount, setTypedCount] = useState(() => - disableHeroAnimation ? totalLength : 0 +const COMMAND_CD = 'cd davidbingmann/.'; +const COMMAND_CAT = 'cat about.txt'; +const TYPE_INTERVAL = 90; + +const STAGE = { + TYPING_CD: 0, + PROFILE: 1, + TYPING_CAT: 2, + ABOUT: 3, + DONE: 4, +}; + +const socialLinks = [ + { + label: 'LinkedIn', + href: 'https://www.linkedin.com/in/david-bingmann-13b897293/', + Icon: FaLinkedinIn, + }, + { label: 'Mail', href: 'mailto:contact@davidbingmann.de', Icon: FaEnvelope }, + { label: 'GitHub', href: 'https://github.com/davidbingmann', Icon: FaGithub }, + { label: 'X', href: 'https://x.com/dxv1d04', Icon: FaXTwitter }, +]; + +function useMediaQuery(query) { + const [matches, setMatches] = useState( + () => typeof window !== 'undefined' && window.matchMedia(query).matches ); - const [hidePrompt, setHidePrompt] = useState(() => disableHeroAnimation); - const promptRef = useRef(null); - const [promptWidth, setPromptWidth] = useState(0); - - const promptText = fullPrompt.slice(0, Math.min(typedCount, fullPrompt.length)); - const pathText = - typedCount > fullPrompt.length - ? fullPath.slice(0, typedCount - fullPrompt.length) - : ''; useEffect(() => { - document.title = 'David Bingmann'; - }, []); + const mediaQuery = window.matchMedia(query); + const handleChange = (event) => setMatches(event.matches); - useEffect(() => { - const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); - const handleMotionChange = (event) => setReduceMotion(event.matches); + setMatches(mediaQuery.matches); + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, [query]); - setReduceMotion(motionQuery.matches); - if (motionQuery.addEventListener) { - motionQuery.addEventListener('change', handleMotionChange); - return () => motionQuery.removeEventListener('change', handleMotionChange); - } + return matches; +} - motionQuery.addListener(handleMotionChange); - return () => motionQuery.removeListener(handleMotionChange); - }, []); +function useTerminalTimeline(instant) { + const [stage, setStage] = useState(instant ? STAGE.DONE : STAGE.TYPING_CD); + const [typedCd, setTypedCd] = useState(instant ? COMMAND_CD.length : 0); + const [typedCat, setTypedCat] = useState(instant ? COMMAND_CAT.length : 0); useEffect(() => { - const screenQuery = window.matchMedia('(max-width: 720px)'); - const handleScreenChange = (event) => setIsSmallScreen(event.matches); - - setIsSmallScreen(screenQuery.matches); - if (screenQuery.addEventListener) { - screenQuery.addEventListener('change', handleScreenChange); - return () => screenQuery.removeEventListener('change', handleScreenChange); + if (instant) { + setStage(STAGE.DONE); + setTypedCd(COMMAND_CD.length); + setTypedCat(COMMAND_CAT.length); + return undefined; } - screenQuery.addListener(handleScreenChange); - return () => screenQuery.removeListener(handleScreenChange); - }, []); - - useEffect(() => { - if (disableHeroAnimation) { - setTypedCount(totalLength); - setHidePrompt(true); + let delay; + let action; + + if (stage === STAGE.TYPING_CD && typedCd < COMMAND_CD.length) { + delay = TYPE_INTERVAL; + action = () => setTypedCd((count) => count + 1); + } else if (stage === STAGE.TYPING_CD) { + delay = 400; + action = () => setStage(STAGE.PROFILE); + } else if (stage === STAGE.PROFILE) { + delay = 900; + action = () => setStage(STAGE.TYPING_CAT); + } else if (stage === STAGE.TYPING_CAT && typedCat < COMMAND_CAT.length) { + delay = TYPE_INTERVAL; + action = () => setTypedCat((count) => count + 1); + } else if (stage === STAGE.TYPING_CAT) { + delay = 400; + action = () => setStage(STAGE.ABOUT); + } else if (stage === STAGE.ABOUT) { + delay = 500; + action = () => setStage(STAGE.DONE); + } else { return undefined; } - const timer = setInterval(() => { - setTypedCount((current) => { - if (current >= totalLength) { - return current; - } - return current + 1; - }); - }, 150); - - return () => { - clearInterval(timer); - }; - }, [disableHeroAnimation, totalLength]); + const timer = setTimeout(action, delay); + return () => clearTimeout(timer); + }, [instant, stage, typedCd, typedCat]); - useEffect(() => { - if (disableHeroAnimation) { - return undefined; - } + return { stage, typedCd, typedCat }; +} - if (typedCount === totalLength) { - const timer = setTimeout(() => setHidePrompt(true), 500); - return () => clearTimeout(timer); - } - return undefined; - }, [disableHeroAnimation, typedCount, totalLength]); +export default function Home() { + const reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); + const smallScreen = useMediaQuery('(max-width: 720px)'); + const { stage, typedCd, typedCat } = useTerminalTimeline( + reduceMotion || smallScreen + ); useEffect(() => { - if (disableHeroAnimation) { - return undefined; - } - - if (promptText.length === fullPrompt.length && promptRef.current) { - setPromptWidth(promptRef.current.getBoundingClientRect().width); - } - return undefined; - }, [disableHeroAnimation, promptText, fullPrompt.length]); + document.title = 'David Bingmann'; + }, []); return (
-
- Portrait of David Bingmann -
-
-
- - {promptText} - - {pathText} -
-
Bachelor’s student
+
+
+ + + + david@bingmann.de — zsh
-
- -
-

about/

-
-

- My name is David Bingmann and I am currently studying Business - Informatics & Artificial Intelligence at the University of Trier. -

-

- In addition to my studies, I work as a research assistant at the - German Research Center for Artificial Intelligence (DFKI). -

-

- Feel free to check out my social media channels or write me a{' '} - mail. I look forward to - connecting! +

+

+ ~{' '} + ${' '} + {COMMAND_CD.slice(0, typedCd)} + {stage === STAGE.TYPING_CD && }

+ + {stage >= STAGE.PROFILE && ( +
+ Portrait of David Bingmann +
+

david bingmann

+

bachelor’s student

+
+

+ study + business informatics & ai +

+

+ uni + university of trier +

+

+ work + research assistant @ dfki +

+
+ {socialLinks.map(({ label, href, Icon }) => ( + + + ))} +
+
+
+ )} + + {stage >= STAGE.TYPING_CAT && ( +

+ ~/davidbingmann{' '} + ${' '} + {COMMAND_CAT.slice(0, typedCat)} + {stage === STAGE.TYPING_CAT && ( + + )} +

+ )} + + {stage >= STAGE.ABOUT && ( +

+ My name is David Bingmann and I am currently studying Business + Informatics & Artificial Intelligence at the University of + Trier. In addition to my studies, I work as a research assistant + at the German Research Center for Artificial Intelligence (DFKI). + Feel free to check out my social media channels or write me a{' '} + mail — I look + forward to connecting! +

+ )} + + {stage >= STAGE.DONE && ( +

+ ~/davidbingmann{' '} + ${' '} + +

+ )}
diff --git a/src/styles.css b/src/styles.css index baa07e7..792ce89 100644 --- a/src/styles.css +++ b/src/styles.css @@ -10,6 +10,13 @@ --accent: #2f2f2f; --shadow: rgba(21, 20, 18, 0.05); --hover: rgba(47, 47, 47, 0.08); + --term-bg: #ffffff; + --term-bar: #ecebe5; + --term-border: #e1dfd8; + --term-text: #1c1c1c; + --term-muted: #5f5f5f; + --term-key: #b07d10; + --term-green: #1f9e4b; } :root[data-theme='dark'] { @@ -23,6 +30,13 @@ --accent: #f5f3ee; --shadow: rgba(0, 0, 0, 0.35); --hover: rgba(245, 243, 238, 0.08); + --term-bg: #141414; + --term-bar: #1d1f1b; + --term-border: #2a2c27; + --term-text: #f5f3ee; + --term-muted: #b7b2a8; + --term-key: #febc2e; + --term-green: #28c840; } * { @@ -115,88 +129,161 @@ p { padding-right: calc(var(--page-gutter) + env(safe-area-inset-right, 0px)); } -.hero { - width: auto; - max-width: min(100%, 760px); - aspect-ratio: 1 / 1; - container-type: inline-size; +.terminal { + max-width: 720px; margin: 0 auto; - height: 70vh; - height: 70svh; - min-height: 420px; - background: #0b0b0b; - position: relative; - display: flex; - align-items: flex-end; - justify-content: center; - color: #ffffff; - border-radius: 18px; + border: 1px solid var(--term-border); + border-radius: 12px; + background: var(--term-bg); overflow: hidden; } -.hero-image { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - object-fit: cover; - object-position: 50% 20%; - pointer-events: none; +.terminal-bar { + display: flex; + align-items: center; + gap: 7px; + padding: 10px 14px; + background: var(--term-bar); + border-bottom: 1px solid var(--term-border); } -.hero-overlay { - position: absolute; - inset: 0; - background: linear-gradient(180deg, rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.55) 70%); - z-index: 1; +.terminal-dot { + width: 12px; + height: 12px; + border-radius: 50%; } -.hero-content { - position: relative; - z-index: 2; - text-align: center; - padding: 0 1.5rem 2.5rem; - animation: fadeUp 0.8s ease forwards; +.terminal-dot--close { + background: #ff5f57; } -.hero-command { - display: inline-flex; - align-items: center; - font-size: clamp(2.2rem, 5vw, 3rem); - /* sized so the longest animation state "~ cd davidbingmann/." (20ch) never exceeds the hero */ - font-size: clamp(1.4rem, calc((100cqw - 3.5rem) / 12), 3rem); +.terminal-dot--minimize { + background: #febc2e; +} + +.terminal-dot--zoom { + background: #28c840; +} + +.terminal-title { + margin: 0 auto; + font-size: 0.8rem; + color: var(--term-muted); +} + +.terminal-body { + padding: 1.4rem 1.5rem 1.5rem; + font-size: 1rem; + line-height: 1.75; + color: var(--term-text); +} + +.terminal-line { + margin: 0; + color: var(--term-text); +} + +.terminal-cwd { + color: var(--term-green); +} + +.terminal-dollar { + color: var(--term-muted); +} + +.terminal-cursor { + display: inline-block; + width: 0.6em; + height: 1.05em; + margin-left: 2px; + background: var(--term-text); + vertical-align: text-bottom; +} + +.terminal-cursor--blink { + animation: blink 1.1s steps(1) infinite; +} + +.terminal-profile { + display: flex; + gap: 1.4rem; + margin: 1.1rem 0 1.3rem; + animation: fadeIn 0.5s ease; +} + +.terminal-photo { + width: min(220px, 38%); + aspect-ratio: 2 / 3; + object-fit: cover; + border-radius: 8px; + flex-shrink: 0; +} + +.terminal-facts { + display: flex; + flex-direction: column; + min-width: 0; +} + +.terminal-name { + margin: 0; + font-size: 1.5rem; font-weight: 700; - line-height: 1.1; - text-shadow: 0 2px 12px rgba(0, 0, 0, 0.5); + color: var(--term-text); + line-height: 1.3; } -.hero-tagline { - margin-top: 0.85rem; - font-size: clamp(1.05rem, 2.2vw, 1.4rem); - font-weight: 600; - letter-spacing: 0.06em; - opacity: 0.9; - text-shadow: 0 2px 12px rgba(0, 0, 0, 0.5); +.terminal-role { + margin: 0.1rem 0 0; + color: var(--term-muted); +} + +.terminal-rule { + width: 100%; + margin: 0.8rem 0; + border: 0; + border-top: 1px solid var(--term-border); } -.hero-command .prompt { +.terminal-fact { + margin: 0 0 0.35rem; + color: var(--term-text); +} + +.terminal-key { display: inline-block; - overflow: hidden; - white-space: nowrap; - max-width: var(--prompt-width, 6ch); - margin-right: 1ch; - opacity: 0.9; - transition: max-width 0.35s ease, margin-right 0.35s ease, opacity 0.35s ease; + min-width: 5.5ch; + color: var(--term-key); + font-weight: 700; } -.hero-command .prompt--hidden { - max-width: 0; - margin-right: 0; - opacity: 0; +.terminal-socials { + display: flex; + gap: 1rem; + margin-top: auto; + padding-top: 0.9rem; } -.hero-command .path { - white-space: nowrap; +.terminal-socials a { + display: inline-flex; + color: var(--term-muted); + transition: color 0.2s ease; +} + +.terminal-social-icon { + width: 19px; + height: 19px; +} + +.terminal-about { + margin: 0.4rem 0 1.2rem; + color: var(--term-muted); + animation: fadeIn 0.5s ease; +} + +.terminal-about a { + color: var(--term-text); + border-bottom: 1px solid var(--term-text); } .section { @@ -482,14 +569,9 @@ p { border: 0; } -@keyframes fadeUp { - from { +@keyframes blink { + 50% { opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); } } @@ -528,6 +610,10 @@ p { color: var(--text); } + .terminal-socials a:hover { + color: var(--term-text); + } + .footer-impressum:hover { color: var(--text); border-color: var(--text); @@ -552,25 +638,27 @@ p { gap: 1.25rem 1.75rem; } - .hero { - height: 58vh; - height: 58svh; - min-height: 320px; - border-radius: 14px; + .terminal-body { + padding: 1.1rem 1rem 1.2rem; + font-size: 0.92rem; } - .hero-tagline { - margin-top: 0.75rem; - font-size: clamp(0.95rem, 4vw, 1.2rem); + .terminal-profile { + flex-direction: column; + gap: 1rem; + animation: none; } - .hero-content { + .terminal-about { animation: none; - padding: 0 1.25rem 2rem; } - .hero-command .prompt { - transition: none; + .terminal-photo { + width: min(240px, 70%); + } + + .terminal-name { + font-size: 1.3rem; } .section-body { @@ -618,17 +706,8 @@ p { --page-gutter: 0.95rem; } - .hero-tagline { - margin-top: 0.7rem; - } - - .hero-content { - padding-left: 1rem; - padding-right: 1rem; - } - - .hero-command .path { - white-space: nowrap; + .terminal-body { + font-size: 0.85rem; } .section { From 13db4b7eadb8380d46e383e5e0e56f09dc628bea Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Fri, 12 Jun 2026 23:55:44 +0200 Subject: [PATCH 02/13] Polish terminal output: drop socials, fix casing, smooth expansion Remove the social icon row from the fact sheet (the footer already has the links), use proper capitalization for the profile output, widen the key column so keys no longer touch their values, and reveal each output block with an animated height transition (grid 0fr to 1fr) instead of popping in, so the window grows smoothly. Co-Authored-By: Claude Fable 5 --- src/pages/Home.jsx | 66 +++++++++++++++++----------------------------- src/styles.css | 52 ++++++++++++++++-------------------- 2 files changed, 46 insertions(+), 72 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index ed5c241..7fd4556 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,6 +1,4 @@ import { useEffect, useState } from 'react'; -import { FaEnvelope, FaGithub, FaLinkedinIn } from 'react-icons/fa'; -import { FaXTwitter } from 'react-icons/fa6'; import profilePicture800 from '../assets/profile_picture_800.jpeg'; import profilePicture1200 from '../assets/profile_picture_1200.jpeg'; import profilePictureFull from '../assets/profile_picture.jpeg'; @@ -17,17 +15,6 @@ const STAGE = { DONE: 4, }; -const socialLinks = [ - { - label: 'LinkedIn', - href: 'https://www.linkedin.com/in/david-bingmann-13b897293/', - Icon: FaLinkedinIn, - }, - { label: 'Mail', href: 'mailto:contact@davidbingmann.de', Icon: FaEnvelope }, - { label: 'GitHub', href: 'https://github.com/davidbingmann', Icon: FaGithub }, - { label: 'X', href: 'https://x.com/dxv1d04', Icon: FaXTwitter }, -]; - function useMediaQuery(query) { const [matches, setMatches] = useState( () => typeof window !== 'undefined' && window.matchMedia(query).matches @@ -90,6 +77,14 @@ function useTerminalTimeline(instant) { return { stage, typedCd, typedCat }; } +function Reveal({ open, children }) { + return ( +
+
{children}
+
+ ); +} + export default function Home() { const reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); const smallScreen = useMediaQuery('(max-width: 720px)'); @@ -118,7 +113,7 @@ export default function Home() { {stage === STAGE.TYPING_CD && }

- {stage >= STAGE.PROFILE && ( + = STAGE.PROFILE}>
-

david bingmann

-

bachelor’s student

+

David Bingmann

+

Bachelor’s student


- study - business informatics & ai + Study + Business Informatics & AI

- uni - university of trier + Uni + University of Trier

- work - research assistant @ dfki + Work + Research assistant @ DFKI

-
- {socialLinks.map(({ label, href, Icon }) => ( - - - ))} -
- )} +
- {stage >= STAGE.TYPING_CAT && ( + = STAGE.TYPING_CAT}>

~/davidbingmann{' '} ${' '} @@ -171,9 +153,9 @@ export default function Home() { )}

- )} +
- {stage >= STAGE.ABOUT && ( + = STAGE.ABOUT}>

My name is David Bingmann and I am currently studying Business Informatics & Artificial Intelligence at the University of @@ -183,15 +165,15 @@ export default function Home() { mail — I look forward to connecting!

- )} +
- {stage >= STAGE.DONE && ( + = STAGE.DONE}>

~/davidbingmann{' '} ${' '}

- )} +
diff --git a/src/styles.css b/src/styles.css index 792ce89..38dd840 100644 --- a/src/styles.css +++ b/src/styles.css @@ -204,11 +204,31 @@ p { animation: blink 1.1s steps(1) infinite; } +.terminal-reveal { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.5s ease; +} + +.terminal-reveal--open { + grid-template-rows: 1fr; +} + +.terminal-reveal-inner { + min-height: 0; + overflow: hidden; + opacity: 0; + transition: opacity 0.45s ease 0.15s; +} + +.terminal-reveal--open .terminal-reveal-inner { + opacity: 1; +} + .terminal-profile { display: flex; gap: 1.4rem; margin: 1.1rem 0 1.3rem; - animation: fadeIn 0.5s ease; } .terminal-photo { @@ -252,33 +272,14 @@ p { .terminal-key { display: inline-block; - min-width: 5.5ch; + min-width: 7ch; color: var(--term-key); font-weight: 700; } -.terminal-socials { - display: flex; - gap: 1rem; - margin-top: auto; - padding-top: 0.9rem; -} - -.terminal-socials a { - display: inline-flex; - color: var(--term-muted); - transition: color 0.2s ease; -} - -.terminal-social-icon { - width: 19px; - height: 19px; -} - .terminal-about { margin: 0.4rem 0 1.2rem; color: var(--term-muted); - animation: fadeIn 0.5s ease; } .terminal-about a { @@ -610,10 +611,6 @@ p { color: var(--text); } - .terminal-socials a:hover { - color: var(--term-text); - } - .footer-impressum:hover { color: var(--text); border-color: var(--text); @@ -646,11 +643,6 @@ p { .terminal-profile { flex-direction: column; gap: 1rem; - animation: none; - } - - .terminal-about { - animation: none; } .terminal-photo { From 0c4090cb3531f47b863368d768b0406839ca724f Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:05:45 +0200 Subject: [PATCH 03/13] Redesign resume as chronological card timeline Replace the plain timeline with one continuous chronological spine: each station is a card with a category pill (education in yellow, work in blue), the period right-aligned, and a green 'current' badge plus pulsing timeline dot for ongoing stations. On load the timeline line draws itself top to bottom while the cards cascade in sync; animations stay disabled on small screens and for reduced motion. Co-Authored-By: Claude Fable 5 --- src/data/timeline.js | 40 ++++++------ src/pages/Resume.jsx | 29 +++++++-- src/styles.css | 150 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 169 insertions(+), 50 deletions(-) diff --git a/src/data/timeline.js b/src/data/timeline.js index 85ae512..448a16b 100644 --- a/src/data/timeline.js +++ b/src/data/timeline.js @@ -1,32 +1,34 @@ export const timelineItems = [ { - year: 'October 2025 → Present', - title: 'Switch to University of Trier', - description: - 'Switch to the University of Trier to study Business Informatics and Artificial Intelligence', + period: 'Oct 2025 → present', + title: 'University of Trier', + description: 'B.Sc. Business Informatics & Artificial Intelligence', + category: 'education', + current: true, }, { - year: 'October 2024 → Present', - title: 'Research Assistant at DFKI', + period: 'Oct 2024 → present', + title: 'Research Assistant — DFKI', description: - 'Start of my part-time job at the German Research Center for Artificial Intelligence (DFKI) alongside my studies', + 'German Research Center for Artificial Intelligence, part-time alongside my studies', + category: 'work', + current: true, }, { - year: 'October 2024', - title: "Start of my Bachelor's degree", - description: - "Start of my Bachelor's degree: Artificial Intelligence and Data Science at the University of Applied Sciences in Trier", + period: 'Oct 2024 → Sep 2025', + title: 'University of Applied Sciences Trier', + description: 'B.Sc. Artificial Intelligence & Data Science', + category: 'education', }, { - year: 'May 2024', - title: 'Internship at Tesla Automation', - description: - 'Internship from 27/05/2024 until 07/06/2024 in Controls Engineering', + period: 'May → Jun 2024', + title: 'Intern — Tesla Automation', + description: 'Controls Engineering', + category: 'work', }, { - year: 'March 2024', - title: 'Abitur', - description: - 'Finished my Abitur at the Staatliches Eifelgymnasium in Neuerburg', + period: 'Mar 2024', + title: 'Abitur — Staatliches Eifelgymnasium Neuerburg', + category: 'education', }, ]; diff --git a/src/pages/Resume.jsx b/src/pages/Resume.jsx index 8787c03..7d8d76c 100644 --- a/src/pages/Resume.jsx +++ b/src/pages/Resume.jsx @@ -9,16 +9,31 @@ export default function Resume() { return (

resume/

-
+
{timelineItems.map((item, index) => (
-

{item.year}

-

{item.title}

-

{item.description}

+ +
+ + {item.category} + +
+

+ {item.title} + {item.current && current} +

+ {item.period} +
+ {item.description && ( +

{item.description}

+ )} +
))}
diff --git a/src/styles.css b/src/styles.css index 38dd840..2b599b1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -17,6 +17,7 @@ --term-muted: #5f5f5f; --term-key: #b07d10; --term-green: #1f9e4b; + --term-blue: #2563a8; } :root[data-theme='dark'] { @@ -37,6 +38,7 @@ --term-muted: #b7b2a8; --term-key: #febc2e; --term-green: #28c840; + --term-blue: #7eb8f2; } * { @@ -465,45 +467,117 @@ p { font-weight: 700; } -.timeline { +.resume-timeline { + position: relative; margin-top: 0.5rem; - padding-left: 1.5rem; - border-left: 1px solid var(--line); + padding-left: 1.6rem; display: grid; - gap: 1.8rem; + gap: 1rem; +} + +.resume-timeline::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 1.5px; + background: var(--line); + transform: scaleY(0); + transform-origin: top; + animation: drawLine 1s ease forwards; } -.timeline-item { +.resume-item { position: relative; - padding-left: 1rem; opacity: 0; - transform: translateY(12px); + transform: translateY(14px); animation: fadeIn 0.5s ease forwards; animation-delay: var(--delay); } -.timeline-item::before { - content: ''; +.resume-dot { position: absolute; - left: -1.1rem; - top: 0.35rem; + left: calc(-1.6rem - 4px); + top: 1.35rem; width: 10px; height: 10px; border-radius: 50%; - background: var(--accent); + background: var(--muted); } -.timeline-year { - margin: 0 0 0.4rem; +.resume-dot--current { + background: var(--term-green); + animation: pulse 2s ease-out infinite; +} + +.resume-card { + border: 1px solid var(--line); + border-radius: 10px; + background: var(--term-bg); + padding: 1.15rem 1.3rem; +} + +.resume-tag { + display: inline-block; + margin-bottom: 0.55rem; + padding: 0.1rem 0.6rem; + border: 1px solid var(--line); + border-radius: 999px; + font-size: 0.68rem; + letter-spacing: 0.1em; text-transform: uppercase; - letter-spacing: 0.15em; - font-size: 0.75rem; color: var(--muted); } -.timeline-item h3 { - margin-bottom: 0.4rem; - font-size: 1.3rem; +.resume-tag--education { + color: var(--term-key); + border-color: color-mix(in srgb, var(--term-key) 40%, transparent); +} + +.resume-tag--work { + color: var(--term-blue); + border-color: color-mix(in srgb, var(--term-blue) 40%, transparent); +} + +.resume-card-row { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: baseline; + gap: 0.25rem 1rem; +} + +.resume-card-title { + margin: 0; + font-size: 1.15rem; + line-height: 1.5; +} + +.resume-badge { + display: inline-block; + margin-left: 0.6rem; + padding: 0.05rem 0.5rem; + border: 1px solid color-mix(in srgb, var(--term-green) 40%, transparent); + border-radius: 999px; + font-size: 0.68rem; + font-weight: 400; + color: var(--term-green); + vertical-align: 2px; + white-space: nowrap; +} + +.resume-period { + flex-shrink: 0; + color: var(--muted); + font-size: 0.85rem; + white-space: nowrap; +} + +.resume-card-description { + margin: 0.5rem 0 0; + font-size: 0.95rem; + line-height: 1.6; } .footer { @@ -576,6 +650,21 @@ p { } } +@keyframes drawLine { + to { + transform: scaleY(1); + } +} + +@keyframes pulse { + from { + box-shadow: 0 0 0 0 color-mix(in srgb, var(--term-green) 50%, transparent); + } + to { + box-shadow: 0 0 0 9px transparent; + } +} + @keyframes fadeIn { from { opacity: 0; @@ -662,15 +751,28 @@ p { font-size: 1.05rem; } - .timeline { - padding-left: 1rem; + .resume-timeline { + padding-left: 1.2rem; } - .timeline-item h3 { - font-size: 1.1rem; + .resume-timeline::before { + transform: none; + animation: none; + } + + .resume-dot { + left: calc(-1.2rem - 4px); + } + + .resume-card { + padding: 1rem 1.1rem; + } + + .resume-card-title { + font-size: 1.05rem; } - .timeline-item { + .resume-item { opacity: 1; transform: none; animation: none; From e5203b1c95cb19cae108bb4264b10cc61388dbd7 Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:07:27 +0200 Subject: [PATCH 04/13] Play home intro animation only on first visit per page load Navigating back to home via the tabs now shows the finished terminal immediately; the typing intro replays only after a full page reload. Co-Authored-By: Claude Fable 5 --- src/pages/Home.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 7fd4556..56f3ba6 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -15,6 +15,10 @@ const STAGE = { DONE: 4, }; +// Module-level so the intro only plays on the first visit per page load, +// not every time the user navigates back to home via the tabs. +let introPlayed = false; + function useMediaQuery(query) { const [matches, setMatches] = useState( () => typeof window !== 'undefined' && window.matchMedia(query).matches @@ -88,11 +92,13 @@ function Reveal({ open, children }) { export default function Home() { const reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); const smallScreen = useMediaQuery('(max-width: 720px)'); + const [skipIntro] = useState(() => introPlayed); const { stage, typedCd, typedCat } = useTerminalTimeline( - reduceMotion || smallScreen + reduceMotion || smallScreen || skipIntro ); useEffect(() => { + introPlayed = true; document.title = 'David Bingmann'; }, []); From 06f56547f123211fb4b93e3b45a7444c3a0ead7d Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:08:42 +0200 Subject: [PATCH 05/13] Tag Tesla Automation as internship instead of work Co-Authored-By: Claude Fable 5 --- src/data/timeline.js | 2 +- src/styles.css | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/data/timeline.js b/src/data/timeline.js index 448a16b..8ff5d23 100644 --- a/src/data/timeline.js +++ b/src/data/timeline.js @@ -24,7 +24,7 @@ export const timelineItems = [ period: 'May → Jun 2024', title: 'Intern — Tesla Automation', description: 'Controls Engineering', - category: 'work', + category: 'internship', }, { period: 'Mar 2024', diff --git a/src/styles.css b/src/styles.css index 2b599b1..1c704a1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -18,6 +18,7 @@ --term-key: #b07d10; --term-green: #1f9e4b; --term-blue: #2563a8; + --term-purple: #7a4fb5; } :root[data-theme='dark'] { @@ -39,6 +40,7 @@ --term-key: #febc2e; --term-green: #28c840; --term-blue: #7eb8f2; + --term-purple: #c89bf5; } * { @@ -540,6 +542,11 @@ p { border-color: color-mix(in srgb, var(--term-blue) 40%, transparent); } +.resume-tag--internship { + color: var(--term-purple); + border-color: color-mix(in srgb, var(--term-purple) 40%, transparent); +} + .resume-card-row { display: flex; flex-wrap: wrap; From f7141aa7e3c716618335b31d42df264f3b6a8c66 Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:14:52 +0200 Subject: [PATCH 06/13] Redesign projects list and detail pages to match site card style Project cards now use the shared dark card look with a colored type pill (paper yellow, software blue), the project tags as subtle hashtags, and a green arrow link line for repo/PDF. The detail page gets the same card treatment with a type pill next to the title and body paragraphs cascading in like the resume timeline. The tag pill styles are generalized from resume-tag to tag and shared by both pages; unused repo prefix/label and listTitle fields are dropped and the paper project gets a descriptive title. Co-Authored-By: Claude Fable 5 --- src/data/projects.js | 38 ++++++------ src/pages/Project.jsx | 31 +++++++--- src/pages/Projects.jsx | 22 +++++-- src/pages/Resume.jsx | 2 +- src/styles.css | 130 ++++++++++++++++++++++++++++++++--------- 5 files changed, 161 insertions(+), 62 deletions(-) diff --git a/src/data/projects.js b/src/data/projects.js index 0e26a77..b47ea61 100644 --- a/src/data/projects.js +++ b/src/data/projects.js @@ -1,30 +1,13 @@ export const projects = [ - { - slug: 'paper-humanoid-robots-industry-5', - title: 'Paper', - listTitle: 'Paper', - headline: 'What potential do humanoid robots offer in Industry 5.0?', - tags: ['industry-5.0', 'humanoid-robots', 'ai', 'paper'], - repo: { - prefix: 'PDF', - label: 'davidbingmann/paper', - href: '/papers/paper.pdf', - }, - summary: - 'My paper looks at whether humanoid robots can support the shift from Industry 4.0 to Industry 5.0: more resilient, more human-centered, and better suited to real factories.', - body: [ - 'As part of a seminar at the University of Trier, I wrote this paper on humanoid robots in the context of Industry 5.0: not as a replacement for humans, but as a possible way to make industrial work more resilient, flexible, and human-centered.', - 'The paper discusses why current automation still struggles in human-designed factories, what recent robot learning research makes possible, and where the technical, economic, and regulatory limits still are. You can read the public PDF version above.', - ], - }, { slug: 'echotype', title: 'EchoType', + type: 'software', headline: 'Speak your prompts instead of typing.', tags: ['agents', 'ai', 'productivity', 'voice'], repo: { - label: 'davidbingmann/EchoType', href: 'https://github.com/davidbingmann/EchoType', + linkText: 'github.com/davidbingmann/EchoType', }, summary: 'I was looking for a faster and more intuitive way to write prompts for AI agents like Codex or Claude Code, especially longer prompts that are tedious to type.', @@ -36,4 +19,21 @@ export const projects = [ 'If you want to install EchoType, you can find the installation instructions in the GitHub repository.', ], }, + { + slug: 'paper-humanoid-robots-industry-5', + title: 'Humanoid Robots in Industry 5.0', + type: 'paper', + headline: 'What potential do humanoid robots offer in Industry 5.0?', + tags: ['industry-5.0', 'humanoid-robots', 'ai', 'paper'], + repo: { + href: '/papers/paper.pdf', + linkText: 'paper.pdf', + }, + summary: + 'My paper looks at whether humanoid robots can support the shift from Industry 4.0 to Industry 5.0: more resilient, more human-centered, and better suited to real factories.', + body: [ + 'As part of a seminar at the University of Trier, I wrote this paper on humanoid robots in the context of Industry 5.0: not as a replacement for humans, but as a possible way to make industrial work more resilient, flexible, and human-centered.', + 'The paper discusses why current automation still struggles in human-designed factories, what recent robot learning research makes possible, and where the technical, economic, and regulatory limits still are. You can read the public PDF version above.', + ], + }, ]; diff --git a/src/pages/Project.jsx b/src/pages/Project.jsx index bd54e53..a892798 100644 --- a/src/pages/Project.jsx +++ b/src/pages/Project.jsx @@ -33,19 +33,36 @@ export default function Project() { {project.title}
-

{project.title}

+
+

+ {project.title} +

+ {project.type} +
-
+

{project.headline}

-

- - {project.repo.prefix ?? 'GitHub'}: {project.repo.label} +

+ + →{' '} + {project.repo.linkText}

- {project.body.map((paragraph) => ( -

{paragraph}

+ {project.body.map((paragraph, index) => ( +

+ {paragraph} +

))}
diff --git a/src/pages/Projects.jsx b/src/pages/Projects.jsx index 2c4af12..c0d905b 100644 --- a/src/pages/Projects.jsx +++ b/src/pages/Projects.jsx @@ -15,21 +15,31 @@ export default function Projects() { ))} diff --git a/src/pages/Resume.jsx b/src/pages/Resume.jsx index 7d8d76c..a7412ff 100644 --- a/src/pages/Resume.jsx +++ b/src/pages/Resume.jsx @@ -20,7 +20,7 @@ export default function Resume() { className={`resume-dot${item.current ? ' resume-dot--current' : ''}`} />
- + {item.category}
diff --git a/src/styles.css b/src/styles.css index 1c704a1..d2bd81f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -393,31 +393,34 @@ p { .project-list { display: grid; - gap: 1.25rem; + gap: 1rem; } .project-card { position: relative; - font-size: 1.2rem; - padding: 1.7rem; border: 1px solid var(--line); - background: var(--surface); - box-shadow: 0 20px 40px var(--shadow); - display: grid; - gap: 0.9rem; + border-radius: 10px; + background: var(--term-bg); + padding: 1.15rem 1.3rem; + cursor: pointer; opacity: 0; - transform: translateY(12px); + transform: translateY(14px); animation: fadeIn 0.5s ease forwards; animation-delay: var(--delay); - cursor: pointer; transition: border-color 0.2s ease; } .project-card-title { - font-size: 1.35rem; + margin: 0; + font-size: 1.25rem; letter-spacing: 0.02em; } +.project-card-arrow { + color: var(--muted); + font-weight: 400; +} + .project-title-link::after { content: ''; position: absolute; @@ -425,27 +428,81 @@ p { } .project-card-headline { - margin: 0; + margin: 0.5rem 0 0; + font-size: 1rem; color: var(--text); font-weight: 700; } .project-card-summary { - margin: 0; + margin: 0.45rem 0 0; + font-size: 0.95rem; + line-height: 1.65; } -.project-card-meta { - margin: 0; - font-size: 0.9rem; - color: var(--muted); +.project-card-foot { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: baseline; + gap: 0.35rem 1rem; + margin-top: 0.9rem; } -.project-card-meta a { +.project-card-link { position: relative; z-index: 1; - color: var(--muted); + font-size: 0.85rem; + color: var(--term-green); border-bottom: 1px solid transparent; - transition: color 0.2s ease, border-color 0.2s ease; + transition: border-color 0.2s ease; +} + +.project-card-link-arrow { + color: var(--muted); +} + +.project-card-tags { + font-size: 0.8rem; + color: var(--muted); + opacity: 0.75; +} + +.project-head { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.9rem; +} + +.project-head .tag { + margin-bottom: 0; +} + +.project-detail { + border: 1px solid var(--line); + border-radius: 10px; + background: var(--term-bg); + padding: 1.5rem 1.6rem; + font-size: 1.05rem; + opacity: 0; + transform: translateY(14px); + animation: fadeIn 0.5s ease forwards; +} + +.project-detail-link { + margin: 0 0 1.2rem; +} + +.project-detail-paragraph { + opacity: 0; + transform: translateY(12px); + animation: fadeIn 0.5s ease forwards; + animation-delay: var(--delay); +} + +.project-detail-paragraph:last-child { + margin-bottom: 0; } .project-breadcrumb { @@ -464,7 +521,7 @@ p { } .project-headline { - margin-top: 0; + margin: 0 0 0.9rem; color: var(--text); font-weight: 700; } @@ -520,7 +577,7 @@ p { padding: 1.15rem 1.3rem; } -.resume-tag { +.tag { display: inline-block; margin-bottom: 0.55rem; padding: 0.1rem 0.6rem; @@ -532,17 +589,19 @@ p { color: var(--muted); } -.resume-tag--education { +.tag--education, +.tag--paper { color: var(--term-key); border-color: color-mix(in srgb, var(--term-key) 40%, transparent); } -.resume-tag--work { +.tag--work, +.tag--software { color: var(--term-blue); border-color: color-mix(in srgb, var(--term-blue) 40%, transparent); } -.resume-tag--internship { +.tag--internship { color: var(--term-purple); border-color: color-mix(in srgb, var(--term-purple) 40%, transparent); } @@ -693,9 +752,8 @@ p { border-color: var(--text); } - .project-card-meta a:hover { - color: var(--text); - border-color: var(--text); + .project-card-link:hover { + border-color: var(--term-green); } .project-breadcrumb-link:hover { @@ -786,14 +844,28 @@ p { } .project-card { - font-size: 1rem; + padding: 1rem 1.1rem; opacity: 1; transform: none; animation: none; } .project-card-title { - font-size: 1.2rem; + font-size: 1.15rem; + } + + .project-detail { + padding: 1.2rem 1.25rem; + font-size: 1rem; + opacity: 1; + transform: none; + animation: none; + } + + .project-detail-paragraph { + opacity: 1; + transform: none; + animation: none; } .footer-impressum { From 131e99abb7dbbab506ffee44beb9ca1102dba45f Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:17:29 +0200 Subject: [PATCH 07/13] Restyle imprint page to the shared card design The legal text now sits in the same dark bordered card as the other pages, with a neutral 'legal' pill next to the title and the same fade-up entrance; the old shadow box styling is gone. Also round the not-found note and drop unused social-links CSS. Co-Authored-By: Claude Fable 5 --- src/pages/Impressum.jsx | 5 ++++- src/styles.css | 36 +++++++++++++----------------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/pages/Impressum.jsx b/src/pages/Impressum.jsx index 735ca1b..7675d69 100644 --- a/src/pages/Impressum.jsx +++ b/src/pages/Impressum.jsx @@ -7,7 +7,10 @@ export default function Impressum() { return (
-

imprint/

+
+

imprint/

+ legal +
Date: Sat, 13 Jun 2026 00:21:23 +0200 Subject: [PATCH 08/13] Render section signs and parentheses with a normal-looking glyph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Space Mono draws § and () as small raised glyphs that read as broken superscripts in the legal text. A unicode-range @font-face now serves just these three characters from a local mono font (Menlo/Consolas/ DejaVu/Courier New) while everything else stays Space Mono. Co-Authored-By: Claude Fable 5 --- src/styles.css | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/styles.css b/src/styles.css index 8bfee64..ebcf1dd 100644 --- a/src/styles.css +++ b/src/styles.css @@ -43,6 +43,15 @@ --term-purple: #c89bf5; } +/* Space Mono draws § and parentheses as small raised glyphs that read as + broken superscripts; serve just these characters from a local mono font. */ +@font-face { + font-family: 'Space Mono Glyph Fix'; + src: local('Menlo'), local('Consolas'), local('DejaVu Sans Mono'), + local('Courier New'); + unicode-range: U+00A7, U+0028-0029; +} + * { box-sizing: border-box; } @@ -55,7 +64,7 @@ body { margin: 0; min-height: 100vh; min-height: 100svh; - font-family: 'Space Mono', monospace; + font-family: 'Space Mono Glyph Fix', 'Space Mono', monospace; color: var(--text); background: var(--bg); overflow-x: hidden; From 20876ba3dddea0b23024ad5f91be01273f5b40a6 Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:25:35 +0200 Subject: [PATCH 09/13] Polish mobile layout and enable all animations on small screens On mobile the terminal profile now centers the photo (64% width) with name and role centered beneath it, and resume cards put the period on its own line under the title. The small-screen animation kill switches are removed so the typing intro, timeline drawing, and card cascades play on phones too; prefers-reduced-motion still disables everything. Co-Authored-By: Claude Fable 5 --- src/pages/Home.jsx | 5 ++--- src/styles.css | 40 +++++++++++++++------------------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 56f3ba6..bab91b8 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -91,10 +91,9 @@ function Reveal({ open, children }) { export default function Home() { const reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); - const smallScreen = useMediaQuery('(max-width: 720px)'); const [skipIntro] = useState(() => introPlayed); const { stage, typedCd, typedCat } = useTerminalTimeline( - reduceMotion || smallScreen || skipIntro + reduceMotion || skipIntro ); useEffect(() => { @@ -125,7 +124,7 @@ export default function Home() { className="terminal-photo" src={profilePicture1200} srcSet={`${profilePicture800} 533w, ${profilePicture1200} 800w, ${profilePictureFull} 1000w`} - sizes="(max-width: 720px) 70vw, 220px" + sizes="(max-width: 720px) 64vw, 220px" alt="Portrait of David Bingmann" decoding="async" fetchPriority="high" diff --git a/src/styles.css b/src/styles.css index ebcf1dd..367bf34 100644 --- a/src/styles.css +++ b/src/styles.css @@ -792,11 +792,21 @@ p { .terminal-profile { flex-direction: column; + align-items: center; gap: 1rem; } .terminal-photo { - width: min(240px, 70%); + width: min(240px, 64%); + } + + .terminal-facts { + width: 100%; + } + + .terminal-name, + .terminal-role { + text-align: center; } .terminal-name { @@ -806,9 +816,6 @@ p { .section-body { font-size: 1rem; padding: 1.2rem 1.25rem; - opacity: 1; - transform: none; - animation: none; } .section-body h2 { @@ -819,11 +826,6 @@ p { padding-left: 1.2rem; } - .resume-timeline::before { - transform: none; - animation: none; - } - .resume-dot { left: calc(-1.2rem - 4px); } @@ -836,17 +838,14 @@ p { font-size: 1.05rem; } - .resume-item { - opacity: 1; - transform: none; - animation: none; + .resume-card-row { + flex-direction: column; + align-items: flex-start; + gap: 0.15rem; } .project-card { padding: 1rem 1.1rem; - opacity: 1; - transform: none; - animation: none; } .project-card-title { @@ -856,15 +855,6 @@ p { .project-detail { padding: 1.2rem 1.25rem; font-size: 1rem; - opacity: 1; - transform: none; - animation: none; - } - - .project-detail-paragraph { - opacity: 1; - transform: none; - animation: none; } .footer-impressum { From d228574b890450c063ce6b9caecdefb48bf8fbbb Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:26:44 +0200 Subject: [PATCH 10/13] Ignore local brainstorm mockups Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3139398..cf5a2ef 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ CLAUDE.md node_modules/ dist/ npm-debug.log* +.superpowers/ From 17debbbc934a9fb748ab004c0265809b0974aab5 Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:30:59 +0200 Subject: [PATCH 11/13] Enlarge home terminal slightly Widen the window to 820px and bump body text, padding, photo, and name size to match. Co-Authored-By: Claude Fable 5 --- src/pages/Home.jsx | 2 +- src/styles.css | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index bab91b8..1b41ce7 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -124,7 +124,7 @@ export default function Home() { className="terminal-photo" src={profilePicture1200} srcSet={`${profilePicture800} 533w, ${profilePicture1200} 800w, ${profilePictureFull} 1000w`} - sizes="(max-width: 720px) 64vw, 220px" + sizes="(max-width: 720px) 64vw, 250px" alt="Portrait of David Bingmann" decoding="async" fetchPriority="high" diff --git a/src/styles.css b/src/styles.css index 367bf34..50882c2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -143,7 +143,7 @@ p { } .terminal { - max-width: 720px; + max-width: 820px; margin: 0 auto; border: 1px solid var(--term-border); border-radius: 12px; @@ -185,8 +185,8 @@ p { } .terminal-body { - padding: 1.4rem 1.5rem 1.5rem; - font-size: 1rem; + padding: 1.6rem 1.7rem 1.7rem; + font-size: 1.08rem; line-height: 1.75; color: var(--term-text); } @@ -245,7 +245,7 @@ p { } .terminal-photo { - width: min(220px, 38%); + width: min(250px, 38%); aspect-ratio: 2 / 3; object-fit: cover; border-radius: 8px; @@ -260,7 +260,7 @@ p { .terminal-name { margin: 0; - font-size: 1.5rem; + font-size: 1.6rem; font-weight: 700; color: var(--term-text); line-height: 1.3; From 8410d7bcd238a6d31e2b30f3dc2f9fdfd02028b8 Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:31:41 +0200 Subject: [PATCH 12/13] Let the home terminal fill the content column Drop the max-width cap so the window spans the full 900px main column and scale text, padding, photo, and name up with it. Co-Authored-By: Claude Fable 5 --- src/pages/Home.jsx | 2 +- src/styles.css | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 1b41ce7..3473e88 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -124,7 +124,7 @@ export default function Home() { className="terminal-photo" src={profilePicture1200} srcSet={`${profilePicture800} 533w, ${profilePicture1200} 800w, ${profilePictureFull} 1000w`} - sizes="(max-width: 720px) 64vw, 250px" + sizes="(max-width: 720px) 64vw, 290px" alt="Portrait of David Bingmann" decoding="async" fetchPriority="high" diff --git a/src/styles.css b/src/styles.css index 50882c2..88ced37 100644 --- a/src/styles.css +++ b/src/styles.css @@ -143,7 +143,6 @@ p { } .terminal { - max-width: 820px; margin: 0 auto; border: 1px solid var(--term-border); border-radius: 12px; @@ -185,8 +184,8 @@ p { } .terminal-body { - padding: 1.6rem 1.7rem 1.7rem; - font-size: 1.08rem; + padding: 1.8rem 1.9rem 1.9rem; + font-size: 1.15rem; line-height: 1.75; color: var(--term-text); } @@ -245,7 +244,7 @@ p { } .terminal-photo { - width: min(250px, 38%); + width: min(290px, 40%); aspect-ratio: 2 / 3; object-fit: cover; border-radius: 8px; @@ -260,7 +259,7 @@ p { .terminal-name { margin: 0; - font-size: 1.6rem; + font-size: 1.8rem; font-weight: 700; color: var(--term-text); line-height: 1.3; From c0edc0e4461edb670b6958000087b2efd0b65202 Mon Sep 17 00:00:00 2001 From: David Bingmann Date: Sat, 13 Jun 2026 00:32:58 +0200 Subject: [PATCH 13/13] Bring social links back into the terminal fact sheet Icon links for LinkedIn, mail, GitHub, and X sit at the bottom of the fact column again (centered on mobile); the footer keeps its copy for the other pages. Co-Authored-By: Claude Fable 5 --- src/pages/Home.jsx | 26 ++++++++++++++++++++++++++ src/styles.css | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 3473e88..b03f03a 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,4 +1,6 @@ import { useEffect, useState } from 'react'; +import { FaEnvelope, FaGithub, FaLinkedinIn } from 'react-icons/fa'; +import { FaXTwitter } from 'react-icons/fa6'; import profilePicture800 from '../assets/profile_picture_800.jpeg'; import profilePicture1200 from '../assets/profile_picture_1200.jpeg'; import profilePictureFull from '../assets/profile_picture.jpeg'; @@ -19,6 +21,17 @@ const STAGE = { // not every time the user navigates back to home via the tabs. let introPlayed = false; +const socialLinks = [ + { + label: 'LinkedIn', + href: 'https://www.linkedin.com/in/david-bingmann-13b897293/', + Icon: FaLinkedinIn, + }, + { label: 'Mail', href: 'mailto:contact@davidbingmann.de', Icon: FaEnvelope }, + { label: 'GitHub', href: 'https://github.com/davidbingmann', Icon: FaGithub }, + { label: 'X', href: 'https://x.com/dxv1d04', Icon: FaXTwitter }, +]; + function useMediaQuery(query) { const [matches, setMatches] = useState( () => typeof window !== 'undefined' && window.matchMedia(query).matches @@ -145,6 +158,19 @@ export default function Home() { Work Research assistant @ DFKI

+
+ {socialLinks.map(({ label, href, Icon }) => ( + + + ))} +
diff --git a/src/styles.css b/src/styles.css index 88ced37..1ef9558 100644 --- a/src/styles.css +++ b/src/styles.css @@ -289,6 +289,24 @@ p { font-weight: 700; } +.terminal-socials { + display: flex; + gap: 1.1rem; + margin-top: auto; + padding-top: 1rem; +} + +.terminal-socials a { + display: inline-flex; + color: var(--term-muted); + transition: color 0.2s ease; +} + +.terminal-social-icon { + width: 21px; + height: 21px; +} + .terminal-about { margin: 0.4rem 0 1.2rem; color: var(--term-muted); @@ -755,6 +773,10 @@ p { border-color: var(--term-green); } + .terminal-socials a:hover { + color: var(--term-text); + } + .project-breadcrumb-link:hover { color: var(--text); border-color: var(--text); @@ -808,6 +830,10 @@ p { text-align: center; } + .terminal-socials { + justify-content: center; + } + .terminal-name { font-size: 1.3rem; }