From 3ef3b63631881e173404b1b84cb9624443bb2a15 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:01:38 -0700 Subject: [PATCH 01/30] docs: document field notes portfolio --- README.md | 97 +++++++++++++++++-------------------------------------- 1 file changed, 30 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index e68988f..acb82e0 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,56 @@ # Personal Portfolio -A personality-forward static portfolio for my projects, writing, and contact information. +Quinton's maintained public portfolio: a personality-forward static site about applied software systems, product thinking, and learning through projects. -The site uses a playful rope-and-cow visual theme while organizing project data separately from presentation code. It is intended to become the main public portfolio for SimpleCaci. +## Direction -> **Status:** active work in progress. The main pages, styling, project data, transitions, and custom cursor are present, but several sections still contain placeholder copy and links. +The site is structured as a field notebook for projects that turn messy real-world input into useful output. Its featured work spans geospatial mission data, computer vision, desktop product design, handwriting OCR, voice interaction, and browser-based pose estimation. -## Pages and features +Project status language is intentionally specific. A repository link is not presented as a verified deployment or demo. -- home page with animated hero and current-project cards -- project gallery populated from `data/projects.json` -- writings page -- contact page -- custom cursor, rope interaction, and page transitions -- responsive CSS structure and local visual assets +## Featured work + +- Turion Hackathon 2025 / Solar System Missions +- Gesture-Reactive Avatar +- Ambient Dashboard +- HandwritingConverter +- VoxNav +- Rose Hackathon 2026 / Yoga Pose Match ## Technology - semantic HTML -- CSS -- vanilla JavaScript -- JSON-backed project/profile/writing data -- static hosting compatible - -## Project structure - -```text -index.html home -projects/ project gallery -writings/ writing links -contacts/ contact page -data/ profile, project, and writing content -js/ interactions and data rendering -css/ shared and page-specific styles -assets/ icons, project art, and visual theme -``` +- responsive CSS +- small, progressive vanilla JavaScript enhancement +- static-hosting-compatible relative paths ## Run locally -No build step is required. Use a local server so JSON requests and relative links behave consistently: +No build step is required. Serve the repository root so paths behave like GitHub Pages: ```bash python -m http.server 8000 ``` -Open [http://localhost:8000](http://localhost:8000). - -## Content updates - -Update portfolio content in: - -- `data/profile.json` -- `data/projects.json` -- `data/writings.json` - -Keep claims, project status, screenshots, and links accurate. Do not add placeholder awards, metrics, or deployments. - -## Validation status - -No automated tests or CI workflow currently exist. Before publishing, manually check: +Open . -- every navigation and project link -- desktop and mobile layouts -- keyboard focus and reduced-motion behavior -- image alternative text -- missing JSON/image failure states -- contact details and external links -- the site with JavaScript disabled or unavailable +## Validation -## Known unfinished work +Before publishing: -- the home page contains generic introduction/project copy -- writing entries include example placeholders -- the contact page references an external image -- navigation/footer sections are incomplete -- metadata and page titles are generic -- no deployment workflow or custom-domain documentation exists -- the custom cursor and animation effects need accessibility fallbacks +- parse the HTML and check local links/assets +- verify external project links +- test at desktop and mobile widths +- test keyboard navigation and visible focus +- test with reduced motion enabled +- check the browser console for errors +- verify the deployed GitHub Pages URL after merging -## Roadmap +## Deployment -- replace placeholder copy with a concise developer narrative -- feature the strongest verified projects and case studies -- add consistent project screenshots -- finish navigation and footer -- add reduced-motion and keyboard-friendly behavior -- validate and deploy the static site -- connect the profile README to the finished portfolio +The repository is associated with . There is no deployment workflow in the repository, so the GitHub Pages source and live response should be verified in repository settings after changes merge. ## Authorship -Designed and built by [SimpleCaci](https://github.com/SimpleCaci). A project license has not yet been selected. +Designed and built by [SimpleCaci](https://github.com/SimpleCaci). A project license has not yet been selected. + From 1c039d05c1bb09efb59f9a7d7f7967a8102e909f Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:01:40 -0700 Subject: [PATCH 02/30] feat(portfolio): build applied systems field notes --- index.html | 113 ++++++++--------------------------------------------- 1 file changed, 17 insertions(+), 96 deletions(-) diff --git a/index.html b/index.html index e82b0db..7439cd6 100644 --- a/index.html +++ b/index.html @@ -1,97 +1,18 @@ - - - - - - Quinton — Software Projects - - - - - - - - - - - - -
- -
- -
- -
- cursor image -
- -
- - -
- Top Bar Image -

/QUINTON

-
- - -
-

About Me

-

This is a brief introduction about myself.

-
- - -
-

Current Projects

-

This is my list of projects.

-
- -
-
- - - -
- -
- -
- - - - - - - - - +Quinton — Applied systems and software +
Q/ QuintonSay hello ↗
+

CS + business · California

I build systems for the parts of life that don’t arrive as clean data.

I’m Quinton. I turn movement, speech, handwriting, mission data, and everyday routines into software people can understand and act on.

Software is most interesting when it has to meet the real world.
+

Selected systems / 2025—2026

Six projects, one throughline.

Each starts with a messy input and asks the same question: what would make this genuinely useful?

+
01

Geospatial data · APIs · Visualization

Solar System Missions

An explorer that turns NASA JPL Horizons ephemeris data into navigable mission stories and 3D trajectory plots.

Mission query→JPL vectors→Trajectory
View repository ↗

StatusHackathon prototype under maintenance

Built withPython, Flask, JPL Horizons, Matplotlib, Three.js

Next proofCurrent end-to-end API validation

+
+
02 / COMPUTER VISIONWORKING PROTOTYPE

Gesture-Reactive Avatar

A local webcam pipeline that calibrates to a face, smooths expression changes, and maps them to a playful avatar.

facelandmarksreaction
  • OpenCV
  • MediaPipe
  • Python
Repository ↗
+
03 / PRODUCT SYSTEMSACTIVE PROTOTYPE

Ambient Dashboard

A calm desktop workspace for priorities and focus sessions—useful state without another noisy task app.

25focus05breaklocalstate
  • React
  • TypeScript
  • Tauri
  • Rust
Repository ↗
+
04 / DATA PIPELINEWORKING BETA

HandwritingConverter

A local pipeline that turns photographed notes into reviewable text, confidence reports, and printable multipage PDFs.

IMGOCRPDF
  • OpenCV
  • Tesseract
  • ReportLab
  • pytest
Repository ↗
+
05 / HUMAN INPUTFUNCTIONAL PROTOTYPE

VoxNav

A Windows voice-control assistant with explicit parsing, safe text-preview mode, and visible decision tracing.

HEARD move right faster
MATCHED move(direction, speed)
EXECUTED preview only
  • Python
  • Speech
  • PyAutoGUI
  • Tests
Repository ↗
+
06 / BROWSER VISIONHACKATHON PROTOTYPE

Yoga Pose Match

A camera-based movement game that compares body landmarks to data-derived pose targets and gives joint-specific coaching.

  • MediaPipe
  • FastAPI
  • Canvas
  • pandas
Repository ↗
+
+

Working method / 04 steps

Build the useful version.

  1. 01

    Start with friction

    Find a real workflow, not a feature looking for a reason to exist.

  2. 02

    Map the system

    Make inputs, transformations, constraints, and failure states visible.

  3. 03

    Test the risky part

    Separate deterministic logic from cameras, APIs, speech, and unstable edges.

  4. 04

    Tell the truth

    Call a prototype a prototype. Document what works and what still needs proof.

+

About / Quinton

Technical curiosity, with a business question attached.

I’m a computer science student interested in backend and full-stack development, data systems, optimization, geospatial computing, and computer vision.

Business changes how I look at software: who is it for, what does it replace, where does it fail, and is the result useful enough to keep?

Right now: strengthening project validation, building clearer case studies, and learning by shipping.

field assistant
+

Open channel / 01

Have an interesting problem?

I’m always interested in thoughtful projects, useful systems, and learning from people building them.

