redesign the site as a technical minimalist TUI

Replace the glassy hero treatment with a terminal and TUI vocabulary
while keeping the layout minimal:

- Switch the type stack to JetBrains Mono and flatten every surface:
  hairline borders, square corners, no blur or glow, and a faint
  graph-paper grid in place of the radial gradients.
- Rebuild the home hero as a terminal window whose output is typed in
  on load; the old mouse-parallax glow script becomes that typing
  sequence, and it no-ops under prefers-reduced-motion.
- Give the projects, about, and entry panels title bars, and render
  cards with a metadata strip, status dot, ISO date, and prompt-style
  links.
- Hide the inline nav behind the hamburger on narrow screens; the old
  rule was outranked by the #menu id selector and never applied.
- Add an SVG favicon, which also clears the only 404 on the site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
SowinskiBraedenandClaude Opus 5 committed 2026-10-17 23:41:38 -07:00
1 parent 2e67914fd6
commit 8595290318
10 files changed
+1270 -1167

No files matched your search

+68 -59
View File
@@ -9,7 +9,7 @@
<meta name='keywords' content="Braeden Sowinski, sowinski.dev, about, software engineer, backend systems, BCIT" /> <meta name='keywords' content="Braeden Sowinski, sowinski.dev, about, software engineer, backend systems, BCIT" />
<meta name='viewport' content='width=device-width, initial-scale=1.0'> <meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='robots' content='index, follow'> <meta name='robots' content='index, follow'>
<meta name='theme-color' content='#1d1e20'> <meta name='theme-color' content='#0c0d10'>
<link rel='canonical' href='https://sowinski.dev/about.html'> <link rel='canonical' href='https://sowinski.dev/about.html'>
<meta property='og:type' content='profile'> <meta property='og:type' content='profile'>
<meta property='og:title' content='About Braeden Sowinski | Software Engineer'> <meta property='og:title' content='About Braeden Sowinski | Software Engineer'>
@@ -67,11 +67,12 @@
} }
</script> </script>
<link rel="icon" type="image/svg+xml" href="./public/images/favicon.svg">
<link rel="stylesheet" href="./public/css/style.css"> <link rel="stylesheet" href="./public/css/style.css">
<link rel="stylesheet" href="./public/css/about.css"> <link rel="stylesheet" href="./public/css/about.css">
</head> </head>
<body class="js-stop-transition"> <body class="js-stop-transition" data-page="about">
<header class="header"> <header class="header">
<nav class="nav"> <nav class="nav">
<div class="logo"> <div class="logo">
@@ -133,20 +134,24 @@
<main class="container page-shell"> <main class="container page-shell">
<div class="left-content"> <div class="left-content">
<section id="profile"> <section id="profile">
<img src="https://avatars.githubusercontent.com/u/48144618" class="profile" alt="Braeden Sowinski profile photo"> <h2 class="panel__bar">profile</h2>
<div class="profile-sub"> <div class="panel__body">
<h1 class="profile-name">Braeden Sowinski</h1> <img src="https://avatars.githubusercontent.com/u/48144618" class="profile" alt="Braeden Sowinski profile photo">
<p class="des profile-role"> <div class="profile-sub">
Software Engineer &amp; Systems Admin | CST Student @ BCIT | C4A President <h1 class="profile-name">Braeden Sowinski</h1>
</p> <p class="des profile-role">
<p class="des profile-focus"> Software Engineer &amp; Systems Admin | CST Student @ BCIT | C4A President
Backend systems, APIs, data-driven software, and low-level problem solving </p>
</p> <p class="des profile-focus">
Backend systems, APIs, data-driven software, and low-level problem solving
</p>
</div>
</div> </div>
</section> </section>
<section id="skills"> <section id="skills">
<h3>Skills</h3> <h2 class="panel__bar">skills</h2>
<ul class="text skills-list"> <div class="panel__body">
<ul class="text skills-list">
<li>Java, C, Go, OCaml</li> <li>Java, C, Go, OCaml</li>
<li>JavaScript and Node.js</li> <li>JavaScript and Node.js</li>
<li>Python3+</li> <li>Python3+</li>
@@ -160,64 +165,68 @@
<li>Oral & Written Communication</li> <li>Oral & Written Communication</li>
<li>Team Collaboration & Adaptability</li> <li>Team Collaboration & Adaptability</li>
</ul> </ul>
</div>
</section> </section>
</div> </div>
<section class="about" id="about"> <section class="about" id="about">
<h2 class="title">About Me</h2> <h2 class="panel__bar title">about_me</h2>
<p class="text"> <div class="panel__body">
I'm Braeden Sowinski, a Computer Systems Technology student at <p class="text">
<abbr title="British Columbia Institute of Technology">BCIT</abbr> focused on backend development, I'm Braeden Sowinski, a Computer Systems Technology student at
systems administration, and building practical software that solves real problems. <abbr title="British Columbia Institute of Technology">BCIT</abbr> focused on backend development,
</p> systems administration, and building practical software that solves real problems.
</p>
<p class="text"> <p class="text">
My work spans APIs, full-stack applications, data-driven tools, server infrastructure, My work spans APIs, full-stack applications, data-driven tools, server infrastructure,
and software built around real users and workflows. Through personal projects, freelance work, and software built around real users and workflows. Through personal projects, freelance work,
collaborative BCIT projects, and open-source development, I’ve built experience with C, Go, Java, collaborative BCIT projects, and open-source development, I’ve built experience with C, Go, Java,
JavaScript, TypeScript, Python, Docker, MongoDB, PostgreSQL, and Debian-based systems. JavaScript, TypeScript, Python, Docker, MongoDB, PostgreSQL, and Debian-based systems.
</p> </p>
<p class="text"> <p class="text">
Outside of my own projects and coursework, I’m involved in BCIT’s student tech community as Outside of my own projects and coursework, I’m involved in BCIT’s student tech community as
President of Coding 4 All (C4A) Club. In this role, I help organize events, workshops, and student-focused President of Coding 4 All (C4A) Club. In this role, I help organize events, workshops, and student-focused
opportunities alongside the BCIT Computing Club, with the goal of helping more students learn, opportunities alongside the BCIT Computing Club, with the goal of helping more students learn,
build, and connect. build, and connect.
</p> </p>
<p class="text" style="margin-bottom: 0;"> <p class="text" style="margin-bottom: 0;">
I’m especially interested in backend systems, infrastructure, analytics, and practical tools I’m especially interested in backend systems, infrastructure, analytics, and practical tools
that turn messy real-world problems into clean, usable software. that turn messy real-world problems into clean, usable software.
</p> </p>
</div>
</section> </section>
<section class="honors" id="honors"> <section class="honors" id="honors">
<h2 class="title">Highlights</h2> <h2 class="panel__bar title">highlights</h2>
<div class="panel__body">
<dl>
<dt class="text"><b>Red Team Hackathon - 3rd Place</b></dt>
<dd class="text">
Team Cerberus Gate earned a third-place finish at the inaugural Red Team Hackathon for building a working
prototype around signal classification and emitter tracking.
<a class="honor-inline-link" href="/entry.html?type=updates&slug=red-team-hackathon-2026">Read the full write-up</a>
<div class="honor-subitem">
<span class="honor-subitem__label">Additional recognition</span>
<p class="honor-subitem__text">Best Dressed Award at the inaugural Red Team Hackathon.</p>
</div>
</dd>
<dl> <dt class="text"><b>Capstone Project Recognition</b></dt>
<dt class="text"><b>Red Team Hackathon - 3rd Place</b></dt> <dd class="text">
<dd class="text"> Issued by <abbr title="Sullivan Heights Secondary School">SHSS</abbr> for work on schedule-generating
Team Cerberus Gate earned a third-place finish at the inaugural Red Team Hackathon for building a working software used to produce student schedules and master timetables.
prototype around signal classification and emitter tracking. </dd>
<a class="honor-inline-link" href="/entry.html?type=updates&slug=red-team-hackathon-2026">Read the full write-up</a>
<div class="honor-subitem">
<span class="honor-subitem__label">Additional recognition</span>
<p class="honor-subitem__text">Best Dressed Award at the inaugural Red Team Hackathon.</p>
</div>
</dd>
<dt class="text"><b>Capstone Project Recognition</b></dt> <dt class="text"><b>Master Digital Crafter</b></dt>
<dd class="text"> <dd class="text">
Issued by <abbr title="Sullivan Heights Secondary School">SHSS</abbr> for work on schedule-generating Issued by <abbr title="Sullivan Heights Secondary School">SHSS</abbr> for excellence in Web Development 10
software used to produce student schedules and master timetables. and Software Engineering 11/12.
</dd> </dd>
</dl>
<dt class="text"><b>Master Digital Crafter</b></dt> </div>
<dd class="text">
Issued by <abbr title="Sullivan Heights Secondary School">SHSS</abbr> for excellence in Web Development 10
and Software Engineering 11/12.
</dd>
</dl>
</section> </section>
<footer class="footer flex-container"> <footer class="footer flex-container">
+3 -2
View File
@@ -9,7 +9,7 @@
<meta name='keywords' content="Braeden Sowinski, sowinski.dev, portfolio entry, project, update" /> <meta name='keywords' content="Braeden Sowinski, sowinski.dev, portfolio entry, project, update" />
<meta name='viewport' content='width=device-width, initial-scale=1.0'> <meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='robots' content='index, follow'> <meta name='robots' content='index, follow'>
<meta name='theme-color' content='#1d1e20'> <meta name='theme-color' content='#0c0d10'>
<link rel='canonical' href='https://sowinski.dev/entry.html'> <link rel='canonical' href='https://sowinski.dev/entry.html'>
<meta property='og:type' content='article'> <meta property='og:type' content='article'>
<meta property='og:title' content='Portfolio Entry | Braeden Sowinski'> <meta property='og:title' content='Portfolio Entry | Braeden Sowinski'>
@@ -46,11 +46,12 @@
})(); })();
</script> </script>
<link rel="icon" type="image/svg+xml" href="./public/images/favicon.svg">
<link rel="stylesheet" href="./public/css/style.css"> <link rel="stylesheet" href="./public/css/style.css">
<link rel="stylesheet" href="./public/css/projects.css"> <link rel="stylesheet" href="./public/css/projects.css">
</head> </head>
<body class="js-stop-transition"> <body class="js-stop-transition" data-page="projects">
<header class="header"> <header class="header">
<nav class="nav"> <nav class="nav">
<div class="logo"> <div class="logo">
+65 -51
View File
@@ -9,7 +9,7 @@
<meta name='keywords' content="Braeden Sowinski, sowinski.dev, software engineer, backend systems, APIs, BCIT, portfolio" /> <meta name='keywords' content="Braeden Sowinski, sowinski.dev, software engineer, backend systems, APIs, BCIT, portfolio" />
<meta name='viewport' content='width=device-width, initial-scale=1.0'> <meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='robots' content='index, follow'> <meta name='robots' content='index, follow'>
<meta name='theme-color' content='#1d1e20'> <meta name='theme-color' content='#0c0d10'>
<meta name='application-name' content='sowinski.dev'> <meta name='application-name' content='sowinski.dev'>
<link rel='canonical' href='https://sowinski.dev/'> <link rel='canonical' href='https://sowinski.dev/'>
<meta property='og:type' content='website'> <meta property='og:type' content='website'>
@@ -105,10 +105,11 @@
} }
</script> </script>
<link rel="icon" type="image/svg+xml" href="./public/images/favicon.svg">
<link rel="stylesheet" href="./public/css/style.css"> <link rel="stylesheet" href="./public/css/style.css">
</head> </head>
<body class="js-stop-transition"> <body class="js-stop-transition" data-page="home">
<header class="header"> <header class="header">
<nav class="nav"> <nav class="nav">
<div class="logo"> <div class="logo">
@@ -167,59 +168,72 @@
</div> </div>
</header> </header>
<main class="main hero-main"> <main class="main">
<div class="hero-shell"> <section class="term">
<div class="hero-shell__shine"></div> <div class="term__bar">
<img src="https://avatars.githubusercontent.com/u/48144618" class="profile" alt="Braeden Sowinski profile photo"> <span class="term__title">braeden@sowinski.dev: <b>~</b></span>
<div id="main-wrapper" class="svg-wrapper"> <span class="term__dots">&#9472; &#9633; &#10005;</span>
<svg height="60" width="320" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
<rect x="0" y="0" class="shape" height="60" width="320" />
</svg>
<h1 class="name">Braeden Sowinski</h1>
</div> </div>
<div class="term__body">
<div class="term__avatar-wrap">
<img src="https://avatars.githubusercontent.com/u/48144618" class="term__avatar" alt="Braeden Sowinski profile photo">
<span class="term__avatar-caption">./braeden.png</span>
</div>
<p class="des hero-description"> <div class="term__stream">
Software Engineer & Systems Admin | CST Student @ BCIT <p class="cmd">whoami</p>
</p> <h1 class="name line">Braeden Sowinski</h1>
<p class="des hero-description hero-description-secondary">
Backend systems, APIs, data-driven software, and low-level problem solving
</p>
<div class="hero-badges"> <p class="cmd">cat ./role</p>
<span class="badge">BCIT CST Diploma</span> <p class="des line">
<span class="badge">C4A President</span> Software Engineer &amp; Systems Admin | CST Student @ BCIT
<span class="badge">Hackathon Podium Finisher</span> </p>
<p class="des line hero-description-secondary">
Backend systems, APIs, data-driven software, and low-level problem solving
</p>
<p class="cmd">ls ./highlights</p>
<div class="hero-badges line">
<span class="badge">BCIT CST Diploma</span>
<span class="badge">C4A President</span>
<span class="badge">Hackathon Podium Finisher</span>
</div>
<p class="cmd">ls ./pages</p>
<div class="hero-actions line">
<a class="button button-primary" href="./projects.html">projects</a>
<a class="button button-secondary" href="./about.html">about</a>
</div>
<p class="cmd">cat ./contact</p>
<div class="social-icons line">
<a class="icon" href="https://www.linkedin.com/in/SowinskiBraeden" target="_blank" rel="noopener noreferrer me"
title="LinkedIn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-linkedin"
viewBox="0 0 16 16">
<path
d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z" />
</svg>
</a>
<a class="icon" href="https://git.sowinski.dev/SowinskiBraeden" target="_blank" rel="noopener noreferrer me" title="Gitea" aria-label="Gitea">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path
d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z" />
</svg>
</a>
<a class="icon" href="mailto:braeden@sowinski.dev" target="_blank" rel="noopener noreferrer me" title="Email">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 21" fill="none" stroke="currentcolor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
</a>
</div>
<p class="prompt-end">$ <span class="caret"></span></p>
</div>
</div> </div>
</section>
<div class="hero-actions">
<a class="button button-primary" href="./projects.html">View Projects</a>
<a class="button button-secondary" href="./about.html">About Me</a>
</div>
<div class="social-icons">
<a class="icon" href="https://www.linkedin.com/in/SowinskiBraeden" target="_blank" rel="noopener noreferrer me"
title="LinkedIn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-linkedin"
viewBox="0 0 16 16">
<path
d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z" />
</svg>
</a>
<a class="icon" href="https://git.sowinski.dev/SowinskiBraeden" target="_blank" rel="noopener noreferrer me" title="Gitea" aria-label="Gitea">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path
d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z" />
</svg>
</a>
<a class="icon" href="mailto:braeden@sowinski.dev" target="_blank" rel="noopener noreferrer me" title="Email">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 21" fill="none" stroke="currentcolor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
</a>
</div>
</div>
</main> </main>
<script src="./public/scripts/mobile.js"></script> <script src="./public/scripts/mobile.js"></script>
+12 -11
View File
@@ -9,7 +9,7 @@
<meta name='keywords' content="Braeden Sowinski, sowinski.dev, projects, updates, portfolio, software engineer, BCIT" /> <meta name='keywords' content="Braeden Sowinski, sowinski.dev, projects, updates, portfolio, software engineer, BCIT" />
<meta name='viewport' content='width=device-width, initial-scale=1.0'> <meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='robots' content='index, follow'> <meta name='robots' content='index, follow'>
<meta name='theme-color' content='#1d1e20'> <meta name='theme-color' content='#0c0d10'>
<link rel='canonical' href='https://sowinski.dev/projects.html'> <link rel='canonical' href='https://sowinski.dev/projects.html'>
<meta property='og:type' content='website'> <meta property='og:type' content='website'>
<meta property='og:title' content='Braeden Sowinski Projects and Updates | Software Portfolio'> <meta property='og:title' content='Braeden Sowinski Projects and Updates | Software Portfolio'>
@@ -43,11 +43,12 @@
} }
</script> </script>
<link rel="icon" type="image/svg+xml" href="./public/images/favicon.svg">
<link rel="stylesheet" href="./public/css/style.css"> <link rel="stylesheet" href="./public/css/style.css">
<link rel="stylesheet" href="./public/css/projects.css"> <link rel="stylesheet" href="./public/css/projects.css">
</head> </head>
<body class="js-stop-transition"> <body class="js-stop-transition" data-page="projects">
<header class="header"> <header class="header">
<nav class="nav"> <nav class="nav">
<div class="logo"> <div class="logo">
@@ -108,7 +109,7 @@
<main class="projects-page page-shell"> <main class="projects-page page-shell">
<section class="projects-hero"> <section class="projects-hero">
<p class="eyebrow">Projects & Updates</p> <p class="cmd">ls ~/projects --all</p>
<h1>Braeden Sowinski projects, milestones, and technical writeups.</h1> <h1>Braeden Sowinski projects, milestones, and technical writeups.</h1>
<p class="projects-intro"> <p class="projects-intro">
A collection of backend, infrastructure, data-driven, and full-stack projects, A collection of backend, infrastructure, data-driven, and full-stack projects,
@@ -118,27 +119,27 @@
<section class="content-section"> <section class="content-section">
<div class="section-heading"> <div class="section-heading">
<div> <h2>selected_projects</h2>
<p class="eyebrow">Portfolio</p> <div class="section-heading__right">
<h2>Selected Projects</h2> <span id="projects-count" class="section-count"></span>
<a class="section-link" href="https://git.sowinski.dev/SowinskiBraeden" target="_blank" rel="noopener noreferrer">gitea</a>
</div> </div>
<a class="section-link" href="https://git.sowinski.dev/SowinskiBraeden" target="_blank" rel="noopener noreferrer">View Gitea</a>
</div> </div>
<div id="projects-grid" class="content-grid"> <div id="projects-grid" class="content-grid">
<p class="loading-state">Loading projects...</p> <p class="loading-state">Loading projects...</p>
</div> </div>
<div id="projects-toggle-shell" class="projects-toggle-shell" hidden> <div id="projects-toggle-shell" class="projects-toggle-shell" hidden>
<button id="projects-toggle" class="projects-toggle" type="button" aria-expanded="false"> <button id="projects-toggle" class="projects-toggle" type="button" aria-expanded="false">
Show More Projects show more projects
</button> </button>
</div> </div>
</section> </section>
<section class="content-section updates-section"> <section class="content-section updates-section">
<div class="section-heading"> <div class="section-heading">
<div> <h2>updates_and_achievements</h2>
<p class="eyebrow">Recent</p> <div class="section-heading__right">
<h2>Updates & Achievements</h2> <span id="updates-count" class="section-count"></span>
</div> </div>
</div> </div>
<div id="updates-grid" class="content-grid updates-grid"> <div id="updates-grid" class="content-grid updates-grid">
+201 -151
View File
@@ -1,193 +1,243 @@
.container { .container {
display: grid; display: grid;
grid-template-columns: minmax(280px, 320px) repeat(3, 1fr); grid-template-columns: minmax(260px, 300px) repeat(3, 1fr);
grid-template-areas: grid-template-areas:
"pro about about about" "pro about about about"
"pro other other other" "pro other other other"
"footer footer footer footer"; "footer footer footer footer";
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
gap: 24px; gap: 18px;
padding-top: 18px; padding-top: 22px;
min-height: calc(100vh - 132px); min-height: calc(100vh - 120px);
align-items: stretch; align-items: stretch;
} }
.left-content { .left-content {
grid-area: pro; grid-area: pro;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 18px;
} }
.about { .about {
grid-area: about; grid-area: about;
} }
.honors { .honors {
grid-area: other; grid-area: other;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} }
.footer { .footer {
grid-area: footer; grid-area: footer;
} }
.about { /* ----------------------------------------------------------- TUI panels */
display: flex;
flex-direction: column;
}
section { section {
transition: 0.8s; padding: 0;
padding: 24px; background: var(--surface);
background: border: 1px solid var(--border);
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.025)), display: flex;
linear-gradient(135deg, rgba(235, 82, 52, 0.05), transparent 48%), flex-direction: column;
rgba(20, 21, 24, 0.36);
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 18px 44px rgba(0, 0, 0, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.12),
inset 0 -1px 0 rgba(255, 255, 255, 0.03);
backdrop-filter: blur(14px) saturate(120%);
-webkit-backdrop-filter: blur(14px) saturate(120%);
} }
body.light section { .panel__bar {
background: margin: 0;
linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(255, 255, 255, 0.42)), padding: 6px 12px;
linear-gradient(135deg, rgba(235, 82, 52, 0.04), transparent 48%), background: var(--bar);
rgba(255, 255, 255, 0.34); border-bottom: 1px solid var(--border);
border-color: rgba(255, 255, 255, 0.52); font-size: 0.92rem;
box-shadow: font-weight: 700;
0 16px 36px rgba(0, 0, 0, 0.06), letter-spacing: 0.02em;
inset 0 1px 0 rgba(255, 255, 255, 0.72), color: var(--bright);
inset 0 -1px 0 rgba(255, 255, 255, 0.22);
} }
section h3 { .panel__bar::before {
margin-top: 0; content: "▸ ";
margin-bottom: 14px; color: var(--secondary);
font-size: 1.2rem;
} }
#profile { .panel__body {
background-image: linear-gradient(rgba(0, 0, 0, 0.24), rgba(0, 0, 0, 0.24)), url("../images/pic.png"); padding: 16px;
background-size: cover; flex: 1;
background-position: center;
color: #d8d8d9;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
margin-bottom: 0;
} }
#skills { #skills {
border-top-left-radius: 0; flex: 1;
border-top-right-radius: 0;
flex: 1;
} }
#profile img { #profile .panel__body {
border: 4px solid rgba(235, 82, 52, 0.18); display: flex;
box-shadow: var(--shadow); flex-direction: column;
gap: 14px;
}
#profile img.profile {
width: 100%;
max-width: 180px;
aspect-ratio: 1;
object-fit: cover;
border: 1px solid var(--border-strong);
border-radius: 0;
filter: grayscale(0.3) contrast(1.03);
} }
.profile-sub { .profile-sub {
background: padding: 0;
linear-gradient(180deg, rgba(0, 0, 0, 0.56), rgba(0, 0, 0, 0.42)), border: 0;
rgba(0, 0, 0, 0.36); background: transparent;
padding: 16px;
border-radius: 14px;
margin-top: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
} }
.profile-sub .profile-name,
.profile-sub .profile-role,
.profile-sub .profile-focus {
color: #f4f1ed;
}
.skills-list {
padding-left: 18px;
}
.skills-list li {
margin-bottom: 8px;
}
.text {
font-weight: 400;
transition: 0.8s;
margin: 0 0 14px;
}
dt {
margin-top: 16px;
}
dd {
margin-left: 0;
color: var(--muted);
}
.honor-inline-link {
display: inline-block;
margin-top: 8px;
}
.honor-subitem {
margin-top: 10px;
padding-left: 14px;
}
.honor-subitem__label {
display: inline-block;
font-size: 0.74rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.honor-subitem__text {
margin: 4px 0 0;
color: var(--muted);
}
.honors dl,
.skills-list {
margin-bottom: 0;
}
@media only screen and (max-width: 920px) {
.container {
display: flex;
flex-direction: column;
min-height: auto;
}
#skills {
margin-bottom: 0;
}
}
.profile-name { .profile-name {
margin: 0 0 6px; margin: 0 0 4px;
font-size: clamp(1.45rem, 2vw, 1.9rem); font-size: 1.15rem;
line-height: 1.1; line-height: 1.25;
} }
.profile-role, .profile-role,
.profile-focus { .profile-focus {
margin: 0; margin: 0;
font-size: 0.82rem;
color: var(--primary);
padding-left: 14px;
text-indent: -14px;
} }
.profile-focus { .profile-focus {
margin-top: 6px; margin-top: 6px;
color: rgba(255, 255, 255, 0.76); color: var(--muted);
}
.profile-role::before,
.profile-focus::before {
content: "#\00a0";
color: var(--border-strong);
}
/* ------------------------------------------------------------- contents */
.skills-list {
list-style: none;
padding: 0;
margin: 0;
font-size: 0.85rem;
}
.skills-list li {
margin-bottom: 4px;
color: var(--primary);
padding-left: 14px;
text-indent: -14px;
}
.skills-list li::before {
content: "-\00a0";
color: var(--secondary);
}
.text {
font-weight: 400;
margin: 0 0 14px;
font-size: 0.9rem;
}
.about .panel__body,
.honors .panel__body {
max-width: 92ch;
}
.about .text:last-child {
margin-bottom: 0;
}
.about .text,
.honors .text {
color: var(--primary);
}
.honors dl {
margin: 0;
}
dt {
margin-top: 16px;
}
dt:first-of-type {
margin-top: 0;
}
dt.text {
margin-bottom: 4px;
color: var(--bright);
}
dt.text b::before {
content: "▸ ";
color: var(--secondary);
font-weight: 400;
}
dd {
margin-left: 14px;
color: var(--muted);
border-left: 1px solid var(--border);
padding-left: 12px;
}
dd.text {
color: var(--muted);
}
.honor-inline-link {
display: inline-block;
margin-top: 6px;
font-size: 0.82rem;
color: var(--secondary);
}
.honor-inline-link::before {
content: "> ";
}
.honor-subitem {
margin-top: 10px;
padding-left: 12px;
border-left: 1px solid var(--border);
}
.honor-subitem__label {
display: inline-block;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--secondary);
}
.honor-subitem__text {
margin: 2px 0 0;
color: var(--muted);
font-size: 0.85rem;
}
abbr {
text-decoration: underline dotted var(--border-strong);
cursor: help;
}
@media only screen and (max-width: 920px) {
.container {
display: flex;
flex-direction: column;
min-height: auto;
gap: 14px;
}
#profile img.profile {
max-width: 140px;
}
} }
+332 -132
View File
@@ -1,27 +1,28 @@
.projects-page, .projects-page,
.entry-page { .entry-page {
padding-top: 18px; padding-top: 22px;
} }
/* ----------------------------------------------------------- TUI panels */
.projects-hero, .projects-hero,
.entry-shell, .entry-shell,
.content-section { .content-section {
background: var(--menu-text-bg); background: var(--surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 22px;
padding: 28px;
box-shadow: var(--shadow);
} }
.projects-hero { .projects-hero {
margin-bottom: 24px; margin-bottom: 20px;
padding: 20px;
} }
.projects-hero h1, .projects-hero h1,
.entry-title { .entry-title {
font-size: clamp(2rem, 4vw, 3rem); font-size: clamp(1.3rem, 2.6vw, 1.75rem);
line-height: 1.1; line-height: 1.25;
margin: 6px 0 14px; margin: 8px 0 10px;
max-width: 860px;
} }
.projects-intro, .projects-intro,
@@ -29,287 +30,486 @@
margin: 0; margin: 0;
max-width: 760px; max-width: 760px;
color: var(--muted); color: var(--muted);
font-size: 0.92rem;
} }
.eyebrow { .eyebrow {
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 0.78rem;
color: var(--secondary);
margin: 0; margin: 0;
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--secondary);
font-weight: 700; font-weight: 700;
} }
.content-section { .eyebrow::before {
margin-bottom: 24px; content: "// ";
color: var(--border-strong);
} }
.content-section {
margin-bottom: 20px;
padding: 0;
}
/* Panel title bar, like a window chrome strip in a TUI. */
.section-heading { .section-heading {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: end; align-items: center;
gap: 12px; gap: 12px;
margin-bottom: 18px; margin: 0;
padding: 6px 12px;
background: var(--bar);
border-bottom: 1px solid var(--border);
} }
.section-heading h2 { .section-heading h2 {
margin: 6px 0 0; margin: 0;
font-size: 1.8rem; font-size: 0.95rem;
font-weight: 700;
letter-spacing: 0.02em;
} }
.section-link, .section-heading h2::before {
.card-link, content: "▸ ";
.entry-back, color: var(--secondary);
.inline-button { }
display: inline-flex;
align-items: center; .section-heading .eyebrow {
justify-content: center; display: none;
border-radius: 12px; }
.section-link {
font-size: 12px;
color: var(--muted);
border: 1px solid var(--border); border: 1px solid var(--border);
padding: 10px 14px; padding: 1px 8px;
font-weight: 600; background: var(--surface);
background: transparent;
} }
.section-link:hover, .section-link::before {
.card-link:hover, content: "> ";
.entry-back:hover, color: var(--secondary);
.inline-button:hover { }
border-color: rgba(235, 82, 52, 0.32);
.section-link:hover {
color: var(--bright);
border-color: var(--secondary);
} }
.content-grid { .content-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 18px; gap: 14px;
} padding: 16px;
.content-card.is-hidden {
display: none;
} }
.updates-grid { .updates-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.content-card.is-hidden {
display: none;
}
/* ------------------------------------------------------------ TUI cards */
.content-card { .content-card {
border-radius: 18px; padding: 0;
padding: 22px; background: var(--surface-alt);
background: var(--panel);
border: 1px solid var(--border); border: 1px solid var(--border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100%; min-height: 100%;
scroll-margin-top: 96px; scroll-margin-top: 80px;
transition:
border-color 0.12s linear,
background-color 0.12s linear;
}
.content-card:hover {
border-color: var(--border-strong);
} }
.content-card.featured { .content-card.featured {
border-color: rgba(235, 82, 52, 0.34); border-color: rgba(235, 82, 52, 0.42);
} }
.card-meta, body.light .content-card.featured {
.entry-meta, border-color: rgba(200, 64, 31, 0.42);
.entry-links,
.card-links,
.tag-list,
.entry-tag-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
} }
.card-meta { .card-meta {
margin-bottom: 14px; display: flex;
} flex-wrap: wrap;
.card-kicker,
.card-date,
.entry-pill {
display: inline-flex;
align-items: center; align-items: center;
border-radius: 999px; gap: 10px;
padding: 6px 12px; margin: 0;
font-size: 0.84rem; padding: 4px 12px;
font-weight: 600; background: var(--bar);
border-bottom: 1px solid var(--border);
font-size: 11px;
} }
.card-kicker, .card-kicker {
.entry-pill-primary {
background: rgba(235, 82, 52, 0.16);
color: var(--secondary); color: var(--secondary);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
} }
.card-date, .card-date {
.entry-pill-secondary {
background: rgba(216, 216, 217, 0.08);
color: var(--muted); color: var(--muted);
} }
.card-status {
margin-left: auto;
color: var(--muted);
display: inline-flex;
align-items: center;
gap: 6px;
}
.card-status::before {
content: "";
width: 6px;
height: 6px;
background: var(--muted);
display: inline-block;
}
.card-status[data-state="active"]::before {
background: var(--ok);
}
.card-status[data-state="attention"]::before {
background: var(--secondary);
}
.content-card h3 { .content-card h3 {
margin: 0 0 10px; margin: 0;
font-size: 1.35rem; padding: 12px 12px 0;
line-height: 1.2; font-size: 1.05rem;
line-height: 1.3;
} }
.card-summary { .card-summary {
color: var(--muted); color: var(--muted);
margin: 0 0 18px; margin: 0;
padding: 8px 12px 12px;
font-size: 0.85rem;
flex: 1; flex: 1;
} }
.tag-list,
.entry-tag-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag-list {
padding: 0 12px 12px;
}
.tag, .tag,
.entry-tag { .entry-tag {
display: inline-flex; display: inline-flex;
border-radius: 999px; text-transform: lowercase;
padding: 6px 10px; padding: 0 6px;
font-size: 0.82rem; font-size: 11px;
background: rgba(255, 255, 255, 0.04); color: var(--muted);
background: transparent;
border: 1px solid var(--border); border: 1px solid var(--border);
} }
.tag::before,
.entry-tag::before {
content: "#";
color: var(--border-strong);
margin-right: 2px;
}
.card-links, .card-links,
.entry-links { .entry-links {
margin-top: 18px; display: flex;
flex-wrap: wrap;
gap: 8px;
}
.card-links {
margin-top: auto;
padding: 10px 12px;
border-top: 1px solid var(--border);
background: var(--surface);
}
.card-link,
.entry-back,
.inline-button {
display: inline-flex;
align-items: center;
border: 1px solid var(--border);
background: transparent;
color: var(--primary);
padding: 2px 9px;
font-size: 12px;
}
.card-link::before,
.entry-back::before,
.inline-button::before {
content: ">\00a0";
color: var(--secondary);
}
.card-link:hover,
.entry-back:hover,
.inline-button:hover {
color: var(--bright);
border-color: var(--secondary);
} }
.card-link-primary, .card-link-primary,
.inline-button-primary { .inline-button-primary {
background: var(--secondary); border-color: var(--border-strong);
color: #fff; color: var(--bright);
border-color: transparent;
} }
.card-link-primary:hover, .card-link-primary:hover,
.inline-button-primary:hover { .inline-button-primary:hover {
color: #fff; background: var(--secondary);
filter: brightness(1.04); color: #0c0d10;
}
.card-link-primary:hover::before,
.inline-button-primary:hover::before {
color: #0c0d10;
} }
.projects-toggle-shell { .projects-toggle-shell {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 20px; padding: 0 16px 16px;
} }
.projects-toggle { .projects-toggle {
font-family: var(--mono);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center;
border-radius: 999px;
border: 1px solid var(--border); border: 1px solid var(--border);
padding: 12px 18px; padding: 4px 14px;
font-weight: 600; font-size: 12px;
background: rgba(255, 255, 255, 0.03); background: var(--surface-alt);
color: var(--primary); color: var(--muted);
cursor: pointer;
transition:
color 0.12s linear,
border-color 0.12s linear;
} }
.projects-toggle:hover { .projects-toggle::before {
border-color: rgba(235, 82, 52, 0.32); content: "+\00a0";
color: var(--secondary); color: var(--secondary);
} }
.projects-toggle[aria-expanded="true"]::before {
content: "-\00a0";
}
.projects-toggle:hover {
color: var(--bright);
border-color: var(--secondary);
}
.updates-grid .content-card { .updates-grid .content-card {
background: linear-gradient( background: var(--surface-alt);
180deg,
rgba(235, 82, 52, 0.08),
rgba(255, 255, 255, 0.02)
);
} }
.site-footer { .site-footer {
margin-bottom: 24px; margin-bottom: 22px;
} }
/* ------------------------------------------------------------ entry page */
.entry-shell { .entry-shell {
padding: 24px 28px 30px; padding: 20px 22px 26px;
} }
.entry-header { .entry-header {
margin-bottom: 24px;
}
.entry-back {
margin-bottom: 20px; margin-bottom: 20px;
} }
.entry-meta { .entry-back {
margin-bottom: 18px; margin-bottom: 18px;
} }
.entry-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 14px;
font-size: 11px;
}
.entry-pill {
display: inline-flex;
align-items: center;
padding: 1px 8px;
border: 1px solid var(--border);
}
.entry-pill-primary {
color: var(--secondary);
border-color: rgba(235, 82, 52, 0.4);
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
}
.entry-pill-secondary {
color: var(--muted);
}
.entry-links { .entry-links {
margin-top: 18px; margin-top: 16px;
} }
.entry-content { .entry-content {
margin-top: 28px; margin-top: 24px;
padding-top: 24px; padding-top: 20px;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
font-size: 0.92rem;
max-width: 88ch;
} }
.entry-content h1, .entry-content h1,
.entry-content h2, .entry-content h2,
.entry-content h3, .entry-content h3,
.entry-content h4 { .entry-content h4 {
margin-top: 28px; margin-top: 26px;
line-height: 1.2; margin-bottom: 8px;
line-height: 1.3;
font-size: 1rem;
}
.entry-content h2::before,
.entry-content h3::before {
content: "## ";
color: var(--secondary);
font-weight: 400;
}
.entry-content h3::before {
content: "### ";
} }
.entry-content p, .entry-content p,
.entry-content li, .entry-content li {
.entry-content blockquote {
color: var(--primary); color: var(--primary);
} }
.entry-content ul, .entry-content ul,
.entry-content ol { .entry-content ol {
padding-left: 22px; padding-left: 20px;
}
.entry-content ul {
list-style: none;
padding-left: 4px;
}
.entry-content ul li {
padding-left: 14px;
text-indent: -14px;
}
.entry-content ul li::before {
content: "-\00a0";
color: var(--secondary);
} }
.entry-content a { .entry-content a {
color: var(--secondary); color: var(--secondary);
border-bottom: 1px solid rgba(235, 82, 52, 0.35);
}
.entry-content a:hover {
border-bottom-color: var(--secondary);
} }
.entry-content blockquote { .entry-content blockquote {
margin: 18px 0; margin: 16px 0;
padding: 12px 16px; padding: 10px 14px;
border-left: 4px solid var(--secondary); border-left: 2px solid var(--secondary);
background: rgba(235, 82, 52, 0.08); background: var(--surface-alt);
border-radius: 0 12px 12px 0; color: var(--muted);
} }
.entry-content code { .entry-content code {
background: rgba(0, 0, 0, 0.24); background: var(--bar);
padding: 2px 6px; border: 1px solid var(--border);
border-radius: 6px; padding: 0 4px;
font-size: 0.92rem; font-size: 0.86rem;
} }
.entry-content pre { .entry-content pre {
padding: 16px; padding: 14px;
overflow-x: auto; overflow-x: auto;
background: rgba(0, 0, 0, 0.24); background: var(--bar);
border-radius: 16px; border: 1px solid var(--border);
border-left: 2px solid var(--secondary);
font-size: 0.84rem;
line-height: 1.5;
} }
.entry-content pre code { .entry-content pre code {
background: transparent; background: transparent;
border: 0;
padding: 0; padding: 0;
} }
.entry-empty { .entry-empty {
text-align: center; text-align: center;
padding: 40px 18px; padding: 36px 18px;
} }
@media only screen and (max-width: 740pt) { @media only screen and (max-width: 860px) {
.projects-hero, .projects-hero,
.entry-shell, .entry-shell {
.content-section { padding: 18px;
padding: 22px; }
.content-grid {
padding: 12px;
gap: 12px;
} }
.section-heading { .section-heading {
flex-direction: column; flex-direction: column;
align-items: start; align-items: start;
gap: 6px;
} }
} }
.section-heading__right {
display: flex;
align-items: center;
gap: 10px;
}
.section-count {
font-size: 11px;
color: var(--muted);
}
.section-count::before {
content: "[ ";
color: var(--border-strong);
}
.section-count::after {
content: " ]";
color: var(--border-strong);
}
+504 -708
View File
File diff suppressed because it is too large. Load diff
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" fill="#0c0d10"/>
<path d="M7 10l6 6-6 6" fill="none" stroke="#eb5234" stroke-width="3" stroke-linecap="square"/>
<rect x="17" y="19" width="9" height="3" fill="#c9cdd6"/>
</svg>

