diff --git a/index.html b/index.html index e5a22e3..8b21e00 100644 --- a/index.html +++ b/index.html @@ -4,746 +4,42 @@ adni onoh — ~/portfolio + - + + +
-
adni@portfolio: ~/
-
-
+
- open to new-grad SWE roles · open to learn + open to new-grad SWE roles starting fall 2026 · - buffalo, ny · ET + + buffalo, ny · + + + · + —:—:— + ET + + · graduating spring 2026
@@ -768,10 +72,10 @@

adni onoh.

- software engineer working across distributed systems and full-stack web. recently: built a Raft consensus cluster in Go, a Kademlia-based DHT, and shipped production features at a healthtech startup. + Distributed systems engineer and full-stack web developer. Currently interning at a healthtech startup writing software that's helping people track and care for their mental health, building a personal project that combines my web and distributed systems experience (see Distributed Microservices Demo Suite ↓), and interview prepping.

-
+
cat about.md # readme · markdown
-

// the short version

-

I'm graduating from the University at Buffalo in Spring 2026 with a B.A. in Computer Science and a concentration in Communication.

-

My coursework and side work split between two things that pull on each other: distributed systems & backend (where I've built a Raft consensus cluster, a Kademlia DHT, and a concurrent memory allocator) and full-stack web development (where I currently ship production features at Nara Therapy).

-

The communication concentration shows up in how I work — writing user stories, reviewing teammates' code, and explaining trade-offs to non-engineers. It's been more useful than I expected.

-

// status: open to learn, open to roles starting summer 2026

+

// in a nutshell

+

Open to Learn

+

For the last four years I've focused on systems engineering (more recently distributed systems in particular) and web development, alongside a concentration in communication.

+

In this time, I've gotten much better understanding ideas/tasks from a software perspective enough to implement them, without losing my ability to communicate effectively with non-technical people on the same topics. I like to think of it as being bilingual! (most likely cope because I have been unsuccessful at learning any languages other than English)

+

// status: open to new-grad SWE roles starting fall 2026

@@ -809,7 +113,7 @@

-
+
ls -la work/ # current role
@@ -818,103 +122,93 @@

Software Engineer @ Nara Therapy
healthtech · agile · full-stack
- Building production features on a healthcare web app inside an agile team. Recent work: -
    -
  • Developed a responsive feedback modal that captures user insights while keeping interface consistency across mobile and desktop.
  • -
  • Added dashboard features — organization contact dates, organization deletion, and rating-based filters — following Figma designs and QA criteria so providers can spot low-rated sessions efficiently.
  • -
+

Working at a fast-paced startup with an agile team building features to equip people with continuous mental health tools that help before, during, and after a session.

+

(Nara Therapy - Production Features in the projects section below ↓)

-
+
grep -r skills/ # grouped by frequency of use

// daily use

    -
  • Go
  • JavaScript
  • Python
  • -
  • HTML/CSS
  • -
  • Node.js
  • -
  • Git
  • +
  • Go
  • +
  • C
  • +
  • HTML & CSS
  • +
  • Node.js & Express & EJS
  • +
  • Git/GitHub
  • +
  • Figma
  • +

// comfortable with

  • TypeScript
  • -
  • C
  • React
  • -
  • Express
  • -
  • Flask
  • -
  • PostgreSQL
  • MongoDB
  • MySQL
  • -
  • Java
  • -
  • Figma
  • +
  • PostgreSQL
  • +
  • jQuery

// familiar with

    +
  • Flask
  • OCaml
  • -
  • Scala
  • PHP
  • -
  • EJS
  • +
  • Scala
  • +
  • Java
  • R
  • -
  • jQuery
  • -
  • Protobuf
  • -
  • RPC

// practices

  • Scrum/Agile
  • -
  • User stories
  • Shell scripting
  • Debugging
  • Code review
  • -
  • Jira
  • -
  • Trello
-
-
./animate.sh # css demo · not a self-assessment
+
./animate.sh # css demo · not a self-assessment
// note: - the bars below are a CSS animation demo — a chance to play with transition, transform, and @keyframes. they show how much I've used each language while building this site, not any claim of skill level. for an honest proficiency picture, see the tags above. + the bars below are a simple CSS animation demo, a chance to play with transition, transform, and @keyframes. not a claim of skill level. for an honest proficiency picture, see the tags above.
-
CSS90%
-
+
CSS92%
+
-
HTML85%
-
+
JavaScript68%
+
-
JavaScript75%
-
+
HTML81%
+
-
Go70%
-
+
Go40%
+
-
Python60%
-
+
Python55%
+
-
C45%
-
+
C82%
+
-
-
tree projects/ # 11 entries · filter by category
- -
- filter: - - - - - +
+
tree projects/ # 13 entries · filter by category
+ +
+ + + + + + +
-

// note: OOP principles and Git/GitHub version control applied across all projects.

+

// note: OOP principles and Git/GitHub version control applied across all projects. ● current marks projects I'm actively building right now.

- + + + + + + +
+
+
+ Nara Therapy — Production Features + web + current +
+
TypeScript · React · Figma
+
+
+ Nara's pitch is continuous mental health care. I haven't worked much on the member-facing side of the product yet, but I've contributed across the parts that make it run: the tools providers use to deliver care, the dashboards org admins use to manage it, and the systems the Nara team uses to keep everything moving. + +
+
For providers
+

+ I added a way to filter AI-chat sessions by client rating so low-rated interactions are noticed immediately, which lets a provider catch a struggling client before the next session. I also flagged high-risk journal entries alongside other risk indicators, displayed client feedback on the provider detail page, and helped the AI chatbot reference user journals, mood check-ins, and gratitude logs to tailor conversations to the user. +

+
+ +
+
For org admins
+

+ Plan selection and a default session count on org creation let the dashboard handle both billing models (bulk and pay-per-invite) without manual input. A required organization-type field captures what kind of org is being onboarded, and minimum seats with an adjustable annual fee set baseline billing automatically. I also gated dashboard metrics behind a user threshold so admins don't see misleading numbers before there's real data, and built an in-context feedback popup that centralizes admin feedback into the main table. +

+
+ +
+
For the Nara team
+

+ Contact-date tracking shows when an org first reached out, helping the team prioritize warm leads. Deletion controls let admins clean up duplicate or accidental org entries instead of just rejecting them. And a user-type column on the feedback table tags every submission as coming from a member, org admin, or provider, so feedback can actually be routed and patterned instead of treated as one bucket. +

+
+ +

+ // process: all work follows Figma designs and team QA criteria. +

+
+
+ +
+
+
+ UB CSE Scrum Board + web + current +
+
Bug fixes · feature work · internal tooling
+
+
+ Contributing to the University at Buffalo CSE department's in-house scrum board. Fixing live bugs and adding new features for an existing user base, which means reading other people's code & documentation first, working within the existing architecture, and shipping changes carefully. +

// private repo

+
+
+ + +
Consensus Server Cluster systems
Go · Raft · RPC
- Built a Raft consensus cluster in Go with full leader election, log replication, and crash-recovery — tolerant of both clean restarts and partial state loss. + Built a Raft-inspired consensus cluster in Go with full leader election, log replication, and crash-recovery. Tolerant of both clean restarts and partial state loss.
  • Implemented leader election with election timeouts and term-based voting
  • Log replication with consistency checks across follower nodes
  • Recovery logic that handles partial state loss on restart
+

// private repo

@@ -967,21 +376,23 @@

// practices

  • Implemented RPC-based node communication for iterative lookups
  • Content-addressing and fault-tolerant data storage across the network
  • +

    // private repo

    -
    Message Service & Failure Detector systems
    +
    Message Service & Failure Detector systems
    Go · TCP sockets · Protobuf · channels
    - Implemented a multi-process message service, then used that service to build a heartbeat-based failure detector on top of it. + Implemented a multi-process message service, then used that service to build a heartbeat-based failure detector.
    • TCP sockets for cross-network process communication
    • Go channels for in-process communication and coordination
    • Protobuf protocol buffers for data serialization between processes
    +

    // private repo

    @@ -991,43 +402,31 @@

    // practices

    C · mutexes · concurrency
    - Developed a dynamic, concurrent memory allocator using mutexes and locks to manage allocation, deallocation, and reallocation safely under contention. + Developed a dynamic, concurrent memory allocator using mutexes and locks to manage allocation, deallocation, and reallocation.
      -
    • Thread-safe alloc / free / realloc via mutex-guarded data structures
    • +
    • Thread-safe allocation, freeing, and reallocation under contention
    • Optimized for low fragmentation and reuse of freed blocks
    +

    // private repo

    - -
    -
    -
    Nara Therapy — Production Features web
    -
    JavaScript · React · Figma · QA
    -
    -
    - Current production-side work as a Software Engineer at a healthtech startup. -
      -
    • Responsive feedback modal that maintains consistency across mobile and desktop
    • -
    • Dashboard features: organization contact dates, deletion controls, rating-based filters
    • -
    • All work follows Figma designs and team QA criteria
    • -
    -
    -
    +
    -
    ChatAppX — Real-time Chat & Video web
    +
    ChatAppX — Real-time Chat & Video web
    Python · Node · Express · PostgreSQL · MongoDB · WebSockets
    A real-time web app for text and video chat, with security as a first-class concern.
      -
    • HTML injection prevention, salted password hashing, XSRF & auth tokens
    • -
    • Direct peer-to-peer connections via WebSockets — video chat keeps working through server crashes
    • +
    • HTML injection prevention, salted password hashing, XSRF & auth tokens
    • +
    • Direct peer-to-peer connections via WebSockets so video chat keeps working through server crashes
    • Secure file uploads with content sanitization and validation
    • Comprehensive form validation and error handling
    +

    // private repo

    @@ -1037,24 +436,27 @@

    // practices

    Node · MongoDB · JSX · Figma
    - Cross-sprint contributions on a real product. Worked the full feature loop — user stories, Figma wireframes, acceptance tests, then implementation. + For the Experiential Learning & Research class at UB. Worked on code documentation, user stories & acceptance tests, Figma wireframes mostly and a few implementations.
      -
    • Designed key features: multi-page resume view, automatic resume display, filters, ad slots, AI interview interface
    • -
    • Built real-time multi-resume upload
    • -
    • Integrated a feedback system with automated email routing
    • +
    • Designed key features: multiple resume uploads with automatic resume display, multi-page resume view, resume filters, ad slots, AI interview interface
    • +
    • Implemented real-time multiple resume uploads and integrated a feedback system with automated email routing
    +

    // private repo

    - + +
    Simon's Game fun
    HTML · CSS · JavaScript
    - The classic memory game — repeat the sequence of lights and sounds. Built to practice event-driven JS and DOM manipulation. - ▶ play game + Repeat the sequence of lights and sounds. Built to practice event-driven JS and DOM manipulation. Try beating my personal record of 21 rounds. +
    @@ -1064,8 +466,10 @@

    // practices

    HTML · CSS · JavaScript
    - Two-player dice roll — the higher number wins. A small exercise in randomness, image swapping, and clean conditional UI. - ▶ play game + Two-player dice roll — the higher number wins. You and your buddy choose your player, and whoever wins pays for lunch or something. +
    @@ -1075,26 +479,68 @@

    // practices

    HTML · CSS · JavaScript
    - An interactive drum kit — make beats with your keyboard or by clicking the pads. Covers keyboard events, audio playback, and animated button feedback. - ▶ try it + Make beats with your keyboard or by clicking the pads. Covers keyboard events, audio playback, and animated button feedback. +
    - + +
    This Interactive Resume tools
    HTML · CSS · JavaScript
    - The site you're reading. Hand-built layout with custom animations, filter system, and an image lightbox — no frameworks, no templates. + The site you're reading, built with just HTML/CSS/JS. I hope you enjoy using it! Try the command input at the top: tree projects works. +
    -
    +
    +
    cat process.md # how this site was built
    +

    + There are many things to consider when creating anything. Below are the practices that shaped the building of this website +

    + +
    +
    +

    + // process + Somewhat Agile +

    +

    I did my best to put the skills I learned to practice without being too stiff considering that this is a solo project

    +
      +
    • Iterative — I shipped features in small, reviewable slices (content fixes → layout → projects → polish). No one giant rewrite.
    • +
    • Feedback loops — most features went through a "build, review, refine" pass before being kept. I was able to get feedback from some faculty and friends along the way (and some usability testing).
    • +
    • Coursework — used agile/scrum practice from UB's Software Engineering Concepts course
    • +
    +
    + +
    +

    + // a11y + Making Accessibility a habit +

    +

    Informed by UB's Applied Human-Computer Interaction and Interface Design course, I picked a few accessibility metrics and made the website comply with them.

    +
      +
    • Keyboard navigable — try Tab through the page; every interactive element is reachable and has a visible focus ring. / jumps straight to the command input.
    • +
    • Semantic HTML — proper landmarks (<nav>, <main>, <section>, <footer>), heading hierarchy, ARIA labels on icon-only controls.
    • +
    • Reduced motion — if your OS has "reduce motion" enabled, the section fade-ins, status pulses, and shimmer effects shorten or disable automatically.
    • +
    • Contrast & legibility — both themes meet WCAG AA contrast for body text. Skip link at the top of the DOM for screen-reader users.
    • +
    +
    +
    +
    + +
    cat certifications.txt # verified credentials
    -
    +
    echo "let's build something"
    -
    © 2026 adni onoh · built from scratch with html/css/js
    -
    last commit: today
    +
    © 2026 adni onoh · buffalo, ny · mostly self-written html / css / js
    +
    session uptime: 0:00
    - + - + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..681e248 --- /dev/null +++ b/index.js @@ -0,0 +1,325 @@ +/* ============================================================ + Adni Onoh — Interactive Resume · index.js + ------------------------------------------------------------ + Sections: + 1. Skill bar animation (click to fill / reset) + 2. Theme toggle (dark ↔ light, persisted) + 3. Project category filter + 4. Live clock (Buffalo ET) + 5. Command palette (top-bar input) + 6. Footer uptime ticker + ============================================================ */ + +(function () { + 'use strict'; + + /* ============================================================ + 1. Skill bar animation + ============================================================ */ + const animateBtn = document.getElementById('animateBtn'); + const bars = document.querySelectorAll('.bar-fill'); + + if (animateBtn && bars.length) { + let isFilled = false; + let busy = false; + + const fillBars = () => { + bars.forEach((bar, i) => { + const w = bar.dataset.width; + setTimeout(() => { + bar.style.width = w + '%'; + bar.classList.add('animated'); + }, i * 120); + }); + }; + + const resetBars = () => { + bars.forEach(bar => { + bar.style.width = '0%'; + bar.classList.remove('animated'); + }); + }; + + const setButtonState = (filled) => { + if (filled) { + animateBtn.classList.add('filled'); + animateBtn.querySelector('.btn-prompt').textContent = '↺'; + animateBtn.querySelector('.btn-label').textContent = 'reset'; + } else { + animateBtn.classList.remove('filled'); + animateBtn.querySelector('.btn-prompt').textContent = '$'; + animateBtn.querySelector('.btn-label').textContent = 'run animation'; + } + }; + + animateBtn.addEventListener('click', () => { + if (busy) return; + busy = true; + if (!isFilled) { + fillBars(); + isFilled = true; + setButtonState(true); + } else { + resetBars(); + isFilled = false; + setButtonState(false); + } + setTimeout(() => { busy = false; }, bars.length * 120 + 200); + }); + } + + /* ============================================================ + 2. Theme toggle + ============================================================ */ + const themeToggle = document.getElementById('themeToggle'); + const root = document.documentElement; + + if (themeToggle) { + const themeLabel = themeToggle.querySelector('.theme-label'); + const themeIcon = themeToggle.querySelector('.theme-icon'); + + let savedTheme = 'dark'; + try { savedTheme = localStorage.getItem('theme') || 'dark'; } + catch (e) { /* storage blocked, default applies */ } + + const applyTheme = (theme) => { + if (theme === 'light') { + root.setAttribute('data-theme', 'light'); + themeLabel.textContent = 'dark'; + themeIcon.textContent = '◑'; + } else { + root.removeAttribute('data-theme'); + themeLabel.textContent = 'light'; + themeIcon.textContent = '◐'; + } + }; + + applyTheme(savedTheme); + + themeToggle.addEventListener('click', () => { + const current = root.getAttribute('data-theme') === 'light' ? 'light' : 'dark'; + const next = current === 'light' ? 'dark' : 'light'; + applyTheme(next); + try { localStorage.setItem('theme', next); } catch (e) { /* ignore */ } + }); + } + + /* ============================================================ + 3. Project category filter + ============================================================ */ + const chips = document.querySelectorAll('.filter-chip'); + const projects = document.querySelectorAll('.project[data-category]'); + + chips.forEach(chip => { + chip.addEventListener('click', () => { + const filter = chip.dataset.filter; + + chips.forEach(c => { + c.classList.remove('active'); + c.setAttribute('aria-pressed', 'false'); + }); + chip.classList.add('active'); + chip.setAttribute('aria-pressed', 'true'); + + projects.forEach(p => { + let match; + if (filter === 'all') { + match = true; + } else if (filter === 'current') { + // "current" cross-cuts categories — match any project with a current-tag + match = p.querySelector('.current-tag') !== null; + } else { + match = p.dataset.category === filter; + } + p.classList.toggle('hidden', !match); + }); + }); + }); + + // Initialize aria-pressed state on the active chip + document.querySelector('.filter-chip.active')?.setAttribute('aria-pressed', 'true'); + + /* ============================================================ + 4. Live clock — Buffalo ET, updates every second + ============================================================ */ + const clockTime = document.getElementById('clockTime'); + const clockDate = document.getElementById('clockDate'); + + if (clockTime && clockDate) { + const tzOptions = { timeZone: 'America/New_York' }; + const timeFmt = new Intl.DateTimeFormat('en-US', { + ...tzOptions, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); + const dateFmt = new Intl.DateTimeFormat('en-US', { + ...tzOptions, + weekday: 'short', + month: 'short', + day: 'numeric', + }); + + const tick = () => { + const now = new Date(); + clockTime.textContent = timeFmt.format(now); + clockDate.textContent = dateFmt.format(now).toLowerCase(); + }; + tick(); + setInterval(tick, 1000); + } + + /* ============================================================ + 5. Command palette — type in the top bar to navigate + ------------------------------------------------------------ + Accepted commands map to sections. Plain section names work + too (e.g. "projects" === "tree projects"). Special: + help — show available commands inline + clear — empty the input + ============================================================ */ + const cmdInput = document.getElementById('commandInput'); + + if (cmdInput) { + // Map: command (lowercase) → target section id + const commandMap = { + // canonical commands (mirror the section headers) + 'cat about.md': 'about', + 'cat about': 'about', + 'ls work': 'work', + 'ls -la work': 'work', + 'ls work/': 'work', + 'grep skills': 'skills', + 'grep -r skills': 'skills', + 'grep -r skills/': 'skills', + './animate.sh': 'skills', + 'animate.sh': 'skills', + 'tree projects': 'projects', + 'tree projects/': 'projects', + 'cat process.md': 'process', + 'cat process': 'process', + 'cat certifications.txt': 'certs', + 'cat certifications': 'certs', + 'echo contact': 'contact', + // short aliases — just type the section name + 'about': 'about', + 'work': 'work', + 'skills': 'skills', + 'projects': 'projects', + 'process': 'process', + 'certs': 'certs', + 'certifications': 'certs', + 'contact': 'contact', + }; + + const helpText = 'try: projects · about · work · skills · process · certs · contact · help'; + + const flash = (cls, ms = 600) => { + cmdInput.classList.add(cls); + setTimeout(() => cmdInput.classList.remove(cls), ms); + }; + + const runCommand = (raw) => { + // Normalize: lowercase, collapse runs of whitespace, then close the gap + // between './' or '/' and any following text so './ animate.sh' → './animate.sh'. + const cmd = raw + .trim() + .toLowerCase() + .replace(/\s+/g, ' ') + .replace(/(\.?\/)\s+/g, '$1'); + if (!cmd) return; + + if (cmd === 'help' || cmd === '?' || cmd === 'ls' || cmd === 'ls -la') { + cmdInput.value = ''; + cmdInput.placeholder = helpText; + flash('success'); + // Restore the regular placeholder after a few seconds + setTimeout(() => { cmdInput.placeholder = defaultPlaceholder; }, 4500); + return; + } + + if (cmd === 'clear' || cmd === 'cls') { + cmdInput.value = ''; + flash('success'); + return; + } + + if (cmd === 'whoami') { + cmdInput.value = ''; + cmdInput.placeholder = 'adni — see About section ↓'; + flash('success'); + setTimeout(() => { cmdInput.placeholder = defaultPlaceholder; }, 3500); + document.getElementById('about')?.scrollIntoView({ behavior: 'smooth' }); + return; + } + + const target = commandMap[cmd]; + if (target) { + const el = document.getElementById(target); + if (el) { + flash('success'); + el.scrollIntoView({ behavior: 'smooth' }); + // Clear input after a beat so the success flash is visible + setTimeout(() => { cmdInput.value = ''; }, 700); + } + } else { + flash('error', 700); + cmdInput.placeholder = `command not found: ${raw.trim().slice(0, 30)} — try "help"`; + setTimeout(() => { cmdInput.placeholder = defaultPlaceholder; }, 3000); + } + }; + + // Default placeholder rotates between a few hints + const placeholders = [ + 'type "tree projects" to see what I\'ve built ↓', + 'type a command (try "help" or "projects")', + 'type "projects"; recruiter shortcut', + ]; + let defaultPlaceholder = placeholders[0]; + + // Pick a placeholder per page load, weighted toward the projects nudge + const pickPlaceholder = () => { + const idx = Math.floor(Math.random() * placeholders.length); + defaultPlaceholder = placeholders[idx]; + cmdInput.placeholder = defaultPlaceholder; + }; + pickPlaceholder(); + + cmdInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + runCommand(cmdInput.value); + } else if (e.key === 'Escape') { + cmdInput.value = ''; + cmdInput.blur(); + } + }); + + // Keyboard shortcut: "/" focuses the input from anywhere on the page + document.addEventListener('keydown', (e) => { + if (e.key === '/' && document.activeElement !== cmdInput + && !/^(input|textarea|select)$/i.test(document.activeElement?.tagName || '')) { + e.preventDefault(); + cmdInput.focus(); + } + }); + } + + /* ============================================================ + 6. Footer "uptime" — a small honest detail, no fake commits + ============================================================ */ + const uptimeEl = document.getElementById('footerUptime'); + if (uptimeEl) { + // Calculate how long ago an arbitrary "session start" was — + // resets per page load, so it really is the visitor's session. + const start = Date.now(); + const fmt = () => { + const s = Math.floor((Date.now() - start) / 1000); + const m = Math.floor(s / 60); + const ss = String(s % 60).padStart(2, '0'); + uptimeEl.textContent = `${m}:${ss}`; + }; + fmt(); + setInterval(fmt, 1000); + } +})(); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..3b3ebcf --- /dev/null +++ b/style.css @@ -0,0 +1,1219 @@ +/* ============================================================ + Adni Onoh — Interactive Resume + Theme: Terminal · Green primary · Amber secondary + ============================================================ */ + +:root { + --bg: #0d0d0c; + --bg-elev: #161614; + --bg-soft: #1c1c1a; + --fg: #e8e6df; + --fg-dim: #9a958a; + --fg-faint: #727064; /* darkened from #56524a for AA contrast on UI labels */ + --accent: #7ec699; /* sage green — primary */ + --accent-dim: #5fa97a; + --magenta: #d96f9e; /* tools */ + --cyan: #7fb3c4; /* web */ + --violet: #b794d9; /* fun & demos */ + --secondary: #ffb454; /* warm amber — current status badge, play buttons */ + --red: #e06c75; + --border: rgba(126, 198, 153, 0.15); + --grid: rgba(255, 255, 255, 0.025); +} + +/* Light mode — HackerRank-inspired editor palette + All foreground colors meet WCAG AA (4.5:1 for text, 3:1 for UI). */ +[data-theme="light"] { + --bg: #f8f9fa; + --bg-elev: #ffffff; + --bg-soft: #eef0f3; + --fg: #1a2734; + --fg-dim: #5b6573; + --fg-faint: #737b87; /* darkened from #8a93a0 for AA on white surfaces */ + --accent: #247a44; /* darkened from #2c8d4f to pass AA (4.5+:1 vs white) */ + --accent-dim: #1f6a3a; + --magenta: #b13f73; + --cyan: #2d7a8e; + --violet: #7c4dac; + --secondary: #9e5d10; /* darkened from #c97d2a to pass AA (4.95:1 vs white) */ + --red: #c44a4a; + --border: rgba(36, 122, 68, 0.18); + --grid: rgba(0, 0, 0, 0.025); +} + +/* Smooth theme transitions */ +body, .window-chrome, .skill-block, .project, .entry, .stat, +.contact-card, .cert-card, .demo-note, .bar-track, .bar-fill, +.filter-chip, .animate-btn, .cat-tag, .play-link, .repo-link, +.hero-links a, .command-input, .process-col { + transition: background-color 0.25s ease, color 0.25s ease, + border-color 0.25s ease, box-shadow 0.25s ease; +} + +/* ============================================================ + Accessibility + ============================================================ */ + +.skip-link { + position: absolute; + top: -100px; + left: 12px; + z-index: 200; + padding: 10px 16px; + background: var(--accent); + color: var(--bg); + text-decoration: none; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.05em; + border-radius: 4px; + transition: top 0.18s ease; +} +.skip-link:focus { + top: 12px; + outline: 2px solid var(--secondary); + outline-offset: 2px; +} + +a:focus-visible, +button:focus-visible, +input:focus-visible, +.contact-card:focus-visible, +.cert-card:focus-visible, +.skill-list li:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 2px; +} +.command-input:focus-visible { + outline: none; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } + .section { opacity: 1; transform: none; } + .status::before, .current-tag::before { animation: none; } + .theme-toggle:hover .theme-icon { transform: none; } + .bar-fill { transition: width 0.001ms; } + .bar-fill::after { display: none; } +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +html { scroll-behavior: smooth; } + +body { + background: var(--bg); + background-image: + linear-gradient(var(--grid) 1px, transparent 1px), + linear-gradient(90deg, var(--grid) 1px, transparent 1px); + background-size: 32px 32px; + color: var(--fg); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 14px; + line-height: 1.65; + min-height: 100vh; + letter-spacing: 0.01em; + overflow-x: hidden; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background: radial-gradient(ellipse at center, transparent 0%, transparent 60%, rgba(0,0,0,0.5) 100%); + pointer-events: none; + z-index: 100; + transition: background 0.25s ease; +} +[data-theme="light"] body::before { + background: radial-gradient(ellipse at center, transparent 0%, transparent 70%, rgba(0,0,0,0.04) 100%); +} + +/* ============================================================ + Window chrome (top bar) + ============================================================ */ +.window-chrome { + position: sticky; + top: 0; + z-index: 50; + background: var(--bg-elev); + border-bottom: 1px solid var(--border); + padding: 12px 20px; + display: flex; + align-items: center; + gap: 16px; + backdrop-filter: blur(8px); +} + +.dots { display: flex; gap: 7px; flex-shrink: 0; } +.dot { width: 11px; height: 11px; border-radius: 50%; } +.dot-r { background: #ff5f56; } +.dot-y { background: #ffbd2e; } +.dot-g { background: #27c93f; } + +.window-title { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 12px; + color: var(--fg-dim); +} +.window-title .prefix { color: var(--fg-dim); } +.window-title .prefix .path { color: var(--fg); } +.window-title .prompt-glyph { color: var(--magenta); margin-right: 2px; } + +.command-input-wrap { + position: relative; + display: inline-flex; + align-items: center; + gap: 4px; + flex: 1 1 auto; + min-width: 0; + max-width: 480px; +} +.command-input { + flex: 1; + background: transparent; + border: none; + outline: none; + color: var(--fg); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 12px; + letter-spacing: 0.01em; + padding: 4px 0; + width: 100%; + min-width: 0; + caret-color: var(--accent); +} +.command-input::placeholder { + color: var(--fg-faint); + font-style: italic; + opacity: 0.85; +} +.command-input.error { + color: var(--red); + animation: shake 0.35s; +} +.command-input.success { + color: var(--accent); +} +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-3px); } + 75% { transform: translateX(3px); } +} + +.window-tabs { + display: flex; + gap: 20px; + font-size: 12px; + flex-shrink: 0; +} +.window-tabs a { + color: var(--fg-dim); + text-decoration: none; + transition: color 0.2s; +} +.window-tabs a:hover { color: var(--accent); } + +.theme-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: 8px; + padding: 5px 10px; + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + color: var(--fg-dim); + background: transparent; + border: 1px solid var(--border); + cursor: pointer; + letter-spacing: 0.05em; + text-transform: lowercase; + transition: all 0.2s; + flex-shrink: 0; +} +.theme-toggle:hover { + color: var(--accent); + border-color: var(--accent); + background: var(--bg-soft); +} +.theme-toggle .theme-icon { + font-size: 13px; + display: inline-block; + transition: transform 0.4s ease; +} +.theme-toggle:hover .theme-icon { transform: rotate(180deg); } + +/* ============================================================ + Main container + ============================================================ */ +.shell { + max-width: 980px; + margin: 0 auto; + padding: 48px 32px 96px; +} + +.prompt { + color: var(--accent); + font-weight: 500; + user-select: none; +} +.prompt::before { content: '$ '; color: var(--magenta); } + +.section { + margin: 64px 0; + opacity: 0; + transform: translateY(8px); + animation: rise 0.7s ease forwards; +} +.section:nth-child(1) { animation-delay: 0.05s; } +.section:nth-child(2) { animation-delay: 0.15s; } +.section:nth-child(3) { animation-delay: 0.25s; } +.section:nth-child(4) { animation-delay: 0.35s; } +.section:nth-child(5) { animation-delay: 0.45s; } + +@keyframes rise { + to { opacity: 1; transform: translateY(0); } +} + +.cmd { + font-size: 13px; + color: var(--fg-dim); + margin-bottom: 20px; + display: flex; + align-items: baseline; + gap: 8px; +} +.cmd .out { color: var(--fg-faint); margin-left: 12px; font-size: 11px; } +.cmd-demo { margin-top: 64px; } + +/* ============================================================ + Hero + ============================================================ */ +.hero { + margin-top: 24px; + padding: 40px 0 32px; + border-bottom: 1px dashed var(--border); +} + +.hero-meta { + display: flex; + gap: 14px; + align-items: center; + font-size: 11px; + color: var(--fg-faint); + margin-bottom: 24px; + flex-wrap: wrap; +} +.status { + display: inline-flex; + align-items: center; + gap: 6px; +} +.status::before { + content: ''; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--secondary); + box-shadow: 0 0 8px var(--secondary); + animation: pulse 2s ease infinite; +} +@keyframes pulse { 50% { opacity: 0.4; } } + +.location-clock { + display: inline-flex; + align-items: baseline; + gap: 6px; + flex-wrap: wrap; +} +.clock { + color: var(--accent); + display: inline-flex; + align-items: baseline; + gap: 6px; + font-family: 'JetBrains Mono', monospace; +} +.clock .clock-sep { color: var(--fg-faint); } + +.hero h1 { + font-family: 'JetBrains Mono', monospace; + font-size: clamp(2.4rem, 6vw, 4rem); + font-weight: 700; + line-height: 1.05; + letter-spacing: -0.03em; + margin-bottom: 16px; +} +.hero h1 .accent { color: var(--accent); } +.hero h1 .dim { color: var(--fg-faint); font-weight: 300; } + +.hero-tagline { + color: var(--fg-dim); + font-size: 15px; + max-width: 580px; + margin-bottom: 28px; + font-family: 'IBM Plex Mono', monospace; +} +.hero-tagline em { color: var(--accent); font-style: normal; } +.tagline-link { + color: var(--secondary); + text-decoration: underline; + text-decoration-color: rgba(255, 180, 84, 0.4); + text-underline-offset: 3px; + transition: text-decoration-color 0.2s, color 0.2s; +} +.tagline-link:hover { + color: var(--secondary); + text-decoration-color: var(--secondary); +} + +.hero-links { + display: flex; + gap: 8px; + flex-wrap: wrap; + font-size: 12px; +} +.hero-links a { + color: var(--fg); + text-decoration: none; + padding: 8px 14px; + border: 1px solid var(--border); + background: var(--bg-elev); + transition: all 0.2s; +} +.hero-links a:hover { + border-color: var(--accent); + color: var(--accent); + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 var(--accent-dim); +} +.hero-links a.primary { + background: var(--accent); + color: var(--bg); + border-color: var(--accent); +} +.hero-links a.primary:hover { + background: var(--bg); + color: var(--accent); +} + +/* ============================================================ + About + ============================================================ */ +.about-block { + padding-left: 24px; + border-left: 2px solid var(--accent-dim); + font-family: 'IBM Plex Mono', monospace; + color: var(--fg); + max-width: 700px; +} +.about-block p { margin-bottom: 12px; } +.about-block .comment { color: var(--fg-faint); font-style: italic; font-size: 12px; } +.about-block .inline-accent { color: var(--accent); } +.inline-accent { color: var(--accent); } +.inline-current { color: var(--secondary); font-weight: 500; } + +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1px; + background: var(--border); + margin-top: 32px; + border: 1px solid var(--border); +} +.stat { + background: var(--bg-elev); + padding: 18px 20px; +} +.stat-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--fg-faint); + margin-bottom: 6px; +} +.stat-value { color: var(--accent); font-size: 15px; font-weight: 500; } + +/* ============================================================ + Experience entries + ============================================================ */ +.entry { + padding: 24px 0; + border-bottom: 1px dashed var(--border); + display: grid; + grid-template-columns: 140px 1fr; + gap: 24px; +} +.entry:last-child { border-bottom: none; } +.entry-meta { + color: var(--fg-faint); + font-size: 11px; + letter-spacing: 0.05em; +} +.entry-title { + font-size: 16px; + color: var(--fg); + margin-bottom: 4px; + font-weight: 500; +} +.entry-title .at { color: var(--accent); } +.entry-role { color: var(--fg-dim); font-size: 13px; margin-bottom: 10px; } +.entry-desc { color: var(--fg-dim); font-size: 13px; max-width: 560px; } +.entry-desc ul { margin-top: 8px; list-style: none; padding-left: 4px; } +.entry-desc li { + padding-left: 16px; + position: relative; + margin-bottom: 4px; +} +.entry-desc li::before { + content: '→'; + position: absolute; + left: 0; + color: var(--accent); +} + +/* ============================================================ + Skills + ============================================================ */ +.skill-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 24px; + margin-top: 12px; +} +.skill-block { padding: 20px; background: var(--bg-elev); border: 1px solid var(--border); } +.skill-block h4 { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--accent); + margin-bottom: 14px; + font-weight: 500; +} +.skill-list { + list-style: none; + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.skill-list li { + padding: 4px 10px; + font-size: 12px; + color: var(--fg); + background: var(--bg-soft); + border: 1px solid transparent; + transition: all 0.15s; +} +.skill-list li:hover { + border-color: var(--accent); + color: var(--accent); +} + +.css-demo { margin-top: 16px; } +.demo-note { + background: var(--bg-elev); + border: 1px dashed var(--border); + border-left: 3px solid var(--accent); + padding: 16px 20px; + font-size: 13px; + color: var(--fg-dim); + font-family: 'IBM Plex Mono', monospace; + margin-bottom: 28px; + max-width: 760px; + line-height: 1.6; +} +.demo-note-label { color: var(--accent); font-weight: 500; margin-right: 4px; } +.demo-note code { + color: var(--magenta); + background: var(--bg-soft); + padding: 1px 6px; + font-size: 12px; + border-radius: 2px; +} +.demo-note strong { color: var(--fg); font-weight: 500; } + +.skill-bars { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px 32px; + max-width: 760px; + margin-bottom: 24px; +} +.bar-item { display: flex; flex-direction: column; gap: 6px; } +.bar-row { display: flex; justify-content: space-between; font-size: 12px; } +.bar-name { color: var(--fg); } +.bar-pct { color: var(--fg-faint); font-family: 'JetBrains Mono', monospace; } +.bar-track { + height: 6px; + background: var(--bg-elev); + border: 1px solid var(--border); + overflow: hidden; + position: relative; +} +.bar-fill { + height: 100%; + width: 0; + background: linear-gradient(90deg, var(--accent-dim) 0%, var(--accent) 100%); + transition: width 1.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} +.bar-fill::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent); + transform: translateX(-100%); +} +.bar-fill.animated::after { animation: shimmer 1.4s ease-out; } +@keyframes shimmer { to { transform: translateX(100%); } } + +.animate-btn { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + color: var(--bg); + background: var(--accent); + border: 1px solid var(--accent); + padding: 10px 18px; + cursor: pointer; + transition: all 0.2s; + letter-spacing: 0.02em; + display: inline-flex; + align-items: center; + gap: 8px; +} +.animate-btn:hover { + background: transparent; + color: var(--accent); + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 var(--accent-dim); +} +.animate-btn .btn-prompt { color: var(--bg); opacity: 0.7; font-weight: 500; } +.animate-btn:hover .btn-prompt { color: var(--magenta); opacity: 1; } +.animate-btn.filled { + background: transparent; + color: var(--accent); + border-color: var(--accent-dim); +} +.animate-btn.filled .btn-prompt { color: var(--magenta); opacity: 1; } +.animate-btn.filled:hover { + background: var(--bg-elev); + border-color: var(--accent); +} + +/* ============================================================ + Project filter bar + ============================================================ */ +.filter-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 14px; +} +.filter-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--fg-faint); + margin-right: 4px; +} +.filter-chip { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + color: var(--fg-dim); + background: var(--bg-elev); + border: 1px solid var(--border); + padding: 6px 12px; + cursor: pointer; + transition: all 0.18s; + letter-spacing: 0.02em; + display: inline-flex; + align-items: center; + gap: 6px; +} +.filter-chip:hover { + color: var(--accent); + border-color: var(--accent-dim); +} +.filter-chip.active { + background: var(--accent); + color: var(--bg); + border-color: var(--accent); +} +.filter-chip .count { + font-size: 10px; + opacity: 0.7; + padding: 1px 5px; + background: rgba(0,0,0,0.18); + border-radius: 2px; +} +.filter-chip.active .count { background: rgba(255,255,255,0.18); } + +.filter-chip-current { + color: var(--secondary); + border-color: rgba(255, 180, 84, 0.35); +} +.filter-chip-current:hover { + color: var(--secondary); + border-color: var(--secondary); +} +.filter-chip-current.active { + background: var(--secondary); + color: var(--bg); + border-color: var(--secondary); +} + +.projects-note { + font-family: 'IBM Plex Mono', monospace; + font-size: 12px; + color: var(--fg-faint); + margin-bottom: 20px; + font-style: italic; +} + +/* ============================================================ + Project cards + ============================================================ */ +.projects { display: grid; gap: 1px; background: var(--border); border: 1px solid var(--border); } +.project { + background: var(--bg-elev); + padding: 28px 28px; + transition: background 0.2s; +} +.project:hover { background: var(--bg-soft); } +.project-header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 16px; + margin-bottom: 8px; + flex-wrap: wrap; +} +.project-name { + font-size: 17px; + color: var(--fg); + font-weight: 500; +} +.project-name::before { content: '└─ '; color: var(--accent); margin-right: 6px; } +.project-tech { + font-size: 11px; + color: var(--fg-faint); + letter-spacing: 0.05em; +} +.project-desc { color: var(--fg-dim); font-size: 13px; max-width: 680px; margin-top: 6px; } +.project-desc em { color: var(--accent); font-style: normal; } +.project-desc ul { margin-top: 8px; list-style: none; padding-left: 4px; } +.project-desc li { + padding-left: 16px; + position: relative; + margin-bottom: 3px; +} +.project-desc li::before { + content: '→'; + position: absolute; + left: 0; + color: var(--accent); +} + +/* Impact groups inside the Nara card — three subsections (providers / org admins / team) + each with a small uppercase label and a list of contributions with "why this matters" details */ +.impact-group { + margin-top: 18px; + padding: 16px 18px; + background: var(--bg); + border: 1px solid var(--border); + border-left: 2px solid var(--accent-dim); +} +.impact-group-label { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--accent); + margin-bottom: 10px; + font-weight: 500; +} +.impact-list { + list-style: none !important; + padding-left: 0 !important; + margin-top: 0 !important; +} +.impact-list li { + padding-left: 16px; + margin-bottom: 12px; + line-height: 1.55; +} +.impact-list li:last-child { margin-bottom: 0; } +.impact-list li::before { + content: '→'; + color: var(--accent); + position: absolute; + left: 0; +} +.impact-list li strong { + color: var(--fg); + font-weight: 500; + display: block; + margin-bottom: 2px; +} +.impact-detail { + display: block; + color: var(--fg-dim); + font-size: 12.5px; + font-style: italic; + padding-left: 0; +} +.impact-detail em { + color: var(--accent); + font-style: normal; +} +/* Prose-paragraph version of an impact group (replaces the bullet list). + Slightly smaller and looser than body so it reads inside the bordered box. */ +.impact-paragraph { + font-size: 13px; + color: var(--fg-dim); + line-height: 1.65; + margin: 0; +} +.impact-process { + margin-top: 16px; + font-size: 12px; + color: var(--fg-faint); + font-family: 'IBM Plex Mono', monospace; +} + +/* Repo-private note for academic projects without a public repo link */ +.repo-note { + margin-top: 14px; + font-size: 11px; + color: var(--fg-faint); + font-family: 'IBM Plex Mono', monospace; + font-style: italic; + opacity: 0.85; +} + +/* Inline kbd for in-prose keyboard hints */ +.inline-kbd { + color: var(--fg); + background: var(--bg-soft); + border: 1px solid var(--border); + padding: 1px 6px; + font-size: 11px; + border-radius: 3px; + font-family: 'JetBrains Mono', monospace; +} + +/* Category tags */ +.cat-tag { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--accent); + background: var(--bg-soft); + border: 1px solid var(--border); + padding: 2px 8px; + margin-left: 10px; + vertical-align: middle; + font-weight: 400; + border-radius: 2px; +} +.project[data-category="systems"] .cat-tag { color: var(--accent); border-color: rgba(126, 198, 153, 0.3); } +.project[data-category="web"] .cat-tag { color: var(--cyan); border-color: rgba(127, 179, 196, 0.3); } +.project[data-category="fun"] .cat-tag { color: var(--violet); border-color: rgba(183, 148, 217, 0.3); } +.project[data-category="tools"] .cat-tag { color: var(--magenta); border-color: rgba(217, 111, 158, 0.3); } + +.cat-tag-secondary { + color: var(--cyan) !important; + border-color: rgba(127, 179, 196, 0.3) !important; + margin-left: 4px; +} + +.project.project-featured { + border-left: 1.5px solid var(--secondary); + padding-left: 32px; +} +[data-theme="light"] .project.project-featured { + border-left-color: var(--secondary); +} + +.microservices { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1px; + background: var(--border); + border: 1px solid var(--border); + margin: 20px 0 18px; +} +.microservice { + background: var(--bg); + padding: 14px 16px; +} +.microservice-name { + font-size: 13px; + color: var(--fg); + font-weight: 500; + margin-bottom: 6px; + display: flex; + align-items: center; + gap: 8px; +} +.microservice-lang { + display: inline-block; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--secondary); + background: var(--bg-soft); + border: 1px solid rgba(255, 180, 84, 0.25); + padding: 1px 7px; + border-radius: 2px; + font-weight: 400; +} +.microservice-role { + font-size: 12px; + color: var(--fg-dim); + line-height: 1.5; +} + +.microservices-footnote { + font-size: 12.5px; + color: var(--fg-dim); + padding: 14px 16px; + background: var(--bg); + border: 1px dashed var(--border); + border-left: 1px solid var(--secondary); + margin: 14px 0 12px; + line-height: 1.6; + font-family: 'IBM Plex Mono', monospace; +} +.microservices-footnote strong { + color: var(--secondary); + font-weight: 500; +} + +.microservices-folders { + font-size: 11px; + color: var(--fg-faint); + font-family: 'JetBrains Mono', monospace; + margin-bottom: 12px; + letter-spacing: 0.02em; +} +.microservices-folders code { + color: var(--fg-dim); + background: var(--bg-soft); + padding: 1px 6px; + border-radius: 2px; + font-size: 11px; + border: 1px solid var(--border); +} + +.current-tag { + display: inline-flex; + align-items: center; + gap: 5px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--secondary); + background: rgba(255, 180, 84, 0.08); + border: 1px solid rgba(255, 180, 84, 0.35); + padding: 2px 8px; + margin-left: 6px; + vertical-align: middle; + font-weight: 500; + border-radius: 2px; +} +.current-tag::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--secondary); + box-shadow: 0 0 6px var(--secondary); + animation: pulse 2s ease infinite; +} +[data-theme="light"] .current-tag { + background: rgba(158, 93, 16, 0.08); + border-color: rgba(158, 93, 16, 0.4); +} + +.project-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 14px; +} +.play-link, .repo-link { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + text-decoration: none; + letter-spacing: 0.02em; + transition: all 0.2s; +} +.play-link { + color: var(--secondary); + background: var(--bg-soft); + border: 1px solid rgba(255, 180, 84, 0.25); +} +.play-link:hover { + background: var(--secondary); + color: var(--bg); + border-color: var(--secondary); + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 rgba(255, 180, 84, 0.3); +} +.repo-link { + color: var(--accent); + background: var(--bg-soft); + border: 1px solid var(--border); +} +.repo-link:hover { + background: var(--accent); + color: var(--bg); + border-color: var(--accent); + transform: translate(-1px, -1px); + box-shadow: 2px 2px 0 var(--accent-dim); +} + +.project[data-category] { + transition: opacity 0.25s, transform 0.25s, max-height 0.3s; +} +.project.hidden { display: none; } + +/* ============================================================ + Process & Practice + ============================================================ */ +.process-intro { + font-family: 'IBM Plex Mono', monospace; + font-size: 14px; + color: var(--fg-dim); + max-width: 740px; + margin-bottom: 32px; + padding-left: 24px; + border-left: 2px solid var(--accent-dim); +} + +.process-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 1px; + background: var(--border); + border: 1px solid var(--border); +} + +.process-col { + background: var(--bg-elev); + padding: 28px 30px; +} + +.process-col-title { + font-size: 17px; + color: var(--fg); + font-weight: 500; + margin-bottom: 14px; + letter-spacing: -0.005em; + display: flex; + flex-direction: column; + gap: 4px; +} +.process-col-tag { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--accent); + font-weight: 500; +} + +.process-col-blurb { + font-size: 13px; + color: var(--fg-dim); + margin-bottom: 18px; + font-family: 'IBM Plex Mono', monospace; +} +.process-col-blurb em { + color: var(--accent); + font-style: normal; +} + +.process-list { + list-style: none; + padding: 0; +} +.process-list li { + position: relative; + padding-left: 20px; + margin-bottom: 12px; + font-size: 13px; + color: var(--fg-dim); + line-height: 1.55; +} +.process-list li:last-child { margin-bottom: 0; } +.process-list li::before { + content: '→'; + position: absolute; + left: 0; + top: 0; + color: var(--accent); + font-weight: 500; +} +.process-list li strong { + color: var(--fg); + font-weight: 500; +} +.process-list li em { + color: var(--accent); + font-style: normal; +} +.process-list li code { + color: var(--magenta); + background: var(--bg-soft); + padding: 1px 5px; + font-size: 11px; + border-radius: 2px; + font-family: 'JetBrains Mono', monospace; +} +.process-list li kbd { + color: var(--fg); + background: var(--bg-soft); + border: 1px solid var(--border); + padding: 1px 6px; + font-size: 11px; + border-radius: 3px; + font-family: 'JetBrains Mono', monospace; + box-shadow: 0 1px 0 var(--border); +} + +/* ============================================================ + Certifications + ============================================================ */ +.cert-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1px; + background: var(--border); + border: 1px solid var(--border); +} +.cert-card { + background: var(--bg-elev); + padding: 22px 24px; + text-decoration: none; + color: inherit; + display: block; + transition: background 0.2s; + position: relative; +} +.cert-card:hover { background: var(--bg-soft); } +.cert-card:hover .cert-link { color: var(--accent); } +.cert-issuer { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--fg-faint); + margin-bottom: 8px; +} +.cert-title { + font-size: 16px; + color: var(--fg); + font-weight: 500; + margin-bottom: 14px; +} +.cert-link { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + color: var(--fg-dim); + transition: color 0.2s; + letter-spacing: 0.02em; +} + +/* ============================================================ + Contact + ============================================================ */ +.contact-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; + margin-top: 16px; +} +.contact-card { + display: block; + padding: 18px 20px; + background: var(--bg-elev); + border: 1px solid var(--border); + text-decoration: none; + color: var(--fg); + transition: all 0.2s; +} +.contact-card:hover { + border-color: var(--accent); + transform: translate(-2px, -2px); + box-shadow: 4px 4px 0 var(--accent-dim); +} +.contact-card-label { + font-size: 10px; + color: var(--fg-faint); + text-transform: uppercase; + letter-spacing: 0.12em; + margin-bottom: 4px; +} +.contact-card-value { font-size: 13px; color: var(--accent); } + +/* ============================================================ + Footer + ============================================================ */ +footer { + margin-top: 80px; + padding-top: 20px; + border-top: 1px dashed var(--border); + color: var(--fg-faint); + font-size: 11px; + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + font-family: 'JetBrains Mono', monospace; +} +footer .accent { color: var(--accent); } +footer .uptime-marker { color: var(--secondary); } + +/* ============================================================ + Responsive + ============================================================ */ +@media (max-width: 880px) { + .window-tabs { display: none; } + .window-title { + flex: 1; + justify-content: flex-start; + } + .command-input-wrap { + flex: 1 1 auto; + max-width: none; + } +} + +@media (max-width: 640px) { + .shell { padding: 32px 20px 64px; } + .entry { grid-template-columns: 1fr; gap: 8px; } + .hero-meta { font-size: 10px; } + .theme-toggle .theme-label { display: none; } + .window-title .prefix { display: none; } +} \ No newline at end of file