+ + From 06df69f4cbdcf4a4a3b7c57453c8dcd10792fdca Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:01:42 -0700 Subject: [PATCH 03/30] feat(portfolio): build applied systems field notes --- css/global.css | 470 +------------------------------------------------ 1 file changed, 2 insertions(+), 468 deletions(-) diff --git a/css/global.css b/css/global.css index f0c53cc..60bc9dd 100644 --- a/css/global.css +++ b/css/global.css @@ -1,468 +1,2 @@ -/* ========================= - GLOBAL RESET / BASELINE - ========================= */ -*, -*::before, -*::after { box-sizing: border-box; } - -html, body { height: 100%; } - -body { - margin: 0; - font-family: var(--font-main); - font-size: 16px; - letter-spacing: -0.01em; - color: var(--color-text-primary); - background-color: var(--color-primary); - - /* custom cursor */ - cursor: none; -} - -img, svg, video, canvas { - display: block; - max-width: 100%; - height: auto; -} - -:focus-visible { - outline: 2px solid rgba(255,255,255,0.55); - outline-offset: 3px; - border-radius: 10px; -} - -/* ========================= - ROOT TOKENS - ========================= */ -:root { - /* Primary Palette */ - --color-primary: #85b9f2; - --color-secondary: #3d73e6; - --color-accent: #918ce0; - - /* Shades of Gray */ - --gray-light: #e9ecef; - --gray-medium: #6c757d; - --gray-dark: #343a40; - - /* Utility/Status Colors */ - --color-success: #28a745; - --color-warning: #ffc107; - --color-error: #dc3545; - - /* UI Tokens */ - --color-text-primary: #0e1630; - --color-hover-background: rgba(255, 255, 255, 0.14); - - /* font */ - --font-main: "Space Grotesk", system-ui, sans-serif; - - /* layout / polish */ - --page-max: 1120px; - --page-pad: clamp(16px, 3vw, 32px); - --frame-pad: clamp(12px, 2vw, 24px); - - --card-pad: clamp(16px, 2.5vw, 28px); - --card-gap: clamp(14px, 2vw, 22px); - - --radius-card: 16px; - --radius-hero: 18px; - - --glass: rgba(255, 255, 255, 0.10); - --glass-hover: rgba(255, 255, 255, 0.12); - --glass-border: rgba(255, 255, 255, 0.18); - --glass-border-hover: rgba(255, 255, 255, 0.30); - - --shadow-card: 0 10px 26px rgba(0, 0, 0, 0.18); - --shadow-card-hover: 0 16px 40px rgba(0, 0, 0, 0.24); - - --shadow-hero: 0 18px 48px rgba(0,0,0,0.28); - --shadow-hero-hover: 0 22px 60px rgba(0,0,0,0.34); - - --transition-fast: 220ms ease; - --transition-med: 260ms ease; - - /* HERO IMAGE (cloud banner) */ - --hero-bg-url: url("../assets/images/clouds-banner.png"); -} - -/* ========================= - TYPE - ========================= */ -h1 { - margin: 0 0 0.35em 0; - font-size: clamp(30px, 3.2vw, 44px); - font-weight: 700; - letter-spacing: -0.03em; -} - -h2 { - margin: 0 0 0.45em 0; - font-size: clamp(20px, 2.1vw, 28px); - font-weight: 600; -} - -p { - margin: 0 0 1em 0; - font-size: 16px; - line-height: 1.6; -} - -/* ========================= - CUSTOM CURSOR (works with your JS) - cursor div can be inside main; fixed positioning ignores that - ========================= */ -#cursor { - position: fixed; - left: 0; - top: 0; - - width: 18px; - height: 18px; - border-radius: 999px; - - background: rgba(255, 255, 255, 0.78); - box-shadow: 0 12px 28px rgba(0,0,0,0.18); - backdrop-filter: blur(8px); - - pointer-events: none; - z-index: 999999; - - /* JS sets left/top + transform scale on hover */ - transform: translate(-50%, -50%) scale(1); - transition: transform 140ms ease, opacity 140ms ease, width 140ms ease, height 140ms ease; - - /* subtle breathing animation */ - animation: cursorPulse 2.4s ease-in-out infinite; -} - -@keyframes cursorPulse { - 0% { filter: drop-shadow(0 0 0 rgba(255,255,255,0)); } - 50% { filter: drop-shadow(0 0 10px rgba(255,255,255,0.35)); } - 100% { filter: drop-shadow(0 0 0 rgba(255,255,255,0)); } -} - -/* optional: hide cursor when leaving window */ -body.is-out #cursor { opacity: 0; } - -/* ========================= - LAYOUT - ========================= */ -main { - padding: var(--page-pad); -} - -.main-frame { - max-width: var(--page-max); - margin: 0 auto; - padding-inline: var(--frame-pad); -} - -/* ========================= - AUTO-CARDS (keep your behavior) - everything inside main becomes a “card” - EXCEPT the hero - ========================= */ -main > div:not(.top-bar-section) { - margin: 0 0 var(--card-gap) 0; - padding: var(--card-pad); - - border: 1px solid var(--glass-border); - border-radius: var(--radius-card); - - background: var(--glass); - backdrop-filter: blur(10px); - - box-shadow: var(--shadow-card); - - transition: - transform var(--transition-fast), - box-shadow var(--transition-fast), - background-color var(--transition-fast), - border-color var(--transition-fast); -} - -main > div:not(.top-bar-section):hover { - transform: translateY(-3px); - box-shadow: var(--shadow-card-hover); - border-color: var(--glass-border-hover); - background: var(--glass-hover); -} - -main > div > *:first-child { margin-top: 0; } -main > div > *:last-child { margin-bottom: 0; } - -@media (prefers-reduced-motion: reduce) { - #cursor { animation: none; transition: none; } - main > div:not(.top-bar-section) { transition: none; } - main > div:not(.top-bar-section):hover { transform: none; } -} - -/* ========================= - HERO (BANNER-SIZED, NOT FULLSCREEN) - ========================= */ -.top-bar-section { - width: 100%; - - /* banner sizing: prevents “giant full-screen hero” */ - min-height: unset; - height: clamp(220px, 34vh, 420px); - - margin: 0 0 var(--card-gap) 0; - - border-radius: var(--radius-hero); - overflow: hidden; - - border: 1px solid rgba(255, 255, 255, 0.14); - box-shadow: var(--shadow-hero); - - position: relative; - - /* dreamy cloud banner */ - background: var(--hero-bg-url) center / cover no-repeat; - - transition: - transform var(--transition-med), - box-shadow var(--transition-med), - border-color var(--transition-med); - - display: flex; /* Enable flexbox */ - align-items: center; /* Vertical centering */ - justify-content: center; /* Optional: horizontal centering */ - border: 1px solid #ccc; /* Just for visibility */ -} - -/* subtle parallax drift using background-position */ -@keyframes cloudDrift { - 0% { background-position: 50% 45%; } - 50% { background-position: 52% 48%; } - 100% { background-position: 50% 45%; } -} - -.top-bar-section { - animation: cloudDrift 14s ease-in-out infinite; - display: flex; - align-items: center; - justify-content: center; - text-align: center; -} - -.top-bar-section > *{ - margin:0%; - border: 0%; /* kills default h1 margin pushing things */ -} - -.top-bar-section h1{ - margin: 0; - line-height: 1; - font-size: clamp(42px, 7vw, 120px); /* adjust max if you want smaller */ -} - -.top-bar-section:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-hero-hover); - border-color: rgba(255, 255, 255, 0.35); -} - -/* soft overlay glow (no layout impact) */ -.top-bar-section::before { - content: ""; - position: absolute; - inset: 0; - pointer-events: none; - - background: - radial-gradient(900px 420px at 50% 30%, rgba(255,255,255,0.22), transparent 65%), - radial-gradient(900px 520px at 50% 120%, rgba(61,115,230,0.12), transparent 65%); - - mix-blend-mode: screen; - opacity: 0.85; -} - -/* optional: a gentle vignette for readability if text is on hero */ -.top-bar-section::after { - content: ""; - position: absolute; - inset: 0; - pointer-events: none; - - background: linear-gradient( - to bottom, - rgba(0,0,0,0.06), - rgba(0,0,0,0.12) - ); - - opacity: 0.55; - -} -/* ========================= - NAVBAR - ========================= */ -.top-nav { - height: 4rem; - display: flex; - align-items: center; - justify-content: center; -} - -.top-nav__inner { - display: flex; - align-items: center; - justify-content: center; -} - -.top-nav__list { - list-style: none; - margin: 0; - padding: 0; - - display: flex; - align-items: center; - gap: 0.5rem; -} - -.top-nav__item { margin: 0; padding: 0; } - -.top-nav__link { - color: var(--color-text-primary); - text-decoration: none; - - display: inline-flex; - align-items: center; - gap: 0.5rem; - - padding: 0.5rem 0.7rem; - border-radius: 0.75rem; - - font-size: 0.875rem; - font-weight: 500; - - transition: color 200ms ease, background-color 200ms ease, box-shadow 200ms ease, transform 200ms ease; -} - -.top-nav__link:hover { - color: var(--gray-light); - background: var(--color-hover-background); - box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22); - transform: translateY(-1px); -} - -.top-nav__icon { - width: 20px; - height: 20px; - flex: 0 0 auto; - display: block; - fill: currentColor; -} - -.top-nav__label { display: none; } - -@media (min-width: 768px) { - .top-nav__list { gap: 1rem; } - .top-nav__label { display: inline; } -} - -/* ========================= - ROPE (pull cord) - ========================= */ -@keyframes sway { - 0% { transform: translate(-50%, 0px) rotate(-2deg); } - 50% { transform: translate(-50%, 0px) rotate(2deg); } - 100% { transform: translate(-50%, 0px) rotate(-2deg); } -} - -#rope { - position: fixed; - top: 0; - right: 3%; - - transform: translate(-50%, 0px); - transform-origin: top center; - - z-index: 10000; - - width: min(70px, 5vw); - height: auto; - object-fit: contain; - - opacity: 1; - display: block; - pointer-events: auto; - cursor: grab; - - filter: drop-shadow(0 6px 6px rgba(0,0,0,0.25)); - transition: filter var(--transition-fast); -} - -#rope:hover { - filter: drop-shadow(0 10px 16px rgba(0,0,0,0.35)); -} - -#rope:active { - cursor: grabbing; - filter: drop-shadow(0 8px 14px rgba(237, 198, 69, 0.65)); -} - -/* optional idle sway */ -#rope.idle-sway { - animation: sway 2.8s ease-in-out infinite; -} - -/* ========================= - WORK IN PROGRESS IMAGE - ========================= */ -.work-in-progress-image { - border-top: 20px solid var(--color-error); - border-bottom: 20px solid var(--color-error); - border-radius: 13px; - - width: min(560px, 70%); - height: auto; - - display: block; - margin-left: auto; - margin-right: auto; - - transition: transform 240ms ease, filter 240ms ease; -} - -.work-in-progress-image:hover { - transform: scale(1.06); - filter: drop-shadow(0 12px 22px rgba(0,0,0,0.25)); -} - -/* ========================= - PAGE TRANSITION OVERLAY - ========================= */ -#wipe { - position: fixed; - inset: 0; - z-index: 9999; - pointer-events: none; - - margin: 0; - padding: 0; - - display: grid; - gap: 0; - - background: transparent !important; - opacity: 0; -} - -#wipe .bar { - margin: 0; - padding: 0; - border: 0; - border-radius: 0; - - width: 100%; - height: 100%; - - background: #0b0b0b !important; - transform: scaleX(0); - will-change: transform; -} - - - +:root{--ink:#08110f;--panel:#0d1916;--paper:#e9eee5;--muted:#9eaaa3;--line:rgba(233,238,229,.16);--green:#93e7ae;--amber:#f2b866;--blue:#82c9e8;--red:#ef8d78;--mono:"DM Mono",monospace;--sans:"Manrope",sans-serif;--max:1240px}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--ink);color:var(--paper);font:16px/1.6 var(--sans);-webkit-font-smoothing:antialiased}body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.03;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}a{color:inherit;text-decoration:none}a:focus-visible{outline:3px solid var(--green);outline-offset:4px}.skip{position:fixed;z-index:9;background:var(--paper);color:var(--ink);padding:.6rem 1rem;transform:translateY(-150%)}.skip:focus{transform:none}header,main,footer{width:min(calc(100% - 3rem),var(--max));margin:auto}body>header{height:84px;display:flex;align-items:center;gap:2rem;border-bottom:1px solid var(--line)}.brand{font-weight:700;margin-right:auto}.brand b,.eyebrow{font-family:var(--mono);color:var(--green)}nav{display:flex;gap:1.8rem}nav a,.hello{font-size:.82rem;color:var(--muted)}nav a:hover,.hello:hover{color:var(--paper)}.hello{border-left:1px solid var(--line);padding-left:2rem}.eyebrow{font-size:.68rem;letter-spacing:.08em;text-transform:uppercase;margin:0 0 1rem}.eyebrow i{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 15px var(--green);margin-right:.6rem}h1,h2,h3{line-height:1.08;letter-spacing:-.045em;margin-top:0}h1{font-size:clamp(3rem,7vw,7rem);font-weight:500}h2{font-size:clamp(2.3rem,4.5vw,4.6rem);font-weight:500}h3{font-size:1.55rem}.button{display:inline-flex;min-height:50px;align-items:center;padding:.65rem 1.15rem;border:1px solid var(--line);font-size:.82rem;font-weight:600;transition:.2s}.button:hover{transform:translateY(-2px);background:#ffffff0d}.button.primary{background:var(--green);border-color:var(--green);color:var(--ink)}.actions{display:flex;gap:.8rem}.hero{min-height:calc(100vh - 84px);display:grid;grid-template-columns:1.7fr .65fr;gap:clamp(2rem,6vw,7rem);align-items:center;position:relative;padding:7rem 0 5rem;border-bottom:1px solid var(--line);overflow:hidden}.grid{position:absolute;inset:0 45% 0 -20vw;background-image:linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px);background-size:50px 50px;opacity:.13;transform:perspective(500px) rotateX(65deg) translateY(45%)}.hero>div:not(.grid){z-index:1}.lead{max-width:720px;color:#c5cec8;font-size:clamp(1.05rem,1.7vw,1.35rem)}.hero .actions{margin-top:2.3rem}.hero aside{border:1px solid var(--line);background:#0d1916cc;padding:1rem}.aside-head{display:flex;justify-content:space-between;font:400 .58rem var(--mono);color:var(--muted)}.aside-head span:last-child{color:var(--green)}.orbit{height:220px;border:1px solid var(--line);margin:1.2rem 0;position:relative;background:radial-gradient(circle,#93e7ae15,transparent 55%)}.orbit:before,.orbit:after{content:"";position:absolute;inset:25%;border:1px solid #93e7ae55;border-radius:50%;transform:rotate(-20deg)}.orbit:after{inset:38%;border-style:dashed}.orbit i{position:absolute;width:7px;height:7px;border-radius:50%;background:var(--green);top:25%;left:29%;box-shadow:0 0 18px var(--green)}.orbit b{position:absolute;inset:49%;background:var(--paper);border-radius:50%}.hero dl{margin:0}.hero dl div{display:grid;grid-template-columns:65px 1fr;gap:1rem;border-top:1px solid var(--line);padding:.65rem 0}.hero dt{font:400 .6rem var(--mono);color:var(--muted);text-transform:uppercase}.hero dd{margin:0;font-size:.76rem}.hero small{position:absolute;bottom:1rem;color:var(--muted);font:400 .62rem var(--mono)}.section{padding:8rem 0;border-bottom:1px solid var(--line)}.section-head{display:grid;grid-template-columns:1.5fr .65fr;gap:4rem;align-items:end;margin-bottom:4rem}.section-head h2{margin:0}.section-head>p{color:var(--muted);font-size:.9rem}.feature{display:grid;grid-template-columns:70px 1.4fr .8fr;gap:2rem;padding:3rem 0;margin-bottom:1.2rem;border-block:1px solid var(--line)}.num{font:.8rem var(--mono);color:var(--green)}.feature h3{font-size:clamp(2rem,4vw,4rem);margin-bottom:1rem}.summary{color:#c1cac4;max-width:650px}.flow{display:flex;flex-wrap:wrap;align-items:center;gap:.7rem;margin:2rem 0;color:var(--green);font:.62rem var(--mono)}.flow span{border:1px solid var(--line);padding:.5rem .7rem;color:var(--paper)}.repo{font:500 .7rem var(--mono);border-bottom:1px solid var(--green);padding-bottom:.25rem}.facts{border-left:1px solid var(--line);padding-left:2rem}.facts p{font-size:.78rem;color:#c1cac4;border-bottom:1px solid var(--line);padding-bottom:1rem}.facts b{display:block;color:var(--muted);font:.58rem var(--mono);text-transform:uppercase}.cards{display:grid;grid-template-columns:repeat(2,1fr);gap:1.2rem}.card{--accent:var(--green);min-height:450px;padding:1.5rem;border:1px solid var(--line);display:flex;flex-direction:column;background:var(--panel);position:relative;transition:.2s}.card:before{content:"";position:absolute;inset:0 0 auto;height:2px;background:var(--accent)}.card:hover{transform:translateY(-3px);border-color:#ffffff55}.card.amber{--accent:var(--amber)}.card.blue{--accent:var(--blue)}.card.paper{--accent:var(--paper)}.card.red{--accent:var(--red)}.card.green{--accent:var(--green)}.card header{width:auto;height:auto;display:flex;justify-content:space-between;border:0;margin:0;font:400 .55rem var(--mono);color:var(--muted)}.card h3{font-size:2rem;margin:3rem 0 1rem}.card>p{color:#aeb9b2}.card ul{list-style:none;display:flex;flex-wrap:wrap;gap:.4rem;padding:0;margin:auto 0 1.5rem}.card li{font:.58rem var(--mono);border:1px solid var(--line);padding:.3rem .5rem}.card>a{align-self:flex-start}.visual{margin:1.2rem 0}.pipeline{display:flex;align-items:center;gap:.5rem;font:.55rem var(--mono);text-transform:uppercase}.pipeline i{width:35px;height:1px;background:var(--accent)}.metrics{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--line)}.metrics span{padding:.6rem;border-right:1px solid var(--line);font:.55rem var(--mono);color:var(--muted)}.metrics span:last-child{border:0}.metrics b{display:block;color:var(--accent);font-size:1.1rem}.docs{height:65px;position:relative}.docs span{position:absolute;width:65px;height:50px;border:1px solid var(--line);padding:.5rem;font:.55rem var(--mono);background:var(--panel)}.docs span:nth-child(2){left:38px;top:8px}.docs span:nth-child(3){left:76px;top:16px;border-color:var(--accent)}.terminal{background:#050907;border:1px solid var(--line);padding:.8rem;font:.55rem/1.8 var(--mono);color:var(--muted)}.terminal b{display:inline-block;width:65px;color:var(--accent)}.pose{height:75px;position:relative}.pose i{position:absolute;width:25px;height:25px;border:1px solid var(--accent);border-radius:50%;left:45px}.pose b{position:absolute;width:1px;height:45px;background:var(--accent);left:57px;top:25px}.pose span,.pose:after{content:"";position:absolute;width:55px;height:1px;background:var(--accent);left:30px;top:40px;transform:rotate(18deg)}.pose:after{top:68px;transform:rotate(-28deg)}.method{list-style:none;display:grid;grid-template-columns:repeat(4,1fr);padding:0;margin:0;border-top:1px solid var(--line)}.method li{padding:2rem 1.5rem 1rem 0;border-right:1px solid var(--line);min-height:250px}.method li+li{padding-left:1.5rem}.method span{font:.62rem var(--mono);color:var(--green)}.method h3{font-size:1.2rem;margin:4rem 0 1rem}.method p{font-size:.78rem;color:var(--muted)}.about{display:grid;grid-template-columns:1fr 1fr 180px;gap:4rem}.about h2{font-size:clamp(2.3rem,4vw,4rem)}.about>div>p{color:#c5cec8}.about .note{font:.65rem/1.6 var(--mono);color:var(--green);border-top:1px solid var(--line);padding-top:1rem}.about figure{margin:0;text-align:center;border:1px solid var(--line);padding:1rem;transform:rotate(2deg);background:#d8dfd5;color:var(--ink)}.about img{display:block;width:100%;height:auto;filter:grayscale(1) contrast(1.15);mix-blend-mode:multiply}.about figcaption{font:.55rem var(--mono);text-transform:uppercase}.contact{padding:10rem 0;text-align:center;max-width:850px;margin:auto}.contact>p:not(.eyebrow){max-width:600px;margin:0 auto 2rem;color:var(--muted)}.contact .actions{justify-content:center}footer{display:flex;justify-content:space-between;padding:2rem 0 3rem;border-top:1px solid var(--line);font:.65rem var(--mono);color:var(--muted)}footer p{margin:0}@media(max-width:950px){.hero{grid-template-columns:1fr}.hero aside{max-width:520px}.section-head{grid-template-columns:1fr;gap:1rem}.feature{grid-template-columns:45px 1fr}.facts{grid-column:2;border-left:0;padding-left:0;display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}.cards{grid-template-columns:1fr}.method{grid-template-columns:repeat(2,1fr)}.about{grid-template-columns:1fr 1fr}.about figure{display:none}}@media(max-width:650px){header,main,footer{width:calc(100% - 2rem)}body>header{height:70px}nav{display:none}.hello{padding-left:1rem}.hero{min-height:auto;padding:5rem 0 4rem}.hero aside{display:none}h1{font-size:clamp(2.8rem,14vw,4.8rem)}.actions{flex-direction:column}.section{padding:5rem 0}.feature{grid-template-columns:1fr;padding:2rem 0}.num{display:none}.facts{grid-column:1;grid-template-columns:1fr}.card{min-height:420px}.card header{flex-direction:column;gap:.3rem}.card h3{margin-top:2rem}.method{grid-template-columns:1fr}.method li,.method li+li{min-height:0;padding:2rem 0;border-right:0;border-bottom:1px solid var(--line)}.method h3{margin-top:2rem}.about{grid-template-columns:1fr;gap:1rem}.contact .actions{align-items:stretch}footer{display:grid;gap:.5rem}}@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*:before,*:after{transition-duration:.01ms!important}} + From f911983ff16f768d9b31e3b35056601492bedc14 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:01:44 -0700 Subject: [PATCH 04/30] feat(portfolio): build applied systems field notes --- css/index.css | 57 ++------------------------------------------------- 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/css/index.css b/css/index.css index 32dcf84..7985caf 100644 --- a/css/index.css +++ b/css/index.css @@ -1,55 +1,2 @@ -/* index.html */ -.top-bar-section { - position: relative; - display: inline-block; -} - -.top-bar-section img { - display: block; - width: 100%; - height: auto; -} - -.top-bar-section .page-title { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -100%); - color: var(--color-primary); - font-size: 9cqw; - font-weight: bold; - text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); -} - - - -/* others-sections */ -.other-sections-section { - display: flex; - flex-wrap: wrap; - justify-content: center; - container-type:inline-size; /* needed for cqw */ -} - - -.other-sections-section a{ /* to treat the div like a block */ - display:block; - text-decoration:none; - color:inherit; -} - -.other-sections-section a div { - margin: 10px; - padding: 20px; - font-size: 3.3cqw; - transition: background-color 0.2s ease, font-size 0.2s ease; -} - -.other-sections-section a:hover div { - font-size: 4cqw; - background-color: var(--color-primary); -} - -.other-sections-section a:active div { - filter: drop-shadow(0 0 10px gold); -} +/* Page styles are consolidated in global.css for the single-page portfolio. */ + From 963d82087c7da9551949fb9338867a1b202d07e1 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:01:45 -0700 Subject: [PATCH 05/30] feat(portfolio): build applied systems field notes --- js/main.js | 43 ++++--------------------------------------- 1 file changed, 4 insertions(+), 39 deletions(-) diff --git a/js/main.js b/js/main.js index 2483309..7c2e17a 100644 --- a/js/main.js +++ b/js/main.js @@ -1,39 +1,4 @@ -//global variables -let projects; - -//to grab and parse the JSON file -async function grabData(dataName) { - const requestURL = "../data/"+dataName+".json"; - const request = new Request(requestURL); - - const response = await fetch(request); - const data = await response.json(); - - console.log("Data loaded:", data) - - return data; -} - -//to populate projects -function populateProject(obj) { - const section = document.querySelector(".current-items-section"); - const myH1 = document.createElement("h1"); - myH1.textContent = obj.title; - section.appendChild(myH1); - - const myPara = document.createElement("p"); - myPara.textContent = `Disc: ${obj.description} // Status: ${obj.status}`; - section.appendChild(myPara); -} - - - -// async usage -(async function () { - const data = await grabData("projects"); - projects = data.projects; - for (const project of projects){ // javascript uses "of" to grab the elements and "for" to grab indices - populateProject(project); - } -})(); - +const year=document.querySelector("#year");if(year)year.textContent=String(new Date().getFullYear()); +const sections=[...document.querySelectorAll("main section[id]")],links=[...document.querySelectorAll("nav a[href^='#']")]; +if("IntersectionObserver" in window&§ions.length&&links.length){const observer=new IntersectionObserver(entries=>{const visible=entries.find(entry=>entry.isIntersecting);if(!visible)return;links.forEach(link=>link.toggleAttribute("aria-current",link.getAttribute("href")===`#${visible.target.id}`))},{rootMargin:"-35% 0px -55%",threshold:0});sections.forEach(section=>observer.observe(section));} + From 647e012edbbb933a537b9f79a3387b4919268254 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:27:51 -0700 Subject: [PATCH 06/30] feat(portfolio): complete editorial redesign --- README.md | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index acb82e0..a910d79 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,51 @@ # Personal Portfolio -Quinton's maintained public portfolio: a personality-forward static site about applied software systems, product thinking, and learning through projects. +Quinton's maintained public portfolio: a handcrafted static site about applied software systems, product thinking, and learning through projects. ## Direction -The site is structured as a field notebook for projects that turn messy real-world input into useful output. Its featured work spans geospatial mission data, computer vision, desktop product design, handwriting OCR, voice interaction, and browser-based pose estimation. +The design combines a technical field notebook with a restrained rope-and-ranch motif. It keeps the original cow mascot, but core navigation and content no longer depend on a custom cursor, animation, or JavaScript. -Project status language is intentionally specific. A repository link is not presented as a verified deployment or demo. +The featured work spans geospatial mission data, computer vision, desktop product design, handwriting OCR, voice interaction, and browser-based pose estimation. Project status language is intentionally specific: a repository link is not presented as a verified deployment or demo. -## Featured work +## Pages -- Turion Hackathon 2025 / Solar System Missions -- Gesture-Reactive Avatar -- Ambient Dashboard -- HandwritingConverter -- VoxNav -- Rose Hackathon 2026 / Yoga Pose Match +- `index.html` - homepage, selected work, method, about, and contact callout +- `projects/` - JSON-backed project field notes with no-JavaScript and load-failure fallbacks +- `writings/` - honest archive placeholder until finished writing exists +- `contacts/` - verified public email and GitHub links ## Technology - semantic HTML -- responsive CSS -- small, progressive vanilla JavaScript enhancement -- static-hosting-compatible relative paths +- responsive CSS with custom properties +- small progressive-enhancement JavaScript +- JSON-backed project content +- dependency-free static validation ## Run locally -No build step is required. Serve the repository root so paths behave like GitHub Pages: +No build step is required. Serve the repository root so JSON and relative paths behave like GitHub Pages: ```bash python -m http.server 8000 ``` -Open . +Open and visit every public page. -## Validation +## Validate -Before publishing: +```bash +python scripts/validate_site.py +``` -- parse the HTML and check local links/assets -- verify external project links -- test at desktop and mobile widths -- test keyboard navigation and visible focus -- test with reduced motion enabled -- check the browser console for errors -- verify the deployed GitHub Pages URL after merging +The script checks page structure, local links and images, alternative text, external-tab safety, obvious placeholders or encoding corruption, and the project JSON schema. GitHub Actions runs the same check for pull requests and pushes to `main`. ## Deployment -The repository is associated with . There is no deployment workflow in the repository, so the GitHub Pages source and live response should be verified in repository settings after changes merge. +The repository is associated with . Confirm the GitHub Pages source and live response after merging; the repository does not contain a custom deployment action. ## Authorship -Designed and built by [SimpleCaci](https://github.com/SimpleCaci). A project license has not yet been selected. +Designed and built by [SimpleCaci](https://github.com/SimpleCaci). A project license has not yet been selected. From a1202a25b796fb12164729b144e58053a15e61c5 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:27:52 -0700 Subject: [PATCH 07/30] feat(portfolio): complete editorial redesign --- contacts/index.html | 124 +++----------------------------------------- 1 file changed, 7 insertions(+), 117 deletions(-) diff --git a/contacts/index.html b/contacts/index.html index c0e835a..8aded9d 100644 --- a/contacts/index.html +++ b/contacts/index.html @@ -1,122 +1,12 @@ - - - - Contact - - - - - - - + Contact - Quinton - -
- -
- -
- - - -
- Top Bar Image -

/CONTACT

-
- -
-

Reach Out

- -

- Minimal. Direct. Human. -

- - -
-
-
email
- - simplesimpleemailemail@gmail.com - -
best for collabs & opportunities
-
- -
-
github
- - github.com/SimpleCaci - -
projects, experiments, code
-
-
- - -
-
- response time - 1–3 days -
-
- timezone - PST -
-
- status - open to ideas -
-
- - -

- cow-approved communication only. -

- - Cow QA -
- - -
- -
- -
- - - - - - - + + +

Contact / Open channel

Talk to a human, not a funnel.

If you want to discuss a project, a technical idea, or something useful that should exist, email is the simplest route. Public code and experiments live on GitHub.


Contact options

This page uses only contact information that was already public in the portfolio repository. There is no tracking script or third-party contact form.

Cow-approved communication
+ + + From 5ea7f2d1784d08ec912ace75a30e0362add624c5 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:27:54 -0700 Subject: [PATCH 08/30] feat(portfolio): complete editorial redesign --- css/contacts.css | 315 +++-------------------------------------------- 1 file changed, 15 insertions(+), 300 deletions(-) diff --git a/css/contacts.css b/css/contacts.css index c567021..13e82b4 100644 --- a/css/contacts.css +++ b/css/contacts.css @@ -1,300 +1,15 @@ -/* ===== FORCE CONTACT STYLES (wins over most existing CSS) ===== */ -.contacts{ - padding: 24px !important; - border-radius: 18px !important; - background: rgba(255,255,255,0.12) !important; - border: 1px solid rgba(255,255,255,0.22) !important; - backdrop-filter: blur(12px) !important; - box-shadow: 0 18px 44px rgba(0,0,0,0.18) !important; -} - -.contacts h1{ - margin: 0 0 12px 0 !important; - font-size: clamp(28px, 3.2vw, 44px) !important; - letter-spacing: -0.03em !important; -} - -.contacts .contacts-intro{ - margin: 0 0 14px 0 !important; - opacity: 0.85 !important; -} - -/* chips row */ -.contacts .chip-row{ - display: flex !important; - flex-wrap: wrap !important; - gap: 10px !important; - margin: 12px 0 14px 0 !important; -} - -.contacts .chip{ - border: 1px solid rgba(255,255,255,0.24) !important; - background: rgba(255,255,255,0.14) !important; - border-radius: 999px !important; - padding: 10px 12px !important; - cursor: pointer !important; - box-shadow: 0 12px 26px rgba(0,0,0,0.10) !important; -} - -.contacts .contact-form{ - display: grid !important; - gap: 14px !important; - padding: 18px !important; - border-radius: 16px !important; - background: rgba(255,255,255,0.12) !important; - border: 1px solid rgba(255,255,255,0.22) !important; - box-shadow: 0 14px 34px rgba(0,0,0,0.14) !important; -} - -.contacts .form-row{ display: grid !important; gap: 8px !important; } -.contacts .form-row-inline{ display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 12px !important; } -.contacts .inline-field{ display: grid !important; gap: 8px !important; } - -.contacts label{ font-size: 13px !important; opacity: 0.82 !important; } - -.contacts input, -.contacts select, -.contacts textarea{ - width: 100% !important; - padding: 12px 12px !important; - border-radius: 14px !important; - border: 1px solid rgba(255,255,255,0.26) !important; - background: rgba(255,255,255,0.18) !important; - outline: none !important; - box-shadow: inset 0 1px 0 rgba(255,255,255,0.22), 0 10px 26px rgba(0,0,0,0.10) !important; -} - -.contacts textarea{ min-height: 140px !important; resize: vertical !important; } - -.contacts .char-row{ - display:flex !important; - justify-content: space-between !important; - align-items:center !important; - gap: 12px !important; - margin-top: 6px !important; -} - -.contacts .microcopy{ font-size: 12px !important; opacity: 0.72 !important; } - -.contacts .form-actions{ - display:flex !important; - justify-content: space-between !important; - align-items:center !important; - gap: 12px !important; - flex-wrap: wrap !important; -} - -.contacts .contact-submit{ - border: 0 !important; - cursor: pointer !important; - padding: 12px 16px !important; - border-radius: 14px !important; - font-weight: 700 !important; - background: linear-gradient(135deg, rgba(63,115,230,0.78), rgba(184,156,255,0.66)) !important; - box-shadow: 0 16px 36px rgba(0,0,0,0.18) !important; -} - -/* mini cards */ -.contacts .contacts-mini{ - display:grid !important; - grid-template-columns: repeat(3, minmax(0, 1fr)) !important; - gap: 12px !important; - margin-top: 14px !important; -} - -.contacts .mini-card{ - padding: 12px !important; - border-radius: 14px !important; - border: 1px solid rgba(255,255,255,0.22) !important; - background: rgba(255,255,255,0.10) !important; - box-shadow: 0 12px 28px rgba(0,0,0,0.12) !important; -} - -.contacts .mini-link{ font-weight: 700 !important; } - -/* cow */ -.contacts .work-in-progress-image{ - margin-top: 16px !important; - border-radius: 18px !important; - border: 1px solid rgba(255,255,255,0.22) !important; - box-shadow: 0 22px 50px rgba(0,0,0,0.22) !important; -} - -/* phone */ -@media (max-width: 820px){ - .contacts .form-row-inline{ grid-template-columns: 1fr !important; } - .contacts .contacts-mini{ grid-template-columns: 1fr !important; } - .contacts .contact-submit{ width: 100% !important; } -} - - -/* contacts.css — styling for the simplified /CONTACT page */ - -.contacts{ - width: min(980px, calc(100% - 48px)); - margin: 42px auto 120px; -} - -.contacts h1{ - margin: 0 0 10px 0; -} - -.contacts-intro{ - margin: 0 0 18px 0; - opacity: 0.85; - line-height: 1.55; -} - -/* Cards grid */ -.contact-cards{ - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; - margin-top: 18px; -} - -.contact-card{ - padding: 16px 16px; - border-radius: 16px; - - background: rgba(255,255,255,0.08); - border: 1px solid rgba(255,255,255,0.16); - backdrop-filter: blur(10px); - - box-shadow: 0 14px 34px rgba(0,0,0,0.18); - transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease, background-color 180ms ease; -} - -.contact-card:hover{ - transform: translateY(-2px); - border-color: rgba(255,255,255,0.28); - box-shadow: 0 18px 46px rgba(0,0,0,0.26); - background: rgba(255,255,255,0.10); -} - -.contact-label{ - font-size: 0.92rem; - letter-spacing: 0.12em; - text-transform: uppercase; - opacity: 0.75; - margin-bottom: 10px; -} - -.contact-link{ - display: inline-block; - font-weight: 700; - text-decoration: none; - color: inherit; - - /* long emails wrap cleanly */ - overflow-wrap: anywhere; - - position: relative; - padding-bottom: 2px; -} - -/* crisp underline animation */ -.contact-link::after{ - content: ""; - position: absolute; - left: 0; - bottom: -2px; - height: 2px; - width: 100%; - transform: scaleX(0); - transform-origin: left; - transition: transform 180ms ease; - background: rgba(255,255,255,0.65); -} - -.contact-card:hover .contact-link::after{ - transform: scaleX(1); -} - -.contact-sub{ - margin-top: 10px; - opacity: 0.78; - line-height: 1.45; - font-size: 0.98rem; -} - -/* Meta row */ -.contact-meta{ - margin-top: 16px; - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; -} - -.meta-item{ - padding: 12px 14px; - border-radius: 14px; - - background: rgba(255,255,255,0.06); - border: 1px solid rgba(255,255,255,0.12); - backdrop-filter: blur(10px); - - box-shadow: 0 10px 24px rgba(0,0,0,0.14); -} - -.meta-title{ - display: block; - font-size: 0.86rem; - letter-spacing: 0.10em; - text-transform: uppercase; - opacity: 0.72; - margin-bottom: 6px; -} - -.meta-value{ - display: block; - font-weight: 650; - opacity: 0.92; -} - -/* Cow image */ -.work-in-progress-image{ - width: 100%; - max-width: 760px; - margin: 18px auto 0; - display: block; - - border-radius: 18px; - border: 1px solid rgba(255,255,255,0.14); - box-shadow: 0 18px 50px rgba(0,0,0,0.22); - - /* makes it feel more “embedded” into your glass theme */ - filter: saturate(1.05) contrast(1.02); -} - -.contacts-small{ - margin-top: 14px; - opacity: 0.75; - line-height: 1.5; -} - -/* Mobile */ -@media (max-width: 720px){ - .contacts{ - width: min(980px, calc(100% - 28px)); - margin: 34px auto 110px; - } - - .contact-cards{ - grid-template-columns: 1fr; - } - - .contact-meta{ - grid-template-columns: 1fr; - } -} - -/* Reduced motion */ -@media (prefers-reduced-motion: reduce){ - .contact-card{ - transition: none; - } - .contact-link::after{ - transition: none; - } -} +.contact-intro { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(17rem, .55fr); gap: var(--s6); align-items: end; padding-block: var(--s7); } +.contact-intro h1 { max-width: 10ch; margin-bottom: 0; } +.contact-intro p:last-child { color: var(--muted); } +.contact-layout { display: grid; grid-template-columns: minmax(0, 1fr) 16rem; gap: var(--s6); padding-block: var(--s7); } +.contact-list { display: grid; gap: var(--s3); } +.contact-card { display: grid; grid-template-columns: 8rem 1fr auto; gap: var(--s4); align-items: center; min-height: 8rem; padding: var(--s4); border: 1px solid var(--line); border-radius: var(--radius); background: rgba(255, 250, 240, .48); } +.contact-card h2 { margin-bottom: 0; font-family: var(--serif); font-size: 1.8rem; } +.contact-card p { margin-bottom: 0; color: var(--muted); font-size: .85rem; } +.contact-card a { color: var(--rust); font: 700 .74rem var(--mono); } +.contact-mascot { align-self: start; margin: 0; padding: var(--s4); border: 1px solid var(--line); background: var(--paper-deep); text-align: center; transform: rotate(1.5deg); } +.contact-mascot img { aspect-ratio: 1; object-fit: contain; filter: grayscale(1) contrast(1.1); mix-blend-mode: multiply; } +.contact-mascot figcaption { font: .61rem var(--mono); text-transform: uppercase; } +.privacy-note { max-width: var(--reading); margin-top: var(--s5); padding-top: var(--s3); border-top: 1px solid var(--line); color: var(--muted); font-size: .8rem; } +@media (max-width: 48rem) { .contact-intro, .contact-layout { grid-template-columns: 1fr; } .contact-card { grid-template-columns: 1fr; gap: .45rem; } .contact-mascot { display: none; } } + From ca37c54c9ad2ef17613442eb4c8ab2f29e467225 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:27:59 -0700 Subject: [PATCH 09/30] feat(portfolio): complete editorial redesign --- css/global.css | 84 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/css/global.css b/css/global.css index 60bc9dd..0bf5bf3 100644 --- a/css/global.css +++ b/css/global.css @@ -1,2 +1,84 @@ -:root{--ink:#08110f;--panel:#0d1916;--paper:#e9eee5;--muted:#9eaaa3;--line:rgba(233,238,229,.16);--green:#93e7ae;--amber:#f2b866;--blue:#82c9e8;--red:#ef8d78;--mono:"DM Mono",monospace;--sans:"Manrope",sans-serif;--max:1240px}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--ink);color:var(--paper);font:16px/1.6 var(--sans);-webkit-font-smoothing:antialiased}body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.03;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}a{color:inherit;text-decoration:none}a:focus-visible{outline:3px solid var(--green);outline-offset:4px}.skip{position:fixed;z-index:9;background:var(--paper);color:var(--ink);padding:.6rem 1rem;transform:translateY(-150%)}.skip:focus{transform:none}header,main,footer{width:min(calc(100% - 3rem),var(--max));margin:auto}body>header{height:84px;display:flex;align-items:center;gap:2rem;border-bottom:1px solid var(--line)}.brand{font-weight:700;margin-right:auto}.brand b,.eyebrow{font-family:var(--mono);color:var(--green)}nav{display:flex;gap:1.8rem}nav a,.hello{font-size:.82rem;color:var(--muted)}nav a:hover,.hello:hover{color:var(--paper)}.hello{border-left:1px solid var(--line);padding-left:2rem}.eyebrow{font-size:.68rem;letter-spacing:.08em;text-transform:uppercase;margin:0 0 1rem}.eyebrow i{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 15px var(--green);margin-right:.6rem}h1,h2,h3{line-height:1.08;letter-spacing:-.045em;margin-top:0}h1{font-size:clamp(3rem,7vw,7rem);font-weight:500}h2{font-size:clamp(2.3rem,4.5vw,4.6rem);font-weight:500}h3{font-size:1.55rem}.button{display:inline-flex;min-height:50px;align-items:center;padding:.65rem 1.15rem;border:1px solid var(--line);font-size:.82rem;font-weight:600;transition:.2s}.button:hover{transform:translateY(-2px);background:#ffffff0d}.button.primary{background:var(--green);border-color:var(--green);color:var(--ink)}.actions{display:flex;gap:.8rem}.hero{min-height:calc(100vh - 84px);display:grid;grid-template-columns:1.7fr .65fr;gap:clamp(2rem,6vw,7rem);align-items:center;position:relative;padding:7rem 0 5rem;border-bottom:1px solid var(--line);overflow:hidden}.grid{position:absolute;inset:0 45% 0 -20vw;background-image:linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px);background-size:50px 50px;opacity:.13;transform:perspective(500px) rotateX(65deg) translateY(45%)}.hero>div:not(.grid){z-index:1}.lead{max-width:720px;color:#c5cec8;font-size:clamp(1.05rem,1.7vw,1.35rem)}.hero .actions{margin-top:2.3rem}.hero aside{border:1px solid var(--line);background:#0d1916cc;padding:1rem}.aside-head{display:flex;justify-content:space-between;font:400 .58rem var(--mono);color:var(--muted)}.aside-head span:last-child{color:var(--green)}.orbit{height:220px;border:1px solid var(--line);margin:1.2rem 0;position:relative;background:radial-gradient(circle,#93e7ae15,transparent 55%)}.orbit:before,.orbit:after{content:"";position:absolute;inset:25%;border:1px solid #93e7ae55;border-radius:50%;transform:rotate(-20deg)}.orbit:after{inset:38%;border-style:dashed}.orbit i{position:absolute;width:7px;height:7px;border-radius:50%;background:var(--green);top:25%;left:29%;box-shadow:0 0 18px var(--green)}.orbit b{position:absolute;inset:49%;background:var(--paper);border-radius:50%}.hero dl{margin:0}.hero dl div{display:grid;grid-template-columns:65px 1fr;gap:1rem;border-top:1px solid var(--line);padding:.65rem 0}.hero dt{font:400 .6rem var(--mono);color:var(--muted);text-transform:uppercase}.hero dd{margin:0;font-size:.76rem}.hero small{position:absolute;bottom:1rem;color:var(--muted);font:400 .62rem var(--mono)}.section{padding:8rem 0;border-bottom:1px solid var(--line)}.section-head{display:grid;grid-template-columns:1.5fr .65fr;gap:4rem;align-items:end;margin-bottom:4rem}.section-head h2{margin:0}.section-head>p{color:var(--muted);font-size:.9rem}.feature{display:grid;grid-template-columns:70px 1.4fr .8fr;gap:2rem;padding:3rem 0;margin-bottom:1.2rem;border-block:1px solid var(--line)}.num{font:.8rem var(--mono);color:var(--green)}.feature h3{font-size:clamp(2rem,4vw,4rem);margin-bottom:1rem}.summary{color:#c1cac4;max-width:650px}.flow{display:flex;flex-wrap:wrap;align-items:center;gap:.7rem;margin:2rem 0;color:var(--green);font:.62rem var(--mono)}.flow span{border:1px solid var(--line);padding:.5rem .7rem;color:var(--paper)}.repo{font:500 .7rem var(--mono);border-bottom:1px solid var(--green);padding-bottom:.25rem}.facts{border-left:1px solid var(--line);padding-left:2rem}.facts p{font-size:.78rem;color:#c1cac4;border-bottom:1px solid var(--line);padding-bottom:1rem}.facts b{display:block;color:var(--muted);font:.58rem var(--mono);text-transform:uppercase}.cards{display:grid;grid-template-columns:repeat(2,1fr);gap:1.2rem}.card{--accent:var(--green);min-height:450px;padding:1.5rem;border:1px solid var(--line);display:flex;flex-direction:column;background:var(--panel);position:relative;transition:.2s}.card:before{content:"";position:absolute;inset:0 0 auto;height:2px;background:var(--accent)}.card:hover{transform:translateY(-3px);border-color:#ffffff55}.card.amber{--accent:var(--amber)}.card.blue{--accent:var(--blue)}.card.paper{--accent:var(--paper)}.card.red{--accent:var(--red)}.card.green{--accent:var(--green)}.card header{width:auto;height:auto;display:flex;justify-content:space-between;border:0;margin:0;font:400 .55rem var(--mono);color:var(--muted)}.card h3{font-size:2rem;margin:3rem 0 1rem}.card>p{color:#aeb9b2}.card ul{list-style:none;display:flex;flex-wrap:wrap;gap:.4rem;padding:0;margin:auto 0 1.5rem}.card li{font:.58rem var(--mono);border:1px solid var(--line);padding:.3rem .5rem}.card>a{align-self:flex-start}.visual{margin:1.2rem 0}.pipeline{display:flex;align-items:center;gap:.5rem;font:.55rem var(--mono);text-transform:uppercase}.pipeline i{width:35px;height:1px;background:var(--accent)}.metrics{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--line)}.metrics span{padding:.6rem;border-right:1px solid var(--line);font:.55rem var(--mono);color:var(--muted)}.metrics span:last-child{border:0}.metrics b{display:block;color:var(--accent);font-size:1.1rem}.docs{height:65px;position:relative}.docs span{position:absolute;width:65px;height:50px;border:1px solid var(--line);padding:.5rem;font:.55rem var(--mono);background:var(--panel)}.docs span:nth-child(2){left:38px;top:8px}.docs span:nth-child(3){left:76px;top:16px;border-color:var(--accent)}.terminal{background:#050907;border:1px solid var(--line);padding:.8rem;font:.55rem/1.8 var(--mono);color:var(--muted)}.terminal b{display:inline-block;width:65px;color:var(--accent)}.pose{height:75px;position:relative}.pose i{position:absolute;width:25px;height:25px;border:1px solid var(--accent);border-radius:50%;left:45px}.pose b{position:absolute;width:1px;height:45px;background:var(--accent);left:57px;top:25px}.pose span,.pose:after{content:"";position:absolute;width:55px;height:1px;background:var(--accent);left:30px;top:40px;transform:rotate(18deg)}.pose:after{top:68px;transform:rotate(-28deg)}.method{list-style:none;display:grid;grid-template-columns:repeat(4,1fr);padding:0;margin:0;border-top:1px solid var(--line)}.method li{padding:2rem 1.5rem 1rem 0;border-right:1px solid var(--line);min-height:250px}.method li+li{padding-left:1.5rem}.method span{font:.62rem var(--mono);color:var(--green)}.method h3{font-size:1.2rem;margin:4rem 0 1rem}.method p{font-size:.78rem;color:var(--muted)}.about{display:grid;grid-template-columns:1fr 1fr 180px;gap:4rem}.about h2{font-size:clamp(2.3rem,4vw,4rem)}.about>div>p{color:#c5cec8}.about .note{font:.65rem/1.6 var(--mono);color:var(--green);border-top:1px solid var(--line);padding-top:1rem}.about figure{margin:0;text-align:center;border:1px solid var(--line);padding:1rem;transform:rotate(2deg);background:#d8dfd5;color:var(--ink)}.about img{display:block;width:100%;height:auto;filter:grayscale(1) contrast(1.15);mix-blend-mode:multiply}.about figcaption{font:.55rem var(--mono);text-transform:uppercase}.contact{padding:10rem 0;text-align:center;max-width:850px;margin:auto}.contact>p:not(.eyebrow){max-width:600px;margin:0 auto 2rem;color:var(--muted)}.contact .actions{justify-content:center}footer{display:flex;justify-content:space-between;padding:2rem 0 3rem;border-top:1px solid var(--line);font:.65rem var(--mono);color:var(--muted)}footer p{margin:0}@media(max-width:950px){.hero{grid-template-columns:1fr}.hero aside{max-width:520px}.section-head{grid-template-columns:1fr;gap:1rem}.feature{grid-template-columns:45px 1fr}.facts{grid-column:2;border-left:0;padding-left:0;display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}.cards{grid-template-columns:1fr}.method{grid-template-columns:repeat(2,1fr)}.about{grid-template-columns:1fr 1fr}.about figure{display:none}}@media(max-width:650px){header,main,footer{width:calc(100% - 2rem)}body>header{height:70px}nav{display:none}.hello{padding-left:1rem}.hero{min-height:auto;padding:5rem 0 4rem}.hero aside{display:none}h1{font-size:clamp(2.8rem,14vw,4.8rem)}.actions{flex-direction:column}.section{padding:5rem 0}.feature{grid-template-columns:1fr;padding:2rem 0}.num{display:none}.facts{grid-column:1;grid-template-columns:1fr}.card{min-height:420px}.card header{flex-direction:column;gap:.3rem}.card h3{margin-top:2rem}.method{grid-template-columns:1fr}.method li,.method li+li{min-height:0;padding:2rem 0;border-right:0;border-bottom:1px solid var(--line)}.method h3{margin-top:2rem}.about{grid-template-columns:1fr;gap:1rem}.contact .actions{align-items:stretch}footer{display:grid;gap:.5rem}}@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*:before,*:after{transition-duration:.01ms!important}} +:root { + --ink: #171b16; + --paper: #f3eddf; + --paper-deep: #e5dac4; + --surface: #fffaf0; + --text: #20251f; + --muted: #5f685e; + --moss: #536b4d; + --rust: #a44f32; + --blue: #547183; + --rope: #a97842; + --line: rgba(32, 37, 31, 0.22); + --sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --serif: Georgia, "Times New Roman", serif; + --mono: "Courier New", ui-monospace, monospace; + --content: 76rem; + --reading: 45rem; + --s1: .4rem; + --s2: .7rem; + --s3: 1rem; + --s4: 1.5rem; + --s5: 2.3rem; + --s6: 3.6rem; + --s7: 5.6rem; + --radius: .55rem; + --shadow: 0 1.25rem 3rem rgba(55, 42, 25, .09); + --focus: 0 0 0 .25rem rgba(164, 79, 50, .3); +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { margin: 0; background: var(--paper); color: var(--text); font: 1rem/1.65 var(--sans); -webkit-font-smoothing: antialiased; } +body::before { position: fixed; inset: 0; z-index: -1; content: ""; opacity: .045; pointer-events: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); } +img { display: block; max-width: 100%; height: auto; } +a { color: inherit; text-underline-offset: .2em; } +a:hover { text-decoration-color: var(--rust); } +a:focus-visible, button:focus-visible, summary:focus-visible, select:focus-visible { outline: 0; box-shadow: var(--focus); } +button, select { font: inherit; } +p, h1, h2, h3, dl, figure { margin-top: 0; } +h1, h2, h3 { line-height: 1.08; text-wrap: balance; } +h1, h2 { font-family: var(--serif); font-weight: 400; letter-spacing: -.035em; } +h1 { font-size: clamp(3rem, 8vw, 7.4rem); } +h2 { font-size: clamp(2.2rem, 5vw, 4.4rem); } +h3 { font-size: clamp(1.25rem, 2vw, 1.75rem); } +.skip-link { position: fixed; top: 1rem; left: 1rem; z-index: 100; padding: .75rem 1rem; background: var(--ink); color: var(--paper); transform: translateY(-180%); } +.skip-link:focus { transform: none; } +.site-header, .site-footer, .page-shell { width: min(calc(100% - 2rem), var(--content)); margin-inline: auto; } +.site-header { display: flex; min-height: 5.25rem; align-items: center; gap: var(--s5); border-bottom: 1px solid var(--line); } +.wordmark { display: inline-flex; align-items: center; gap: .7rem; margin-right: auto; color: var(--text); font-weight: 750; text-decoration: none; } +.wordmark-mark { display: grid; width: 2rem; height: 2rem; place-items: center; border-radius: 50%; background: var(--ink); color: var(--paper); font: 700 .75rem var(--mono); } +.desktop-nav { display: flex; align-items: center; gap: 1.35rem; } +.desktop-nav a, .mobile-nav a { color: var(--muted); font-size: .81rem; font-weight: 650; text-decoration: none; } +.desktop-nav a:hover, .desktop-nav a[aria-current="page"], .mobile-nav a[aria-current="page"] { color: var(--rust); } +.nav-github { padding-left: 1.35rem; border-left: 1px solid var(--line); } +.mobile-nav { display: none; position: relative; } +.mobile-nav summary { display: grid; min-width: 2.8rem; min-height: 2.8rem; place-items: center; border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer; font: 700 .7rem var(--mono); list-style: none; } +.mobile-nav summary::-webkit-details-marker { display: none; } +.mobile-nav-menu { position: absolute; top: calc(100% + .6rem); right: 0; z-index: 20; display: grid; min-width: 13rem; padding: .55rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); } +.mobile-nav-menu a { padding: .8rem; } +.eyebrow, .annotation, .status-label, .tag-list li { font-family: var(--mono); font-size: .68rem; letter-spacing: .075em; text-transform: uppercase; } +.eyebrow { margin-bottom: var(--s3); color: var(--rust); } +.section-heading { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(16rem, .6fr); gap: var(--s6); align-items: end; margin-bottom: var(--s6); } +.section-heading h2, .section-heading > p { margin-bottom: 0; } +.section-heading > p { color: var(--muted); } +.button-row { display: flex; flex-wrap: wrap; gap: var(--s2); } +.button { display: inline-flex; min-height: 2.9rem; align-items: center; justify-content: center; padding: .7rem 1rem; border: 1px solid var(--ink); border-radius: var(--radius); font-size: .82rem; font-weight: 700; text-decoration: none; transition: transform 160ms, background 160ms, color 160ms; } +.button:hover { transform: translateY(-2px); } +.button-primary { background: var(--ink); color: var(--paper); } +.button-primary:hover { background: var(--rust); } +.text-link { color: var(--rust); font: 700 .74rem var(--mono); } +.tag-list { display: flex; flex-wrap: wrap; gap: .42rem; margin: 0; padding: 0; list-style: none; } +.tag-list li { padding: .32rem .5rem; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); font-size: .6rem; } +.status-label { color: var(--moss); } +.rope-rule { height: .5rem; border: 0; background: url("../assets/images/rope.png") center 47% / 4rem auto repeat-x; filter: sepia(.45) saturate(.65); opacity: .4; } +.empty-state, .error-state { padding: var(--s5); border: 1px dashed var(--rope); border-radius: var(--radius); background: rgba(255, 250, 240, .58); } +.error-state { border-color: var(--rust); } +.visually-hidden { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important; } +.site-footer { display: grid; grid-template-columns: 1fr auto; gap: var(--s4); padding-block: var(--s5); border-top: 1px solid var(--line); color: var(--muted); font-size: .77rem; } +.footer-nav { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--s3); } +.footer-note { margin: 0; } +@media (max-width: 48rem) { .desktop-nav { display: none; } .mobile-nav { display: block; } .site-header { min-height: 4.5rem; } .section-heading { grid-template-columns: 1fr; gap: var(--s2); } .site-footer { grid-template-columns: 1fr; } .footer-nav { justify-content: flex-start; } } +@media (max-width: 24rem) { .site-header, .site-footer, .page-shell { width: min(calc(100% - 1.25rem), var(--content)); } .button-row { display: grid; } } +@media (prefers-contrast: more) { :root { --line: rgba(19, 23, 18, .5); --muted: #394038; } .button, .tag-list li { border-width: 2px; } } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; } } From 3cbabcdd40c35a81d92bcafcdf7b72d5958bbdc3 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:03 -0700 Subject: [PATCH 10/30] feat(portfolio): complete editorial redesign --- css/index.css | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/css/index.css b/css/index.css index 7985caf..2e61176 100644 --- a/css/index.css +++ b/css/index.css @@ -1,2 +1,46 @@ -/* Page styles are consolidated in global.css for the single-page portfolio. */ +.hero { position: relative; display: grid; min-height: calc(100vh - 5.25rem); grid-template-columns: minmax(0, 1.55fr) minmax(17rem, .55fr); gap: clamp(2rem, 7vw, 7rem); align-items: center; padding-block: var(--s7); } +.hero::after { position: absolute; right: 1.5rem; bottom: 1.1rem; color: var(--muted); content: "FIELD NOTE 01 / APPLIED SYSTEMS"; font: .61rem var(--mono); letter-spacing: .08em; } +.hero h1 { max-width: 12ch; margin-bottom: var(--s4); } +.hero-lead { max-width: 43rem; color: var(--muted); font-size: clamp(1.05rem, 1.7vw, 1.3rem); } +.hero-card { position: relative; padding: var(--s4); border: 1px solid var(--line); border-radius: var(--radius); background: rgba(255, 250, 240, .58); box-shadow: var(--shadow); transform: rotate(1deg); } +.hero-card::before { position: absolute; top: -.45rem; left: 50%; width: 5rem; height: 1rem; background: rgba(173, 135, 83, .24); content: ""; transform: translateX(-50%) rotate(-2deg); } +.field-map { position: relative; height: 13rem; margin-bottom: var(--s3); border: 1px solid var(--line); background-image: linear-gradient(var(--line) 1px, transparent 1px), linear-gradient(90deg, var(--line) 1px, transparent 1px); background-size: 2rem 2rem; overflow: hidden; } +.field-map::before { position: absolute; inset: 20% 12%; border: 2px dashed var(--rope); border-radius: 50%; content: ""; transform: rotate(-18deg); } +.field-map::after { position: absolute; top: 31%; left: 28%; width: .65rem; height: .65rem; border-radius: 50%; background: var(--rust); box-shadow: 0 0 0 .3rem rgba(164, 79, 50, .18); content: ""; } +.field-list { margin: 0; } +.field-list div { display: grid; grid-template-columns: 4.6rem 1fr; gap: var(--s2); padding-block: .55rem; border-top: 1px solid var(--line); } +.field-list dt { color: var(--muted); font: .61rem var(--mono); text-transform: uppercase; } +.field-list dd { margin: 0; font-size: .77rem; } +.home-section { padding-block: var(--s7); border-top: 1px solid var(--line); } +.featured { display: grid; grid-template-columns: 3rem minmax(0, 1.25fr) minmax(15rem, .6fr); gap: var(--s4); padding-block: var(--s5); border-block: 1px solid var(--line); } +.featured h3 { margin-bottom: var(--s2); font: 400 clamp(2rem, 4vw, 3.9rem) var(--serif); } +.project-number { color: var(--rust); font: .74rem var(--mono); } +.system-flow { display: flex; flex-wrap: wrap; align-items: center; gap: .55rem; margin-block: var(--s4); color: var(--rope); font: .64rem var(--mono); } +.system-flow span { padding: .45rem .65rem; border: 1px solid var(--line); color: var(--text); } +.project-facts { padding-left: var(--s4); border-left: 1px solid var(--line); } +.project-facts p { padding-bottom: var(--s3); border-bottom: 1px solid var(--line); color: var(--muted); font-size: .79rem; } +.project-facts strong { display: block; margin-bottom: .2rem; color: var(--text); font: .61rem var(--mono); text-transform: uppercase; } +.preview-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s3); margin-top: var(--s3); } +.preview { --accent: var(--moss); display: flex; min-height: 23rem; flex-direction: column; padding: var(--s4); border: 1px solid var(--line); border-top: .3rem solid var(--accent); border-radius: var(--radius); background: rgba(255, 250, 240, .45); } +.preview:nth-child(2) { --accent: var(--blue); } .preview:nth-child(3) { --accent: var(--rope); } .preview:nth-child(4) { --accent: var(--rust); } +.preview h3 { margin: var(--s5) 0 var(--s2); font: 400 2rem var(--serif); } +.preview > p { color: var(--muted); } +.preview .tag-list { margin-top: auto; margin-bottom: var(--s4); } +.method-grid { display: grid; grid-template-columns: repeat(4, 1fr); margin: 0; padding: 0; border-top: 1px solid var(--line); list-style: none; } +.method-grid li { min-height: 15rem; padding: var(--s4); border-right: 1px solid var(--line); } +.method-grid li:last-child { border-right: 0; } +.method-grid span { color: var(--rust); font: .67rem var(--mono); } +.method-grid h3 { margin-top: var(--s5); } +.method-grid p { color: var(--muted); font-size: .81rem; } +.about-grid { display: grid; grid-template-columns: 1fr 1fr 10rem; gap: var(--s6); align-items: start; } +.about-copy { color: var(--muted); font-size: 1.03rem; } +.about-note { padding-top: var(--s3); border-top: 1px solid var(--line); color: var(--rust); font: .69rem var(--mono); } +.mascot { margin: 0; padding: var(--s3); border: 1px solid var(--line); background: var(--paper-deep); text-align: center; transform: rotate(2deg); } +.mascot img { aspect-ratio: 1; object-fit: contain; filter: grayscale(1) contrast(1.1); mix-blend-mode: multiply; } +.mascot figcaption { font: .59rem var(--mono); text-transform: uppercase; } +.home-contact { max-width: 50rem; margin-inline: auto; padding-block: var(--s7); text-align: center; } +.home-contact > p:not(.eyebrow) { max-width: 36rem; margin-inline: auto; color: var(--muted); } +.home-contact .button-row { justify-content: center; } +@media (max-width: 60rem) { .hero { min-height: auto; grid-template-columns: 1fr; } .hero-card { max-width: 32rem; } .featured { grid-template-columns: 2rem 1fr; } .project-facts { grid-column: 2; padding-left: 0; border-left: 0; } .method-grid { grid-template-columns: repeat(2, 1fr); } .about-grid { grid-template-columns: 1fr 1fr; } .mascot { display: none; } } +@media (max-width: 40rem) { .hero { padding-block: var(--s6); } .hero::after, .hero-card { display: none; } .featured { grid-template-columns: 1fr; } .project-number { display: none; } .project-facts { grid-column: 1; } .preview-grid, .method-grid, .about-grid { grid-template-columns: 1fr; } .method-grid li { min-height: 0; border-right: 0; border-bottom: 1px solid var(--line); } } From 9e282b0badb9bb25d1efabd24de28342d80bc347 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:05 -0700 Subject: [PATCH 11/30] feat(portfolio): complete editorial redesign --- css/projects.css | 461 ++++------------------------------------------- 1 file changed, 35 insertions(+), 426 deletions(-) diff --git a/css/projects.css b/css/projects.css index 090bcb0..d590352 100644 --- a/css/projects.css +++ b/css/projects.css @@ -1,426 +1,35 @@ -/*projects/index.html*/ - -/* ========================= - DATE SIDEBAR -========================= */ -.date-sidebar{ - position: fixed; - top: 30%; - right: 24px; - z-index: 999; - max-width: 190px; - - padding: 12px 14px; - border-radius: 14px; - - background: rgba(255,255,255,0.10); - border: 1px solid rgba(255,255,255,0.18); - backdrop-filter: blur(10px); - - color: var(--gray-light); - box-shadow: 0 14px 34px rgba(0,0,0,0.25); - - transition: transform 220ms ease, box-shadow 220ms ease, border-color 220ms ease, background-color 220ms ease; -} - -.date-sidebar:hover{ - transform: translateY(-2px); - box-shadow: 0 18px 46px rgba(0,0,0,0.32); - border-color: rgba(255,255,255,0.28); - background: rgba(255,255,255,0.12); -} - -.date-sidebar-label{ - font-size: 0.85rem; - opacity: 0.75; - margin: 0 0 6px 0; - letter-spacing: -0.01em; -} - -.date-sidebar-date{ - font-size: 1.05rem; - font-weight: 700; - margin: 0; - letter-spacing: -0.02em; -} - -/* optional: hide on small screens so it doesn't cover content */ -@media (max-width: 760px){ - .date-sidebar{ display:none; } -} - -/* ========================= - GRID LAYOUT (SIDE-BY-SIDE) -========================= */ -.current-items-section{ - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 18px; - align-items: stretch; - padding: 10px; -} - -/* ========================= - PROJECT CARD (COMPACT + YUMMY) -========================= */ -.project-card{ - display: flex; - flex-direction: column; - gap: 12px; - - padding: 1.2rem; - margin: 0; /* grid handles spacing */ - border-radius: 16px; - - background: rgba(255,255,255,0.08); - border: 1px solid rgba(255,255,255,0.18); - backdrop-filter: blur(10px); - - box-shadow: 0 12px 28px rgba(0,0,0,0.18); - transition: transform 220ms ease, box-shadow 220ms ease, border-color 220ms ease, background-color 220ms ease; -} - -.project-card:hover{ - transform: translateY(-6px); - box-shadow: 0 20px 46px rgba(0,0,0,0.28); - border-color: rgba(125, 211, 252, 0.35); - background: rgba(255,255,255,0.095); -} - -/* ========================= - HEADER -========================= */ -.project-header{ - display: flex; - flex-direction: column; - gap: 6px; -} - -.project-header h2{ - margin: 0; - font-size: 1.25rem; - letter-spacing: -0.02em; - line-height: 1.15; -} - -.project-meta{ - margin: 0; - font-size: 0.85rem; - opacity: 0.7; -} - -/* ========================= - IMAGE -========================= */ -.project-image-wrap{ - border-radius: 12px; - overflow: hidden; - border: 1px solid rgba(255,255,255,0.10); -} - -.project-image{ - width: 100%; - height: 160px; - object-fit: cover; - display: block; - transition: transform 300ms ease; -} - -.project-card:hover .project-image{ - transform: scale(1.04); -} - -/* ========================= - TECH ICONS ROW -========================= */ -.project-tech-icons{ - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; -} - -.tech-chip{ - display: inline-flex; - align-items: center; - justify-content: center; - height: 28px; - min-width: 28px; - padding: 0 8px; - border-radius: 999px; - - background: rgba(255,255,255,0.07); - border: 1px solid rgba(255,255,255,0.10); -} - -.tech-icon{ - width: 18px; - height: 18px; - object-fit: contain; - display: block; -} - -/* if icon missing, we show text */ -.tech-chip--text{ - font-size: 0.85rem; - opacity: 0.9; -} - -/* ========================= - BODY -========================= */ -.project-body{ - display: flex; - flex-direction: column; - gap: 10px; -} - -.project-desc{ - margin: 0; - line-height: 1.45; - font-size: 0.95rem; - opacity: 0.92; -} - -/* tighten the detail lines */ -.project-body p{ - margin: 0.35rem 0; - line-height: 1.5; -} - -/* optional: make the long “Problem/Approach/Result/Learned” block denser */ -.project-details{ - display: flex; - flex-direction: column; - gap: 6px; -} - -.project-details p{ - margin: 0; - font-size: 0.9rem; - opacity: 0.88; -} - -.project-details strong{ - opacity: 1; -} - -/* ========================= - FOOTER -========================= */ -.project-footer{ - margin-top: auto; - display: flex; - justify-content: flex-end; -} - -.project-link{ - color: var(--color-accent); - font-weight: 600; - text-decoration: none; - - padding: 8px 10px; - border-radius: 10px; - - background: rgba(125, 211, 252, 0.14); - border: 1px solid rgba(125, 211, 252, 0.25); - - transition: opacity 150ms ease, background-color 150ms ease, border-color 150ms ease, transform 150ms ease; -} - -.project-link:hover{ - opacity: 0.9; - background: rgba(125, 211, 252, 0.20); - border-color: rgba(125, 211, 252, 0.40); - transform: translateY(-1px); -} - -/* ========================= - NICE KEYBOARD FOCUS -========================= */ -.project-card:focus-within{ - outline: none; - border-color: rgba(145,140,224,0.40); - box-shadow: - 0 18px 44px rgba(0,0,0,0.28), - 0 0 0 3px rgba(145,140,224,0.22); -} - -/* ========================= - RESPONSIVE TUNING -========================= */ -@media (max-width: 420px){ - .current-items-section{ - grid-template-columns: 1fr; - } - .project-image{ - height: 140px; - } - .project-card{ - padding: 1rem; - } -} - -/* ========================= - REDUCE MOTION -========================= */ -@media (prefers-reduced-motion: reduce){ - .date-sidebar, - .project-card, - .project-image, - .project-link{ - transition: none; - } - - .date-sidebar:hover, - .project-card:hover{ - transform: none; - filter: none; - } -} - - - - - - - - - - - - - -/* Add to ../css/projects.css (or move to a dedicated writings.css if you want) */ - -.writings{ - width: min(980px, calc(100% - 48px)); - margin: 42px auto 120px; -} - -.writings-header{ - margin-bottom: 18px; -} - -.writings-header h1{ - margin: 0 0 8px 0; -} - -.writings-header p{ - margin: 0; - opacity: 0.85; - line-height: 1.5; -} - -.writings-list{ - list-style: none; - padding: 0; - margin: 18px 0 0 0; - - display: grid; - gap: 14px; -} - -.writing-item{ - margin: 0; -} - -.writing-link{ - display: grid; - grid-template-columns: 1fr auto; - gap: 14px; - align-items: center; - - padding: 14px 16px; - border-radius: 16px; - - text-decoration: none; - color: inherit; - - background: rgba(255,255,255,0.08); - border: 1px solid rgba(255,255,255,0.16); - backdrop-filter: blur(10px); - - transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease, background-color 180ms ease; - box-shadow: 0 14px 34px rgba(0,0,0,0.18); -} - -.writing-link:hover{ - transform: translateY(-2px); - border-color: rgba(255,255,255,0.28); - box-shadow: 0 18px 46px rgba(0,0,0,0.26); - background: rgba(255,255,255,0.10); -} - -.writing-link:active{ - transform: translateY(-1px); -} - -.writing-main{ - display: grid; - gap: 6px; - min-width: 0; -} - -.writing-title{ - font-weight: 700; - letter-spacing: 0.2px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.writing-desc{ - opacity: 0.82; - line-height: 1.35; - - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.writing-meta{ - opacity: 0.75; - font-size: 0.92rem; - white-space: nowrap; - padding-left: 10px; -} - -@media (max-width: 640px){ - .writing-link{ - grid-template-columns: 1fr; - } - .writing-meta{ - padding-left: 0; - } -} - - -.tech-chip{ - position:relative; - display:inline-flex; - align-items:center; - justify-content:center; -} - -.tech-chip::after{ - content:attr(data-label); - position:absolute; - left:50%; - bottom:calc(100% + 8px); - transform:translateX(-50%); - background:rgba(0,0,0,.85); - color:#fff; - font-size:12px; - padding:3px 8px; - border-radius:6px; - white-space:nowrap; - opacity:0; - pointer-events:none; - z-index:9999; -} - -.tech-chip:hover::after{ - opacity:1; -} +.page-intro { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(16rem, .6fr); gap: var(--s6); align-items: end; padding-block: var(--s7); } +.page-intro h1 { margin-bottom: 0; } +.page-intro-copy { color: var(--muted); } +.project-gallery { padding-bottom: var(--s7); } +.project-list { display: grid; gap: var(--s4); } +.case-card { --accent: var(--moss); display: grid; grid-template-columns: minmax(11rem, .38fr) minmax(0, 1fr); border: 1px solid var(--line); border-radius: var(--radius); background: rgba(255, 250, 240, .5); overflow: hidden; box-shadow: var(--shadow); } +.case-card:nth-child(2n) { --accent: var(--blue); } +.case-card:nth-child(3n) { --accent: var(--rust); } +.case-visual { position: relative; display: grid; min-height: 22rem; place-items: center; background: var(--paper-deep); border-right: 1px solid var(--line); overflow: hidden; } +.case-visual::before { position: absolute; width: 16rem; height: 9rem; border: 2px dashed var(--accent); border-radius: 50%; content: ""; transform: rotate(-18deg); opacity: .7; } +.case-visual::after { position: absolute; width: .8rem; height: .8rem; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 .45rem color-mix(in srgb, var(--accent), transparent 75%); content: ""; } +.case-initials { position: relative; z-index: 1; padding: .6rem .75rem; background: var(--paper-deep); font: 700 1.1rem var(--mono); } +.case-image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; } +.case-body { padding: clamp(1.4rem, 4vw, 3rem); } +.case-topline { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--s2); margin-bottom: var(--s4); } +.case-body h2 { margin-bottom: var(--s2); font-size: clamp(2rem, 4vw, 3.6rem); } +.case-summary { max-width: var(--reading); color: var(--muted); font-size: 1.05rem; } +.case-notes { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s3); margin-block: var(--s5); } +.case-notes div { padding-top: var(--s3); border-top: 1px solid var(--line); } +.case-notes dt { margin-bottom: .35rem; color: var(--rust); font: .62rem var(--mono); text-transform: uppercase; } +.case-notes dd { margin: 0; color: var(--muted); font-size: .82rem; } +.case-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s3); } +.loading-state { display: flex; align-items: center; gap: var(--s3); padding: var(--s5); border: 1px solid var(--line); } +.loading-mark { width: 1rem; height: 1rem; border: 2px solid var(--line); border-top-color: var(--rust); border-radius: 50%; animation: spin .8s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } +.writing-page { min-height: 55vh; padding-bottom: var(--s7); } +.coming-soon { display: grid; grid-template-columns: 1fr minmax(12rem, .35fr); gap: var(--s6); max-width: 62rem; padding: var(--s5); border: 1px dashed var(--rope); background: rgba(255, 250, 240, .5); } +.coming-soon p { color: var(--muted); } +.note-stack { display: grid; align-content: start; gap: .7rem; } +.note-stack span { display: block; padding: .7rem; border: 1px solid var(--line); background: var(--surface); font: .62rem var(--mono); transform: rotate(-1deg); } +.note-stack span:nth-child(2) { transform: rotate(1.5deg); } +@media (max-width: 56rem) { .page-intro { grid-template-columns: 1fr; gap: var(--s3); } .case-card { grid-template-columns: 1fr; } .case-visual { min-height: 12rem; border-right: 0; border-bottom: 1px solid var(--line); } .case-notes { grid-template-columns: 1fr; } } +@media (max-width: 38rem) { .page-intro { padding-block: var(--s6); } .case-actions { align-items: flex-start; flex-direction: column; } .coming-soon { grid-template-columns: 1fr; } } +@media (prefers-reduced-motion: reduce) { .loading-mark { animation: none; border-top-color: var(--line); } } + From dad7e6c537f7979eaf3f55ee8deed73f6763bcd5 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:07 -0700 Subject: [PATCH 12/30] feat(portfolio): complete editorial redesign --- data/profile.json | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/data/profile.json b/data/profile.json index 0ba4d27..2862745 100644 --- a/data/profile.json +++ b/data/profile.json @@ -1,11 +1,10 @@ { - "profile": { - "name": "Quinton", - "tagline": "CS student building projects and learning systems.", - "now": [ - "Building my portfolio site", - "Learning HTML, CSS, and JavaScript", - "Studying finance and investing basics" - ] - } -} \ No newline at end of file + "schemaVersion": 2, + "profile": { + "name": "Quinton", + "tagline": "Computer science student building practical software systems and technical experiments.", + "interests": ["Backend and full-stack development", "Data systems and optimization", "Geospatial computing", "Computer vision", "Practical product thinking"], + "now": ["Strengthening project validation", "Building clearer case studies", "Learning by shipping"] + } +} + From 70f09ce5e66c8a706ba943afacd08b97a364fc22 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:09 -0700 Subject: [PATCH 13/30] feat(portfolio): complete editorial redesign --- data/projects.json | 154 ++++++++++++++++++++++----------------------- 1 file changed, 74 insertions(+), 80 deletions(-) diff --git a/data/projects.json b/data/projects.json index a2d65bc..9f66834 100644 --- a/data/projects.json +++ b/data/projects.json @@ -1,96 +1,90 @@ { + "schemaVersion": 2, "projects": [ { - "title": "Maple The Shepherd - Game", - "description": "Submission for the Brainless Mini Game Jam", - "status": "In Progress", - "tech": ["Godot", "GDScript", "Asprite", "Beepbox.co"], - "date": "2025-12-25", - "image": "../assets/projects/maple-the-sheep.png", - "link": "https://github.com/SimpleCaci/personal-portfolio/tree/main", - "problem": "Create a game revolving around the theme of Simpler Time", - "approach": "Designed a clean layout and built it with vanilla HTML/CSS/JS", - "result": "A retro golf-like shepherding game", - "learned": "Structuring front-end projects cleanly" + "id": "solar-system-missions", + "title": "Solar System Missions", + "shortDescription": "Explore spacecraft missions through NASA JPL Horizons data and generated trajectory plots.", + "category": "Geospatial systems", + "technologies": ["Python", "Flask", "JPL Horizons", "Matplotlib", "Three.js"], + "status": "Hackathon prototype under maintenance", + "repositoryUrl": "https://github.com/SimpleCaci/Turion-Hackathon-2025", + "year": "2025", + "problem": "Real mission data is powerful, but difficult to explore without specialized tools.", + "approach": "Query public ephemeris data, discover mission ranges, and turn vectors into a web-based mission story and trajectory image.", + "limitation": "The network-dependent workflow still needs current end-to-end API validation.", + "featured": true }, { - "title": "Portfolio Website", - "description": "My personal website to document projects and learning.", - "status": "In Progress", - "tech": ["HTML", "CSS", "JavaScript"], - "date": "2025-12-25", - "image": "../assets/projects/personal-portfolio.png", - "link": "https://github.com/SimpleCaci/personal-portfolio/tree/main", - "problem": "I needed to build a website for showcasing my skills", - "approach": "Designed a clean layout and built it with vanilla HTML/CSS/JS", - "result": "A responsive personal portfolio", - "learned": "Structuring front-end projects cleanly" + "id": "gesture-reactive-avatar", + "title": "Gesture-Reactive Avatar", + "shortDescription": "A local avatar that calibrates to a face and reacts to expressions in real time.", + "category": "Computer vision", + "technologies": ["Python", "OpenCV", "MediaPipe"], + "status": "Working prototype", + "repositoryUrl": "https://github.com/SimpleCaci/Gesture-Reactive-Avatar", + "year": "2025", + "problem": "A lightweight streaming avatar needs to feel responsive without uploading camera frames.", + "approach": "Measure facial landmarks locally, calibrate to the current user, smooth rapid changes, and select reaction images.", + "limitation": "Thresholds and camera behavior need testing across more faces, lighting, and devices.", + "featured": true }, { - "title": "Stop Time Blindness", - "description": "A lightweight task manager for daily productivity.", - "status": "Completed", - "tech": ["JavaScript", "PIP", "CSS", "cookies"], - "date": "2025-01-10", - "image": "../assets/projects/stop-time-blindness.png", - "link": "https://simplecaci.github.io/Stop-Time-Blindness/", - "problem": "I needed a simple way to track daily tasks", - "approach": "Used localStorage to persist tasks and JS for logic", - "result": "A fast offline task tracker that works in PIP for tasks", - "learned": "State management and DOM manipulation" + "id": "ambient-dashboard", + "title": "Ambient Dashboard", + "shortDescription": "A calm desktop workspace for priorities, progress, and focus sessions.", + "category": "Product systems", + "technologies": ["React", "TypeScript", "Tauri", "Rust"], + "status": "Active prototype", + "repositoryUrl": "https://github.com/SimpleCaci/ambient-dashboard", + "year": "2025", + "problem": "Daily state should be visible without becoming another noisy task manager.", + "approach": "Combine reusable progress controls, local persistence, theme palettes, and a focus timer inside a desktop shell.", + "limitation": "Weather, calendar integration, component tests, and native packaging remain future work.", + "featured": true }, { - "title": "Reading Crystal Ball", - "description": "Cutie Hack 2025 - Gen AI Winner", - "status": "Completed", - "tech": ["HTML","Python","Python-Flask", "JavaScript", "CSS", "genai"], - "date": "2025-11-15", - "image": "../assets/projects/reading-crystal-ball.png", - "link": "https://example.com", - "problem": "A computer vision project which help children read through a translating text to old english and speaking thorough a text to speech engine", - "approach": "Built a schedule-based planner UI", - "result": "Improved daily productivity", - "learned": "UI structure and time-based logic" - }, - { - "title": "Space Generation", - "description": "Turion Space Hack 2025 - Top 3 Winner", - "status": "Completed", - "tech": ["HTML","Python","Python-Flask", "API", "JavaScript", "CSS", "genai"], - "date": "2025-02-02", - "image": "../assets/projects/default.png", - "link": "https://github.com/SimpleCaci/Turion-Hackathon-2025", - "problem": "Help educate and be more educated about space events", - "approach": "Fetched data from the NASA API API", - "result": "Live learning of any past space event", - "learned": "Async fetch and GenAI systems" + "id": "handwriting-converter", + "title": "HandwritingConverter", + "shortDescription": "Turn photographed notes into reviewable text, confidence reports, and printable PDFs.", + "category": "Data pipeline", + "technologies": ["Python", "OpenCV", "Tesseract", "ReportLab", "pytest"], + "status": "Working beta", + "repositoryUrl": "https://github.com/SimpleCaci/HandwritingConverter", + "year": "2025", + "problem": "Handwriting is quick for thinking, but paper notes are difficult to search, revise, and print.", + "approach": "Preprocess images locally, run OCR, report page confidence, and create text, HTML review, and PDF outputs.", + "limitation": "Recognition quality still depends on handwriting, lighting, camera angle, and Tesseract language data.", + "featured": true }, { - "title": "Handwriting Converter", - "description": "Converts handwritten text into digital text.", - "status": "Prototype", - "tech": ["Python", "OCR", "OpenCV"], - "date": "2025-03-05", - "image": "../assets/projects/default.png", - "link": "https://example.com", - "problem": "Needed a way to digitize handwritten notes", - "approach": "Used OCR libraries to process images", - "result": "Basic handwriting recognition tool", - "learned": "Image processing and OCR pipelines" + "id": "voxnav", + "title": "VoxNav", + "shortDescription": "A Windows voice-control assistant with explicit commands and a safe preview mode.", + "category": "Human input", + "technologies": ["Python", "SpeechRecognition", "PyAutoGUI", "unittest"], + "status": "Functional Windows prototype", + "repositoryUrl": "https://github.com/SimpleCaci/VoxNav", + "year": "2025", + "problem": "Hands-free desktop navigation needs predictable commands and visible safety boundaries.", + "approach": "Parse normalized transcripts into allowlisted actions, trace each decision, and provide a side-effect-free text preview.", + "limitation": "Speech currently uses an external service; real desktop actions still need intentional manual testing.", + "featured": true }, { - "title": "Fitness Tracker", - "description": "Logs workouts and tracks progress.", - "status": "Planned", - "tech": ["JavaScript", "Chart.js"], - "date": "2025-03-20", - "image": "../assets/projects/default.png", - "link": "https://example.com", - "problem": "Wanted to visualize workout progress", - "approach": "Designed charts for reps and sets", - "result": "Clear visualization of fitness data", - "learned": "Data visualization basics" + "id": "yoga-pose-match", + "title": "Yoga Pose Match", + "shortDescription": "A browser-based pose game with live landmark feedback and joint-specific coaching.", + "category": "Browser vision", + "technologies": ["JavaScript", "MediaPipe", "Canvas", "FastAPI", "pandas"], + "status": "Functional hackathon prototype", + "repositoryUrl": "https://github.com/SimpleCaci/Rose-Hackathon-2026", + "year": "2026", + "problem": "Pose-estimation data needs friendly feedback before it feels like a movement game.", + "approach": "Compare browser camera landmarks to data-derived angle targets and name the joint furthest from its target.", + "limitation": "Camera behavior and pose accuracy still require cross-device testing.", + "featured": true } ] } - + From b04e6217b89354ee3ed9632a7fb66e343789fffe Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:10 -0700 Subject: [PATCH 14/30] feat(portfolio): complete editorial redesign --- data/writings.json | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/data/writings.json b/data/writings.json index cc1fade..68e00bb 100644 --- a/data/writings.json +++ b/data/writings.json @@ -1,9 +1,5 @@ { - "writing": [ - { - "title": "Why I Started This Site", - "date": "2024-12-25", - "summary": "Documenting my learning journey and building in public." - } - ] -} \ No newline at end of file + "schemaVersion": 2, + "writing": [] +} + From 1d4cf15925f4b160352ee20d4662725e5e0b5150 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:12 -0700 Subject: [PATCH 15/30] feat(portfolio): complete editorial redesign --- index.html | 63 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index 7439cd6..28aed24 100644 --- a/index.html +++ b/index.html @@ -1,18 +1,49 @@ -Quinton — Applied systems and software -
Q/ QuintonSay hello ↗
-

CS + business · California

I build systems for the parts of life that don’t arrive as clean data.

I’m Quinton. I turn movement, speech, handwriting, mission data, and everyday routines into software people can understand and act on.

Software is most interesting when it has to meet the real world.
-

Selected systems / 2025—2026

Six projects, one throughline.

Each starts with a messy input and asks the same question: what would make this genuinely useful?

-
01

Geospatial data · APIs · Visualization

Solar System Missions

An explorer that turns NASA JPL Horizons ephemeris data into navigable mission stories and 3D trajectory plots.

Mission query→JPL vectors→Trajectory
View repository ↗

StatusHackathon prototype under maintenance

Built withPython, Flask, JPL Horizons, Matplotlib, Three.js

Next proofCurrent end-to-end API validation

-
-
02 / COMPUTER VISIONWORKING PROTOTYPE

Gesture-Reactive Avatar

A local webcam pipeline that calibrates to a face, smooths expression changes, and maps them to a playful avatar.

facelandmarksreaction
  • OpenCV
  • MediaPipe
  • Python
Repository ↗
-
03 / PRODUCT SYSTEMSACTIVE PROTOTYPE

Ambient Dashboard

A calm desktop workspace for priorities and focus sessions—useful state without another noisy task app.

25focus05breaklocalstate
  • React
  • TypeScript
  • Tauri
  • Rust
Repository ↗
-
04 / DATA PIPELINEWORKING BETA

HandwritingConverter

A local pipeline that turns photographed notes into reviewable text, confidence reports, and printable multipage PDFs.

IMGOCRPDF
  • OpenCV
  • Tesseract
  • ReportLab
  • pytest
Repository ↗
-
05 / HUMAN INPUTFUNCTIONAL PROTOTYPE

VoxNav

A Windows voice-control assistant with explicit parsing, safe text-preview mode, and visible decision tracing.

HEARD move right faster
MATCHED move(direction, speed)
EXECUTED preview only
  • Python
  • Speech
  • PyAutoGUI
  • Tests
Repository ↗
-
06 / BROWSER VISIONHACKATHON PROTOTYPE

Yoga Pose Match

A camera-based movement game that compares body landmarks to data-derived pose targets and gives joint-specific coaching.

  • MediaPipe
  • FastAPI
  • Canvas
  • pandas
Repository ↗
-
-

Working method / 04 steps

Build the useful version.

  1. 01

    Start with friction

    Find a real workflow, not a feature looking for a reason to exist.

  2. 02

    Map the system

    Make inputs, transformations, constraints, and failure states visible.

  3. 03

    Test the risky part

    Separate deterministic logic from cameras, APIs, speech, and unstable edges.

  4. 04

    Tell the truth

    Call a prototype a prototype. Document what works and what still needs proof.

-

About / Quinton

Technical curiosity, with a business question attached.

I’m a computer science student interested in backend and full-stack development, data systems, optimization, geospatial computing, and computer vision.

Business changes how I look at software: who is it for, what does it replace, where does it fail, and is the result useful enough to keep?

Right now: strengthening project validation, building clearer case studies, and learning by shipping.

field assistant
-

Open channel / 01

Have an interesting problem?

I’m always interested in thoughtful projects, useful systems, and learning from people building them.

- + + + + + + + + + + + + Quinton - Applied systems and software + + + + + + + +
+
+

CS + business / California

Software for the parts of life that do not arrive as clean data.

I am Quinton. I learn by building systems around movement, speech, handwriting, mission data, and everyday routines - then documenting where they work and where they still need proof.

+ +
+
+
+

Selected systems / 2025-2026

Each starts with a messy input and asks the same question: what would make this genuinely useful?

+ +
+

Working prototype

Gesture-Reactive Avatar

A local webcam pipeline that calibrates to a face, smooths expression changes, and maps them to a playful avatar.

  • OpenCV
  • MediaPipe
  • Python
Repository ↗
+

Active prototype

Ambient Dashboard

A calm desktop workspace for priorities and focus sessions - useful state without another noisy task app.

  • React
  • TypeScript
  • Tauri
  • Rust
Repository ↗
+

Working beta

HandwritingConverter

A local pipeline that turns photographed notes into reviewable text, confidence reports, and printable multipage PDFs.

  • OpenCV
  • Tesseract
  • ReportLab
  • pytest
Repository ↗
+

Functional prototype

VoxNav

A Windows voice-control assistant with explicit command parsing, safe text preview, and visible decision tracing.

  • Python
  • Speech
  • PyAutoGUI
  • Tests
Repository ↗
+

See all selected projects and technical notes ->

+
+

Working method / 04 steps

Build the useful version.

  1. 01

    Start with friction

    Find a real workflow, not a feature looking for a reason to exist.

  2. 02

    Map the system

    Make inputs, transformations, constraints, and failure states visible.

  3. 03

    Test the risky part

    Separate deterministic logic from cameras, APIs, speech, and unstable edges.

  4. 04

    Tell the truth

    Call a prototype a prototype. Document what works and what still needs proof.

+

About / Quinton

Technical curiosity, with a business question attached.

I am a computer science student interested in backend and full-stack development, data systems, optimization, geospatial computing, and computer vision.

Business changes how I look at software: who is it for, what does it replace, where does it fail, and is the result useful enough to keep?

Right now: strengthening project validation, building clearer case studies, and learning by shipping.

Field assistant
+

Open channel / 01

Have an interesting problem?

I am always interested in thoughtful projects, useful systems, and learning from people building them.

+
+ + + + From eebed06bc0ea6b4c7e558429c882b3cbdb5ca8ac Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:14 -0700 Subject: [PATCH 16/30] feat(portfolio): complete editorial redesign --- js/main.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/js/main.js b/js/main.js index 7c2e17a..6e49e4d 100644 --- a/js/main.js +++ b/js/main.js @@ -1,4 +1,8 @@ -const year=document.querySelector("#year");if(year)year.textContent=String(new Date().getFullYear()); -const sections=[...document.querySelectorAll("main section[id]")],links=[...document.querySelectorAll("nav a[href^='#']")]; -if("IntersectionObserver" in window&§ions.length&&links.length){const observer=new IntersectionObserver(entries=>{const visible=entries.find(entry=>entry.isIntersecting);if(!visible)return;links.forEach(link=>link.toggleAttribute("aria-current",link.getAttribute("href")===`#${visible.target.id}`))},{rootMargin:"-35% 0px -55%",threshold:0});sections.forEach(section=>observer.observe(section));} +document.querySelectorAll("[data-current-year]").forEach((element) => { + element.textContent = String(new Date().getFullYear()); +}); + +document.querySelectorAll(".mobile-nav a").forEach((link) => { + link.addEventListener("click", () => link.closest("details")?.removeAttribute("open")); +}); From 5fe3847453c0815316c911387f36179fab635e75 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:15 -0700 Subject: [PATCH 17/30] feat(portfolio): complete editorial redesign --- js/projects.js | 264 +++++++++++++++++++------------------------------ 1 file changed, 103 insertions(+), 161 deletions(-) diff --git a/js/projects.js b/js/projects.js index d043c1f..3513529 100644 --- a/js/projects.js +++ b/js/projects.js @@ -1,175 +1,117 @@ -// ======= CONFIG: map tech name -> icon path (auto-fallback included) ======= -const TECH_ICON_BASE = "../assets/icons"; // adjust to your folder (you wrote ./asset/icon) -const TECH_ICON_EXT = "png"; // png/svg etc +const list = document.querySelector("[data-project-list]"); +const loading = document.querySelector("[data-project-loading]"); +const errorState = document.querySelector("[data-project-error]"); -function slugTech(t) { - return String(t || "") - .trim() - .toLowerCase() - .replace(/\+/g, "plus") - .replace(/\#/g, "sharp") - .replace(/[^\w]+/g, "-") - .replace(/^-+|-+$/g, ""); +function safeText(value, fallback) { + return typeof value === "string" && value.trim() ? value.trim() : fallback; } -function techIconPath(techName) { - const slug = slugTech(techName); - return `${TECH_ICON_BASE}/${slug}.${TECH_ICON_EXT}`; -} - -function buildTechIcons(techArr = []) { - const wrap = document.createElement("div"); - wrap.className = "project-tech-icons"; - - for (const t of techArr) { - const chip = document.createElement("span"); - chip.className = "tech-chip"; - - const img = document.createElement("img"); - img.className = "tech-icon"; - img.alt = t; - img.loading = "lazy"; - img.decoding = "async"; - - console.log("TECH:", t, "=>", techIconPath(t)); - - img.src = techIconPath(t); - - // fallback: if missing icon, show text chip - img.onerror = () => { - img.remove(); - chip.textContent = t; - chip.classList.add("tech-chip--text"); - }; - - chip.title = t; - chip.dataset.label = t; - - chip.appendChild(img); - wrap.appendChild(chip); +function validUrl(value) { + if (typeof value !== "string") return null; + try { + const url = new URL(value); + return url.protocol === "https:" ? url.href : null; + } catch { + return null; } - - return wrap; } -// ======= global ======= -let projects; - -// fetch JSON -async function grabData(dataName) { - const requestURL = `../data/${dataName}.json`; - const response = await fetch(requestURL); - return await response.json(); -} - -function populateProject(project) { - const section = document.querySelector(".current-items-section"); - - const card = document.createElement("article"); - card.className = "project-card"; - card.dataset.date = project.date; - - /* ---------------- HEADER ---------------- */ - const header = document.createElement("div"); - header.className = "project-header"; - - const title = document.createElement("h2"); - title.className = "project-title"; - title.textContent = project.title; - - const meta = document.createElement("p"); - meta.className = "project-meta"; - meta.textContent = `${project.status} • ${project.date}`; - - header.append(title, meta); - - /* ---------------- IMAGE ---------------- */ - if (project.image) { - const imgWrap = document.createElement("div"); - imgWrap.className = "project-image-wrap"; - - const img = document.createElement("img"); - img.src = project.image; - img.alt = project.title; - img.className = "project-image"; - img.loading = "lazy"; - img.decoding = "async"; - - imgWrap.appendChild(img); - card.appendChild(imgWrap); +function projectCard(project, index) { + const article = document.createElement("article"); + article.className = "case-card"; + + const visual = document.createElement("div"); + visual.className = "case-visual"; + const initials = document.createElement("span"); + initials.className = "case-initials"; + initials.setAttribute("aria-hidden", "true"); + initials.textContent = safeText(project.title, "Project").split(/\s+/).slice(0, 2).map((word) => word[0]).join("").toUpperCase(); + visual.append(initials); + + if (typeof project.image === "string" && project.image.trim()) { + const image = document.createElement("img"); + image.className = "case-image"; + image.src = project.image; + image.alt = safeText(project.imageAlt, ""); + image.loading = "lazy"; + image.addEventListener("error", () => image.remove(), { once: true }); + visual.append(image); } - /* ---------------- BODY ---------------- */ const body = document.createElement("div"); - body.className = "project-body"; - - // tech icons row (compact) - body.appendChild(buildTechIcons(project.tech || [])); - - const desc = document.createElement("p"); - desc.className = "project-desc"; - desc.textContent = project.description; - - const details = document.createElement("div"); - details.className = "project-details"; - - const problem = document.createElement("p"); - problem.innerHTML = `Problem: ${project.problem}`; - - const approach = document.createElement("p"); - approach.innerHTML = `Approach: ${project.approach}`; - - const result = document.createElement("p"); - result.innerHTML = `Result: ${project.result}`; - - const learned = document.createElement("p"); - learned.innerHTML = `Learned: ${project.learned}`; - - details.append(problem, approach, result, learned); - body.append(desc, details); - - /* ---------------- FOOTER ---------------- */ - const footer = document.createElement("div"); - footer.className = "project-footer"; + body.className = "case-body"; + const top = document.createElement("div"); + top.className = "case-topline"; + const category = document.createElement("span"); + category.className = "eyebrow"; + category.textContent = `${String(index + 1).padStart(2, "0")} / ${safeText(project.category, "Experiment")}`; + const status = document.createElement("span"); + status.className = "status-label"; + status.textContent = safeText(project.status, "Status not documented"); + top.append(category, status); - const link = document.createElement("a"); - link.href = project.link; - link.textContent = "View Project →"; - link.target = "_blank"; - link.rel = "noopener noreferrer"; - link.className = "project-link"; - - footer.appendChild(link); - - /* ---------------- ASSEMBLE ---------------- */ - card.append(header, body, footer); - section.appendChild(card); + const title = document.createElement("h2"); + title.textContent = safeText(project.title, "Untitled project"); + const summary = document.createElement("p"); + summary.className = "case-summary"; + summary.textContent = safeText(project.shortDescription, "Project notes are still being written."); + + const notes = document.createElement("dl"); + notes.className = "case-notes"; + [["Problem", project.problem], ["Approach", project.approach], ["Current limitation", project.limitation]].forEach(([label, value]) => { + const group = document.createElement("div"); + const term = document.createElement("dt"); + term.textContent = label; + const detail = document.createElement("dd"); + detail.textContent = safeText(value, "Not documented yet."); + group.append(term, detail); + notes.append(group); + }); + + const actions = document.createElement("div"); + actions.className = "case-actions"; + const tags = document.createElement("ul"); + tags.className = "tag-list"; + (Array.isArray(project.technologies) ? project.technologies : []).slice(0, 6).forEach((technology) => { + if (typeof technology !== "string" || !technology.trim()) return; + const item = document.createElement("li"); + item.textContent = technology.trim(); + tags.append(item); + }); + actions.append(tags); + const repositoryUrl = validUrl(project.repositoryUrl); + if (repositoryUrl) { + const link = document.createElement("a"); + link.className = "text-link"; + link.href = repositoryUrl; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.textContent = "View repository ↗"; + link.setAttribute("aria-label", `View ${title.textContent} repository on GitHub`); + actions.append(link); + } + body.append(top, title, summary, notes, actions); + article.append(visual, body); + return article; } -// scrollspy -function setupDateScrollSpy() { - const activeDateEl = document.getElementById("activeDate"); - const cards = document.querySelectorAll(".project-card"); - if (!activeDateEl || !cards.length) return; - - activeDateEl.textContent = cards[0].dataset.date || "—"; - - const observer = new IntersectionObserver((entries) => { - const visible = entries - .filter(e => e.isIntersecting) - .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]; - - if (visible) activeDateEl.textContent = visible.target.dataset.date || "—"; - }, { threshold: [0.25, 0.5, 0.75], rootMargin: "-30% 0px -50% 0px" }); - - cards.forEach(card => observer.observe(card)); +async function loadProjects() { + if (!list) return; + try { + const response = await fetch("../data/projects.json", { headers: { Accept: "application/json" } }); + if (!response.ok) throw new Error(`Project data returned ${response.status}`); + const data = await response.json(); + if (!data || !Array.isArray(data.projects) || !data.projects.length) throw new Error("Project data is empty or malformed"); + const fragment = document.createDocumentFragment(); + data.projects.forEach((project, index) => fragment.append(projectCard(project, index))); + list.replaceChildren(fragment); + loading?.remove(); + } catch (error) { + console.error("Unable to load project data", error); + loading?.remove(); + errorState?.removeAttribute("hidden"); + } } -// run after DOM exists -document.addEventListener("DOMContentLoaded", async () => { - const data = await grabData("projects"); - projects = data.projects; - - for (const project of projects) populateProject(project); - setupDateScrollSpy(); -}); +loadProjects(); + From 1bed7718749aee69efe2e9875d87971b6674e9e4 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:17 -0700 Subject: [PATCH 18/30] feat(portfolio): complete editorial redesign --- projects/index.html | 71 +++++++++++---------------------------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/projects/index.html b/projects/index.html index fbab136..f52922f 100644 --- a/projects/index.html +++ b/projects/index.html @@ -1,59 +1,22 @@ - - - - Projects — Quinton - - - - + + + + Projects - Quinton -
- -
- -
-
-
- -
- Top Bar Image -

/PROJECTS

-
- - - - -
-

Current Projects

-

This is my list of projects.

-
-
-
- - - - -
- - -
- -
- - - - - - - - - \ No newline at end of file + + +
+

Project index / Selected work

Systems, tools, and experiments.

These are the projects that best show how I think: start with a real input, make the system visible, and document the parts that still need work.

No live demo is implied. Each link goes to a verified repository.

+
+ +
+ + + + From 40773dfe431f2cc229ccca6a2a2ae02e500c2148 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:18 -0700 Subject: [PATCH 19/30] feat(portfolio): complete editorial redesign --- writings/index.html | 63 +++++---------------------------------------- 1 file changed, 7 insertions(+), 56 deletions(-) diff --git a/writings/index.html b/writings/index.html index b0e350f..ccffeac 100644 --- a/writings/index.html +++ b/writings/index.html @@ -1,61 +1,12 @@ - - - - Writings — Quinton - - - - - + Writing - Quinton - -
- -
- -
-
-
- -
- Top Bar Image -

/WRITINGS

-
- -
-
-

Writings

-

Essays, notes, reflections, and anything else I decide to publish.

-
- -
    -
  • -
    - Writing archive in progress - Essays and technical notes will appear here after review. -
    -
  • -
-
- - -
- -
- -
- - - - - - - + + +

Writing / Public notebook

Notes need something worth saying.

I plan to use this space for technical decisions, project lessons, and the uncomfortable middle between an idea and a working system.

No placeholder essays are presented as published work.


Archive status / Preparing

The writing archive is coming soon.

The current data did not contain a finished, reviewable article. Until one exists, this page stays honest rather than filling the shelf with fake titles.

Read the project field notes
+ + + From cb17b4a0ce5a3f5ed5e4bfafbaa447dbb5237620 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:19 -0700 Subject: [PATCH 20/30] refactor(portfolio): remove unused interactions --- js/contacts.js | 189 ------------------------------------------------- 1 file changed, 189 deletions(-) delete mode 100644 js/contacts.js diff --git a/js/contacts.js b/js/contacts.js deleted file mode 100644 index ed5ee49..0000000 --- a/js/contacts.js +++ /dev/null @@ -1,189 +0,0 @@ -/* ========================================================= - ADHD-CODED JS (tiny but powerful) - - Progress bar - - Character counter - - “Quick chips” that auto-fill message - - Autosave draft (localStorage) - - Haptic feedback on mobile (vibrate) - Works with your existing .contacts + .contact-form markup - ========================================================= */ - -const form = document.querySelector(".contact-form"); -if (!form) throw new Error("Missing .contact-form"); - -const fields = { - name: form.querySelector("#name"), - email: form.querySelector("#email"), - reason: form.querySelector("#reason"), - priority: form.querySelector("#priority"), - timeline: form.querySelector("#timeline"), - links: form.querySelector("#links"), - message: form.querySelector("#message"), -}; -const submitBtn = form.querySelector(".contact-submit"); - -/* ---------- Inject progress + chips + counter (no HTML changes needed) ---------- */ -const progress = document.createElement("div"); -progress.className = "contact-progress"; -progress.innerHTML = ``; -form.prepend(progress); - -const chipRow = document.createElement("div"); -chipRow.className = "chip-row"; -chipRow.innerHTML = ` - - - - - -`; -form.insertBefore(chipRow, form.querySelector(".form-row")); // puts chips near top - -const counterRow = document.createElement("div"); -counterRow.className = "char-row"; -counterRow.innerHTML = ` -
Tip: 2–5 sentences is perfect.
-
0/600
-`; -fields.message.parentElement.appendChild(counterRow); - -const counterNum = counterRow.querySelector(".char-counter b"); -const progressBar = progress.querySelector("span"); - -/* ---------- Progress calculation ---------- */ -const requiredEls = [fields.email, fields.reason, fields.message]; -const allTrackEls = [fields.name, fields.email, fields.reason, fields.priority, fields.timeline, fields.links, fields.message]; - -function clamp(n, a, b){ return Math.max(a, Math.min(b, n)); } - -function updateProgress() { - // required completion counts more - let score = 0; - let total = 0; - - requiredEls.forEach(el => { - total += 2; - if (el && el.value && el.checkValidity()) score += 2; - }); - - allTrackEls.forEach(el => { - if (!el || requiredEls.includes(el)) return; - total += 1; - if (el.value.trim().length > 0) score += 1; - }); - - const pct = clamp(Math.round((score / total) * 100), 0, 100); - progressBar.style.width = pct + "%"; -} - -/* ---------- Character counter ---------- */ -const MAX = 600; -fields.message.setAttribute("maxlength", String(MAX)); - -function updateCounter() { - const len = fields.message.value.length; - counterNum.textContent = String(len); - // gentle warning near limit - if (len > MAX - 80) { - fields.message.style.borderColor = "rgba(255,193,7,0.55)"; - } else { - fields.message.style.borderColor = ""; - } -} - -/* ---------- Field “done” pop + haptics ---------- */ -function popDone(el) { - el.classList.remove("pt-done"); - void el.offsetWidth; // restart animation - el.classList.add("pt-done"); - if (navigator.vibrate) navigator.vibrate(12); -} - -/* ---------- Chips fill message ---------- */ -chipRow.addEventListener("click", (e) => { - const btn = e.target.closest(".chip"); - if (!btn) return; - const add = btn.dataset.fill || ""; - const msg = fields.message; - msg.value = (msg.value ? msg.value + "\n" : "") + add; - msg.focus(); - updateCounter(); - updateProgress(); - popDone(msg); -}); - -/* ---------- Autosave draft ---------- */ -const KEY = "contact_draft_v1"; -function saveDraft() { - const payload = {}; - Object.keys(fields).forEach(k => payload[k] = (fields[k]?.value ?? "")); - localStorage.setItem(KEY, JSON.stringify(payload)); -} -function loadDraft() { - const raw = localStorage.getItem(KEY); - if (!raw) return; - try { - const payload = JSON.parse(raw); - Object.keys(fields).forEach(k => { - if (fields[k] && typeof payload[k] === "string" && payload[k].length) fields[k].value = payload[k]; - }); - } catch {} -} -loadDraft(); - -/* ---------- Wire events ---------- */ -allTrackEls.forEach(el => { - if (!el) return; - el.addEventListener("input", () => { - updateProgress(); - if (el === fields.message) updateCounter(); - saveDraft(); - }); - el.addEventListener("change", () => { - updateProgress(); - saveDraft(); - if (el.value && el.checkValidity?.()) popDone(el); - }); -}); - -/* Message counter init */ -updateCounter(); -updateProgress(); - -/* ---------- Cow tap = dopamine (tiny haptic + micro compliment) ---------- */ -const cow = document.querySelector(".work-in-progress-image"); -if (cow) { - cow.addEventListener("click", () => { - if (navigator.vibrate) navigator.vibrate([10, 20, 10]); - // playful, minimal, not wordy: - const msgs = ["moo.", "cow buff applied.", "message power +10."]; - cow.alt = msgs[Math.floor(Math.random() * msgs.length)]; - setTimeout(() => (cow.alt = "Work in Progress"), 800); - }); -} - -/* ---------- Submit: validate + feedback ---------- */ -form.addEventListener("submit", (e) => { - // front-end validation feedback - const bad = requiredEls.find(el => !el.checkValidity()); - if (bad) { - e.preventDefault(); - form.classList.remove("sent"); - form.classList.add("shake"); - setTimeout(() => form.classList.remove("shake"), 450); - bad.focus(); - if (navigator.vibrate) navigator.vibrate([25, 30, 25]); - return; - } - - // show “sending” state (bucket will handle the actual post) - submitBtn.classList.add("sending"); - if (navigator.vibrate) navigator.vibrate(18); - - // if you use an external bucket that redirects, you won't see this, - // but it still looks nice in cases where it stays on page: - setTimeout(() => { - form.classList.add("sent"); - localStorage.removeItem(KEY); - }, 600); -}); From 9eb53d73f162a49eb2ef0f4ec9e23e8ccbab12ec Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:20 -0700 Subject: [PATCH 21/30] refactor(portfolio): remove unused interactions --- js/cursor.js | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 js/cursor.js diff --git a/js/cursor.js b/js/cursor.js deleted file mode 100644 index 3605913..0000000 --- a/js/cursor.js +++ /dev/null @@ -1,15 +0,0 @@ -const cursor = document.getElementById("cursor"); - -document.addEventListener("mousemove", (e) => { - cursor.style.left = e.clientX + "px"; - cursor.style.top = e.clientY + "px"; -}); - -document.querySelectorAll("a").forEach(el => { - el.addEventListener("mouseenter", () => { - cursor.style.transform = "translate(-50%, -50%) scale(1.2)"; - }); - el.addEventListener("mouseleave", () => { - cursor.style.transform = "translate(-50%, -50%) scale(1)"; - }); -}); \ No newline at end of file From 412f799dca35fc2a49c8458c0ec073326a28c4b0 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:21 -0700 Subject: [PATCH 22/30] refactor(portfolio): remove unused interactions --- js/hero.js | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 js/hero.js diff --git a/js/hero.js b/js/hero.js deleted file mode 100644 index 5c79e72..0000000 --- a/js/hero.js +++ /dev/null @@ -1,15 +0,0 @@ -const hero = document.querySelector(".top-bar-section"); - -hero.addEventListener("mousemove", (e) => { - const r = hero.getBoundingClientRect(); - const x = (e.clientX - r.left) / r.width - 0.5; - const y = (e.clientY - r.top) / r.height - 0.5; - - hero.style.setProperty("--px", (x * 14).toFixed(2) + "px"); - hero.style.setProperty("--py", (y * 10).toFixed(2) + "px"); -}); - -hero.addEventListener("mouseleave", () => { - hero.style.setProperty("--px", "0px"); - hero.style.setProperty("--py", "0px"); -}); From 748b76b61d4f1db4a720cc5399acd53ab58e673b Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:27 -0700 Subject: [PATCH 23/30] refactor(portfolio): remove unused interactions --- js/rope.js | 82 ------------------------------------------------------ 1 file changed, 82 deletions(-) delete mode 100644 js/rope.js diff --git a/js/rope.js b/js/rope.js deleted file mode 100644 index d99a7bc..0000000 --- a/js/rope.js +++ /dev/null @@ -1,82 +0,0 @@ -// ===== Rope Pull → Transition → Navigate ===== -const rope = document.getElementById("rope"); - -// --- Config --- -const CFG = { - pullThreshold: 95, // trigger when pulled past this - follow: 0.18, // smoothing while dragging - ret: 0.10, // smoothing while returning - swayAmp: 2.2, // degrees - swaySpeed: 0.018, // sway frequency - maxDrag: 220, // max visual pull distance (clamp) - navigateTo: "../index.html", - navigateAt: 0.5 // 0.5 = at full cover (t/2 in your transition) -}; - -// --- State --- -const S = { - dragging: false, - transitioned: false, - startY: 0, - targetY: 0, - currentY: 0, - t: 0, - rotate: 0 -}; - -// --- Helpers --- -const clamp = (v, min, max) => Math.min(max, Math.max(min, v)); - -function setDragging(on, e) { - S.dragging = on; - if (on && e) S.startY = e.clientY - S.currentY; // preserves continuity -} - -function computeTargetY(e) { - const raw = e.clientY - S.startY; - S.targetY = clamp(raw, 0, CFG.maxDrag); -} - -function updateSway() { - S.t += 1; - S.rotate = S.dragging ? 0 : Math.sin(S.t * CFG.swaySpeed) * CFG.swayAmp; -} - -function updatePosition() { - const goal = S.dragging ? S.targetY : 0; - const k = S.dragging ? CFG.follow : CFG.ret; - S.currentY += (goal - S.currentY) * k; -} - -function render() { - rope.style.transform = `translate(-50%, ${S.currentY-100}px) rotate(${S.rotate}deg)`; -} - -function shouldTriggerTransition() { - return !S.transitioned && S.targetY >= CFG.pullThreshold; -} - -function runTransitionAndNavigate() { - S.transitioned = true; - const dur = barsZoomTransition(); // must exist globally - setTimeout(() => window.location.href = CFG.navigateTo, dur * CFG.navigateAt); -} - -// --- Events --- -rope.addEventListener("mousedown", (e) => setDragging(true, e)); - -document.addEventListener("mousemove", (e) => { - if (!S.dragging) return; - computeTargetY(e); - if (shouldTriggerTransition()) runTransitionAndNavigate(); -}); - -document.addEventListener("mouseup", () => setDragging(false)); - -// --- Animation Loop --- -(function tick() { - updateSway(); - updatePosition(); - render(); - requestAnimationFrame(tick); -})(); From 8992eb25f0232217c934f3831e773a7ea35da0e4 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:28 -0700 Subject: [PATCH 24/30] refactor(portfolio): remove unused interactions --- js/titleAnimation.js | 175 ------------------------------------------- 1 file changed, 175 deletions(-) delete mode 100644 js/titleAnimation.js diff --git a/js/titleAnimation.js b/js/titleAnimation.js deleted file mode 100644 index df029fa..0000000 --- a/js/titleAnimation.js +++ /dev/null @@ -1,175 +0,0 @@ -// Enhanced /QUINTON title: crisp slash blink + outline/drop-shadow + multi-animations -// Works with:

/QUINTON

- -const title = document.querySelector(".page-title"); -if (!title) throw new Error("Missing .page-title"); - -const raw = title.textContent.trim(); -const hasSlash = raw.startsWith("/"); -const word = hasSlash ? raw.slice(1) : raw; - -// Build spans -title.innerHTML = - (hasSlash ? `/` : "") + - word.split("").map((ch, i) => `${ch}`).join(""); - -// Inject CSS (self-contained) -const style = document.createElement("style"); -style.textContent = ` - .page-title{ - display:inline-flex; - align-items:baseline; - gap:0.06em; - user-select:none; - position:relative; - } - - /* Base readability: outline + shadow so blink is visible on clouds */ - .page-title .pt-slash, - .page-title .pt-char{ - display:inline-block; - -webkit-text-stroke: 2px rgba(10, 18, 40, 0.18); - text-shadow: - 0 2px 0 rgba(10,18,40,0.28), - 0 10px 24px rgba(0,0,0,0.22); - transition: - color 220ms ease, - transform 220ms ease, - text-shadow 220ms ease, - filter 220ms ease, - opacity 180ms ease; - will-change: transform, color, text-shadow, filter, opacity; - } - - .page-title .pt-slash{ - margin-right:0.02em; - filter: drop-shadow(0 0 10px rgba(255,255,255,0.22)); - } - - /* Ambient “sparkle” for the whole word (very subtle) */ - @keyframes titleShimmer { - 0% { filter: drop-shadow(0 0 0 rgba(255,255,255,0)); } - 50% { filter: drop-shadow(0 0 14px rgba(255,255,255,0.18)); } - 100% { filter: drop-shadow(0 0 0 rgba(255,255,255,0)); } - } - .page-title{ animation: titleShimmer 3.2s ease-in-out infinite; } - - /* “Attention” bounce on hover */ - @keyframes microBounce{ - 0% { transform: translateY(0); } - 35%{ transform: translateY(-2px); } - 70%{ transform: translateY(0); } - 100%{ transform: translateY(0); } - } - .page-title:hover .pt-char, - .page-title:hover .pt-slash{ - animation: microBounce 700ms ease-out 1; - } - - /* Reduce motion */ - @media (prefers-reduced-motion: reduce){ - .page-title{ animation:none; } - .page-title:hover .pt-char, - .page-title:hover .pt-slash{ animation:none; } - } -`; -document.head.appendChild(style); - -const slashEl = title.querySelector(".pt-slash"); -const chars = [...title.querySelectorAll(".pt-char")]; - -// ---------- Slash blink: visible + “terminal glow” ---------- -if (slashEl) { - let on = true; - setInterval(() => { - on = !on; - slashEl.style.opacity = on ? "1" : "0.08"; // not fully gone so you still see the outline - slashEl.style.textShadow = on - ? "0 2px 0 rgba(10,18,40,0.28), 0 10px 24px rgba(0,0,0,0.22), 0 0 18px rgba(255,255,255,0.34)" - : "0 2px 0 rgba(10,18,40,0.32), 0 10px 24px rgba(0,0,0,0.22), 0 0 8px rgba(255,255,255,0.12)"; - }, 520); - - // Tiny “spark” tick to make it feel alive - let spark = false; - setInterval(() => { - spark = !spark; - slashEl.style.filter = spark - ? "drop-shadow(0 0 14px rgba(255,255,255,0.28))" - : "drop-shadow(0 0 10px rgba(255,255,255,0.18))"; - }, 1100); -} - -// ---------- Per-character wave: highlight travels across letters ---------- -const base = { r: 170, g: 178, b: 190 }; // smoky gray-blue -const hi = { r: 255, g: 255, b: 255 }; // bright highlight -const accent = { r: 140, g: 190, b: 255 }; // soft sky accent - -function mix(a, b, amt) { - const r = Math.round(a.r + (b.r - a.r) * amt); - const g = Math.round(a.g + (b.g - a.g) * amt); - const bb = Math.round(a.b + (b.b - a.b) * amt); - return `rgb(${r},${g},${bb})`; -} - -function falloff(dist) { - return Math.max(0, Math.exp(-dist * 0.85)); // smooth decay -} - -let t = 0; -let hoverSlow = false; - -title.addEventListener("mouseenter", () => (hoverSlow = true)); -title.addEventListener("mouseleave", () => (hoverSlow = false)); - -function animate() { - const n = chars.length; - if (!n) return; - - // focus slides across; on hover it slows down for readability - const speed = hoverSlow ? 0.030 : 0.060; - const focus = (t * speed) % n; - - chars.forEach((el, i) => { - const d = Math.abs(i - focus); - const f = falloff(d); - - // color blend: base -> accent -> white as it gets closer - const mid = mix(base, accent, Math.min(1, f * 0.85)); - const col = mix({ r: parseInt(mid.slice(4)), g: 0, b: 0 }, hi, 0); // placeholder to avoid parsing - // Use two-step without parsing: just compute directly - const color = f > 0.55 ? mix(accent, hi, (f - 0.55) / 0.45) : mix(base, accent, f / 0.55); - - el.style.color = color; - - // lift and “bounce” near the focus - const lift = (f > 0.75) ? -3 : (f > 0.45 ? -1.5 : 0); - el.style.transform = `translateY(${lift}px)`; - - // glow near focus + stronger outline for legibility - el.style.textShadow = (f > 0.55) - ? `0 2px 0 rgba(10,18,40,0.30), - 0 10px 24px rgba(0,0,0,0.24), - 0 0 ${Math.round(18 * f)}px rgba(255,255,255,${0.26 * f})` - : `0 2px 0 rgba(10,18,40,0.28), - 0 10px 24px rgba(0,0,0,0.22)`; - }); - - // occasional “sparkle ping” that hits a random letter (subtle) - if (t % 240 === 0 && n > 2) { - const j = Math.floor(Math.random() * n); - const el = chars[j]; - el.style.filter = "drop-shadow(0 0 14px rgba(255,255,255,0.22))"; - setTimeout(() => (el.style.filter = "none"), 220); - } - - t += 1; - requestAnimationFrame(animate); -} -requestAnimationFrame(animate); - -// ---------- Optional: pause work when tab hidden (saves CPU) ---------- -document.addEventListener("visibilitychange", () => { - if (document.hidden) { - chars.forEach(el => { el.style.transform = "translateY(0)"; el.style.filter = "none"; }); - } -}); From 59905e8b7b86edac4315c35e4d0a0eb62f753633 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:30 -0700 Subject: [PATCH 25/30] refactor(portfolio): remove unused interactions --- js/transition.js | 96 ------------------------------------------------ 1 file changed, 96 deletions(-) delete mode 100644 js/transition.js diff --git a/js/transition.js b/js/transition.js deleted file mode 100644 index fb2c6de..0000000 --- a/js/transition.js +++ /dev/null @@ -1,96 +0,0 @@ -// Fix: navigate AFTER the cover animation finishes (i.e., when all bars are fully scaleX(1)) - -function barsZoomTransition({ - bars = 8, - inMs = 600, - coverHoldMs = 400, - outMs = 600, - hideAfterMs = 80 -} = {}) { - const wipe = document.getElementById("wipe"); - if (!wipe) throw new Error("Missing #wipe element"); - - buildBars(wipe, bars); - const barEls = [...wipe.children]; - - wipe.style.opacity = "1"; - - const stIn = inMs / (bars + 6); - const stOut = outMs / (bars + 6); - - // cover - barEls.forEach((b, i) => { - b.animate( - [{ transform: "scaleX(0)" }, { transform: "scaleX(1)" }], - { duration: inMs, delay: i * stIn, easing: "cubic-bezier(.2,.9,.2,1)", fill: "forwards" } - ); - }); - - // IMPORTANT: cover is only guaranteed complete after the last bar finishes - const coverDoneAt = inMs + (bars - 1) * stIn; - - // reveal - const startRevealAt = coverDoneAt + coverHoldMs; - barEls.forEach((b, i) => { - b.animate( - [{ transform: "scaleX(1)" }, { transform: "scaleX(0)" }], - { duration: outMs, delay: startRevealAt + i * stOut, easing: "cubic-bezier(.4,0,.2,1)", fill: "forwards" } - ); - }); - - const total = startRevealAt + outMs + (bars - 1) * stOut + hideAfterMs; - setTimeout(() => (wipe.style.opacity = "0"), total); - - return { total, coverDoneAt, startRevealAt }; -} - -function buildBars(wipe, bars) { - wipe.style.gridTemplateRows = `repeat(${bars}, 1fr)`; - wipe.innerHTML = ""; - for (let i = 0; i < bars; i++) { - const bar = document.createElement("div"); - bar.className = "bar"; - bar.style.transformOrigin = i % 2 === 0 ? "left center" : "right center"; - wipe.appendChild(bar); - } -} - -// Navigate only when fully covered (no chance to show the next page early): -// const tr = barsZoomTransition({ bars: 8, coverHoldMs: 300 }); -// setTimeout(() => (window.location.href = "../index.html"), tr.coverDoneAt + 50); - - - -// REVERSE ANIMATIONS (for load ins) -function playReverseTransition({ bars = 10, outMs = 800 } = {}) { - const wipe = document.getElementById("wipe"); - wipe.style.opacity = "0"; // keep hidden while building - wipe.style.gridTemplateRows = `repeat(${bars}, 1fr)`; - wipe.innerHTML = ""; - - for (let i = 0; i < bars; i++) { - const bar = document.createElement("div"); - bar.className = "bar"; - bar.style.transformOrigin = i % 2 === 0 ? "left center" : "right center"; - bar.style.transform = "scaleX(1)"; // start covered for reverse - wipe.appendChild(bar); - } - - // show only after bars exist - wipe.style.opacity = "1"; - - const barEls = [...wipe.children]; - const stagger = outMs / (bars + 6); - - barEls.forEach((b, i) => { - b.animate( - [{ transform: "scaleX(1)" }, { transform: "scaleX(0)" }], - { duration: outMs, delay: i * stagger, easing: "cubic-bezier(.4,0,.2,1)", fill: "forwards" } - ); - }); - - const total = outMs + (bars - 1) * stagger + 50; - setTimeout(() => (wipe.style.opacity = "0"), total); -} - -window.addEventListener("load", () => playReverseTransition()); \ No newline at end of file From f52c5e0a7bf4888b766241accafc22ac0e6ed6c0 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:31 -0700 Subject: [PATCH 26/30] refactor(portfolio): remove unused interactions --- testing.html | 72 ---------------------------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 testing.html diff --git a/testing.html b/testing.html deleted file mode 100644 index 47dc624..0000000 --- a/testing.html +++ /dev/null @@ -1,72 +0,0 @@ - -
- - - - From b09c859c09afa97e0f45c64b2dbc266251f347a3 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:45 -0700 Subject: [PATCH 27/30] test(portfolio): add static validation --- .github/workflows/static-checks.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/static-checks.yml diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml new file mode 100644 index 0000000..04ab3f3 --- /dev/null +++ b/.github/workflows/static-checks.yml @@ -0,0 +1,21 @@ +name: Static checks + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Validate pages, assets, links, and JSON + run: python scripts/validate_site.py + From 9bd099a8f79400c827f8ef6efc5b1ec6030674cd Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:51 -0700 Subject: [PATCH 28/30] docs(portfolio): add crawl metadata --- robots.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 robots.txt diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..191ed48 --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / + +Sitemap: https://simplecaci.github.io/personal-portfolio/sitemap.xml + From 85e370eab45df7d692cc34e807c3aaef1694793a Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:28:56 -0700 Subject: [PATCH 29/30] test(portfolio): add static validation --- scripts/validate_site.py | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 scripts/validate_site.py diff --git a/scripts/validate_site.py b/scripts/validate_site.py new file mode 100644 index 0000000..c770c5f --- /dev/null +++ b/scripts/validate_site.py @@ -0,0 +1,135 @@ +"""Lightweight, dependency-free checks for the static portfolio.""" + +from __future__ import annotations + +import json +import re +import sys +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import urlparse + +ROOT = Path(__file__).resolve().parents[1] +HTML_FILES = [ROOT / "index.html", ROOT / "projects/index.html", ROOT / "writings/index.html", ROOT / "contacts/index.html"] +REQUIRED_PROJECT_FIELDS = {"id", "title", "shortDescription", "category", "technologies", "status", "repositoryUrl", "year", "problem", "approach", "limitation"} +BAD_TEXT = ("example.com", "This is a brief", "placeholder copy", "â€", "â†", "·") + + +class PageParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.ids: set[str] = set() + self.links: list[dict[str, str | None]] = [] + self.images: list[dict[str, str | None]] = [] + self.titles = 0 + self.main_count = 0 + self.h1_count = 0 + self.errors: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + values = dict(attrs) + if "id" in values and values["id"]: + if values["id"] in self.ids: + self.errors.append(f"duplicate id #{values['id']}") + self.ids.add(values["id"]) + if tag == "a": + self.links.append(values) + elif tag == "img": + self.images.append(values) + elif tag == "title": + self.titles += 1 + elif tag == "main": + self.main_count += 1 + elif tag == "h1": + self.h1_count += 1 + for name in values: + if name.lower().startswith("on"): + self.errors.append(f"inline event handler {name}") + + +def check_page(path: Path) -> list[str]: + source = path.read_text(encoding="utf-8") + parser = PageParser() + parser.feed(source) + errors = list(parser.errors) + relative = path.relative_to(ROOT) + if parser.titles != 1: + errors.append(f"expected one title element, found {parser.titles}") + if parser.main_count != 1: + errors.append(f"expected one main landmark, found {parser.main_count}") + if parser.h1_count != 1: + errors.append(f"expected one h1, found {parser.h1_count}") + if not re.search(r' list[str]: + errors: list[str] = [] + projects = json.loads((ROOT / "data/projects.json").read_text(encoding="utf-8")) + entries = projects.get("projects") + if projects.get("schemaVersion") != 2 or not isinstance(entries, list) or not entries: + return ["data/projects.json: expected schemaVersion 2 and a non-empty projects array"] + ids: set[str] = set() + for index, project in enumerate(entries): + if not isinstance(project, dict): + errors.append(f"data/projects.json: project {index} is not an object") + continue + missing = REQUIRED_PROJECT_FIELDS - project.keys() + if missing: + errors.append(f"data/projects.json: project {index} missing {sorted(missing)}") + project_id = project.get("id") + if project_id in ids: + errors.append(f"data/projects.json: duplicate id {project_id}") + ids.add(project_id) + if not isinstance(project.get("technologies"), list): + errors.append(f"data/projects.json: {project_id} technologies must be an array") + repo = project.get("repositoryUrl", "") + if not isinstance(repo, str) or not repo.startswith("https://github.com/SimpleCaci/"): + errors.append(f"data/projects.json: {project_id} has an unexpected repository URL") + json.loads((ROOT / "data/profile.json").read_text(encoding="utf-8")) + json.loads((ROOT / "data/writings.json").read_text(encoding="utf-8")) + return errors + + +def main() -> int: + errors = [error for page in HTML_FILES for error in check_page(page)] + errors.extend(check_data()) + if errors: + print("Static validation failed:") + for error in errors: + print(f"- {error}") + return 1 + print(f"Static validation passed for {len(HTML_FILES)} pages and project data.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) + From 125fcf8034bab4fdd36a58bf98e3bbbf3728e7f8 Mon Sep 17 00:00:00 2001 From: SimpleCaci <92838396+SimpleCaci@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:29:04 -0700 Subject: [PATCH 30/30] docs(portfolio): add crawl metadata --- sitemap.xml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..92b1362 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,8 @@ + + + https://simplecaci.github.io/personal-portfolio/ + https://simplecaci.github.io/personal-portfolio/projects/ + https://simplecaci.github.io/personal-portfolio/writings/ + https://simplecaci.github.io/personal-portfolio/contacts/ + +