After

Width:  |  Height:  |  Size: 274 B

+39 -12
View File
@@ -134,12 +134,12 @@
const renderLinks = (entry, className) => { const renderLinks = (entry, className) => {
const links = []; const links = [];
const sourceLabel = entry.github && entry.github.includes("git.sowinski.dev") ? "Gitea" : "GitHub"; const sourceLabel = entry.github && entry.github.includes("git.sowinski.dev") ? "gitea" : "github";
links.push(`<a class="${className} ${className}-primary" href="./entry.html?type=${encodeURIComponent(entry.type)}&slug=${encodeURIComponent(entry.slug)}">Read More</a>`); links.push(`<a class="${className} ${className}-primary" href="./entry.html?type=${encodeURIComponent(entry.type)}&slug=${encodeURIComponent(entry.slug)}">details</a>`);
if (entry.demo) { if (entry.demo) {
links.push(`<a class="${className}" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">Live</a>`); links.push(`<a class="${className}" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">live</a>`);
} else if (entry.github) { } else if (entry.github) {
links.push(`<a class="${className}" href="${escapeHtml(entry.github)}" target="_blank" rel="noopener noreferrer">${sourceLabel}</a>`); links.push(`<a class="${className}" href="${escapeHtml(entry.github)}" target="_blank" rel="noopener noreferrer">${sourceLabel}</a>`);
} }
@@ -147,8 +147,21 @@
return `<div class="${className.replace("card-link", "card-links")}">${links.join("")}</div>`; return `<div class="${className.replace("card-link", "card-links")}">${links.join("")}</div>`;
}; };
const statusState = (entry) => {
const status = String(entry.status || "").toLowerCase();
if (status.startsWith("active")) {
return "active";
}
if (entry.type === "updates") {
return "attention";
}
return "idle";
};
const createCard = (entry) => { const createCard = (entry) => {
const formattedDate = formatDate(entry.date);
const featuredClass = entry.featured ? " featured" : ""; const featuredClass = entry.featured ? " featured" : "";
const anchorId = entry.slug ? ` id="${escapeHtml(entry.slug)}"` : ""; const anchorId = entry.slug ? ` id="${escapeHtml(entry.slug)}"` : "";
@@ -156,7 +169,8 @@
<article class="content-card${featuredClass}"${anchorId}> <article class="content-card${featuredClass}"${anchorId}>
<div class="card-meta"> <div class="card-meta">
<span class="card-kicker">${escapeHtml(entry.label || entry.type)}</span> <span class="card-kicker">${escapeHtml(entry.label || entry.type)}</span>
${formattedDate ? `<span class="card-date">${escapeHtml(formattedDate)}</span>` : ""} ${entry.date ? `<span class="card-date">${escapeHtml(entry.date)}</span>` : ""}
${entry.status ? `<span class="card-status" data-state="${statusState(entry)}">${escapeHtml(entry.status.toLowerCase())}</span>` : ""}
</div> </div>
<h3>${escapeHtml(entry.title)}</h3> <h3>${escapeHtml(entry.title)}</h3>
<p class="card-summary">${escapeHtml(entry.summary || "")}</p> <p class="card-summary">${escapeHtml(entry.summary || "")}</p>
@@ -166,6 +180,15 @@
`; `;
}; };
const setSectionCount = (countId, entries) => {
const element = document.getElementById(countId);
if (!element) {
return;
}
element.textContent = `${entries.length} ${entries.length === 1 ? "entry" : "entries"}`;
};
const renderSection = (containerId, entries, emptyMessage) => { const renderSection = (containerId, entries, emptyMessage) => {
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
if (!container) { if (!container) {
@@ -201,7 +224,7 @@
card.classList.toggle("is-hidden", !expanded && index >= defaultVisibleCount); card.classList.toggle("is-hidden", !expanded && index >= defaultVisibleCount);
}); });
toggleButton.textContent = expanded ? "Show Fewer Projects" : "Show More Projects"; toggleButton.textContent = expanded ? "show fewer projects" : "show more projects";
toggleButton.setAttribute("aria-expanded", String(expanded)); toggleButton.setAttribute("aria-expanded", String(expanded));
}; };
@@ -255,8 +278,12 @@
const index = await getIndex(); const index = await getIndex();
const sortedProjects = [...(index.projects || [])].sort(compareByPriorityOrder); const sortedProjects = [...(index.projects || [])].sort(compareByPriorityOrder);
renderSection("projects-grid", sortedProjects, "No projects added yet."); renderSection("projects-grid", sortedProjects, "No projects added yet.");
setSectionCount("projects-count", sortedProjects);
initProjectsToggle(sortedProjects); initProjectsToggle(sortedProjects);
renderSection("updates-grid", [...(index.updates || [])].sort(compareByNewestDate), "No updates added yet.");
const sortedUpdates = [...(index.updates || [])].sort(compareByNewestDate);
renderSection("updates-grid", sortedUpdates, "No updates added yet.");
setSectionCount("updates-count", sortedUpdates);
} catch (error) { } catch (error) {
renderSection("projects-grid", [], error.message); renderSection("projects-grid", [], error.message);
renderSection("updates-grid", [], error.message); renderSection("updates-grid", [], error.message);
@@ -279,7 +306,7 @@
<div class="entry-empty"> <div class="entry-empty">
<p class="eyebrow">Not found</p> <p class="eyebrow">Not found</p>
<h1 class="entry-title">This entry could not be loaded.</h1> <h1 class="entry-title">This entry could not be loaded.</h1>
<a class="entry-back" href="./projects.html">Back to projects</a> <a class="entry-back" href="./projects.html">cd ../projects</a>
</div> </div>
`; `;
return; return;
@@ -305,7 +332,7 @@
shell.innerHTML = ` shell.innerHTML = `
<div class="entry-header"> <div class="entry-header">
<a class="entry-back" href="./projects.html">Back to projects</a> <a class="entry-back" href="./projects.html">cd ../projects</a>
<div class="entry-meta"> <div class="entry-meta">
<span class="entry-pill entry-pill-primary">${escapeHtml(entry.label || entry.type)}</span> <span class="entry-pill entry-pill-primary">${escapeHtml(entry.label || entry.type)}</span>
${entry.date ? `<span class="entry-pill entry-pill-secondary">${escapeHtml(formatDate(entry.date))}</span>` : ""} ${entry.date ? `<span class="entry-pill entry-pill-secondary">${escapeHtml(formatDate(entry.date))}</span>` : ""}
@@ -315,8 +342,8 @@
<p class="entry-summary">${escapeHtml(entry.summary || "")}</p> <p class="entry-summary">${escapeHtml(entry.summary || "")}</p>
${renderTags(entry.tags || [], "entry-tag-list")} ${renderTags(entry.tags || [], "entry-tag-list")}
<div class="entry-links"> <div class="entry-links">
${entry.demo ? `<a class="inline-button inline-button-primary" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">Live</a>` : ""} ${entry.demo ? `<a class="inline-button inline-button-primary" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">live</a>` : ""}
${entry.github ? `<a class="inline-button" href="${escapeHtml(entry.github)}" target="_blank" rel="noopener noreferrer">${entry.github.includes("git.sowinski.dev") ? "Gitea" : "GitHub"}</a>` : ""} ${entry.github ? `<a class="inline-button" href="${escapeHtml(entry.github)}" target="_blank" rel="noopener noreferrer">${entry.github.includes("git.sowinski.dev") ? "gitea" : "github"}</a>` : ""}
</div> </div>
</div> </div>
<article class="entry-content"> <article class="entry-content">
@@ -328,7 +355,7 @@
<div class="entry-empty"> <div class="entry-empty">
<p class="eyebrow">Not found</p> <p class="eyebrow">Not found</p>
<h1 class="entry-title">${escapeHtml(error.message)}</h1> <h1 class="entry-title">${escapeHtml(error.message)}</h1>
<a class="entry-back" href="./projects.html">Back to projects</a> <a class="entry-back" href="./projects.html">cd ../projects</a>
</div> </div>
`; `;
} }
+41 -41
View File
@@ -1,62 +1,62 @@
(function() { (function() {
const heroShell = document.querySelector(".hero-main .hero-shell"); const stream = document.querySelector(".term__stream");
if (!heroShell) { if (!stream) {
return; return;
} }
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const coarsePointer = window.matchMedia("(pointer: coarse)").matches;
if (prefersReducedMotion || coarsePointer) { if (prefersReducedMotion) {
return; return;
} }
let targetX = 0; const CHAR_DELAY = 18;
let targetY = 0; const AFTER_COMMAND_DELAY = 90;
let currentX = 0; const AFTER_OUTPUT_DELAY = 130;
let currentY = 0;
const clamp = (value, min, max) => Math.min(Math.max(value, min), max); // Build the playback script before hiding anything, so a thrown error here
// leaves the page in its fully rendered state.
const steps = Array.from(stream.children).map((element) => ({
element,
isCommand: element.classList.contains("cmd"),
text: element.textContent
}));
const animate = () => { if (!steps.some((step) => step.isCommand)) {
currentX += (targetX - currentX) * 0.065; return;
currentY += (targetY - currentY) * 0.065; }
const bgX = 50 + currentX * 6.8; steps.forEach((step) => {
const bgY = 18 + currentY * 5.6; if (step.isCommand) {
const bgXAlt = 50 - currentX * 5.2; step.element.textContent = "";
const bgYAlt = 30 - currentY * 4.6; return;
const glowRotate = currentX * 16 + currentY * 10; }
const glowShiftX = currentX * 54;
const glowShiftY = currentY * 42;
document.body.style.setProperty("--hero-glow-x", `${bgX}%`); step.element.classList.add("is-typing-hidden");
document.body.style.setProperty("--hero-glow-y", `${bgY}%`); });
document.body.style.setProperty("--hero-glow-alt-x", `${bgXAlt}%`);
document.body.style.setProperty("--hero-glow-alt-y", `${bgYAlt}%`);
document.body.style.setProperty("--hero-glow-rotate", `${glowRotate}deg`);
document.body.style.setProperty("--hero-glow-shift-x", `${glowShiftX}px`);
document.body.style.setProperty("--hero-glow-shift-y", `${glowShiftY}px`);
heroShell.style.setProperty("--hero-shell-spot-x", `${50 + currentX * 14}%`); const wait = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
heroShell.style.setProperty("--hero-shell-spot-y", `${28 + currentY * 10}%`);
requestAnimationFrame(animate); const typeCommand = async (element, text) => {
for (let index = 0; index < text.length; index += 1) {
element.textContent = text.slice(0, index + 1);
await wait(CHAR_DELAY);
}
}; };
window.addEventListener("mousemove", (event) => { const play = async () => {
const x = clamp((event.clientX / window.innerWidth - 0.5) * 2, -1, 1); for (const step of steps) {
const y = clamp((event.clientY / window.innerHeight - 0.5) * 2, -1, 1); if (step.isCommand) {
await typeCommand(step.element, step.text);
await wait(AFTER_COMMAND_DELAY);
continue;
}
targetX = x; step.element.classList.remove("is-typing-hidden");
targetY = y; await wait(AFTER_OUTPUT_DELAY);
}); }
};
window.addEventListener("mouseleave", () => { play();
targetX = 0;
targetY = 0;
});
requestAnimationFrame(animate);
})(); })();