diff --git a/src/app.py b/src/app.py index 07d3d57..4493be2 100755 --- a/src/app.py +++ b/src/app.py @@ -1,6 +1,7 @@ import hashlib import json import os +import re import flask from flask_minify import Minify @@ -71,13 +72,26 @@ projects = load_json("./static/json/projects.json") books = load_json("./static/json/books.json") skills = load_json("./static/json/skills.json") pages = load_json("./static/json/pages.json") +features = load_json("./static/json/features.json") pages['about']['skillList'] = skills pages['projects']['projects'] = projects +pages['projects']['features'] = features +pages['home']['features'] = features pages['home']['books'] = books +pages['home']['projects'] = projects pages['books']['books'] = books pages['status']['services'] = SERVICES +for _name, _page in pages.items(): + _page['id'] = _name + + +@app.template_filter('slug') +def slug(value): + value = re.sub(r'[^a-z0-9]+', '-', str(value).lower()) + return value.strip('-') + # ── Error rendering ────────────────────────────────────────────────── diff --git a/src/static/css/App.css b/src/static/css/App.css index 37eb818..e27e01c 100755 --- a/src/static/css/App.css +++ b/src/static/css/App.css @@ -1,590 +1,962 @@ +/* ------------------------------------------------------------------ + Fonts + ------------------------------------------------------------------ */ @font-face { - font-family: "neon-future"; - src: url("../fonts/NeonFuture.ttf"); + font-family: "Roboto Condensed"; + src: url("../fonts/RobotoCondensed-Regular.ttf") format("truetype"); + font-weight: 400; font-display: swap; } - @font-face { font-family: "shuttlex"; - src: url("../fonts/SHUTTLE-X.ttf"); + src: url("../fonts/SHUTTLE-X.ttf") format("truetype"); font-display: swap; } @font-face { - font-family: "sunset-club"; - src: url("../fonts/SunsetClub.otf"); + font-family: "neon-future"; + src: url("../fonts/NeonFuture.ttf") format("truetype"); font-display: swap; } - @font-face { - font-family: "robotoreg"; - src: url("../fonts/RobotoCondensed-Regular.ttf"); + font-family: "Chess New"; + src: url("../fonts/chessglyph-new.0cc8115c.woff2") format("woff2"); font-display: swap; } +/* ------------------------------------------------------------------ + Tokens + ------------------------------------------------------------------ */ :root { - /* Backgrounds */ - --bg-base: #1a1a1a; - --bg-card-rgb: 24, 24, 24; + --bg: #141414; + --surface: rgba(20, 20, 20, 0.9); + --surface-solid: #141414; + --well: #1c1c1c; - /* Accent (maroon/crimson) */ - --accent: rgb(139, 36, 36); - --accent-rgb: 139, 36, 36; + --ink: #e9e6df; + --ink-2: #b9b5ac; + --ink-3: #85817a; - /* Text */ - --text-heading: #ecebeb; - --text-body: rgb(212, 212, 212); - --text-secondary: #a8a8a8; - --text-link: #a0a0a0a0; + --rule: rgba(233, 230, 223, 0.14); + --rule-strong: rgba(233, 230, 223, 0.32); - /* Status */ - --status-online: #4caf50; - --status-degraded: #ffc107; - --status-offline: #f44336; - --status-good: #8bc34a; + --accent: #b23c3c; + --accent-bright: #d9645c; + --accent-rgb: 178, 60, 60; + + --ok: #62a866; + --warn: #d4a53c; + --bad: #d35047; + + --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-data: "Roboto Condensed", "Arial Narrow", "Helvetica Neue", Arial, sans-serif; + --font-display: "shuttlex", var(--font-body); + + --measure: 62ch; + --page-width: 66rem; + --gutter: clamp(1.25rem, 4vw, 3rem); - /* Effects */ --neon-blue: #5271ff; - - /* Borders */ - --border-subtle: rgba(168, 168, 168, 0.2); } -html, body { +/* ------------------------------------------------------------------ + Base + ------------------------------------------------------------------ */ +html { scroll-behavior: smooth; -} - -/* Enhanced link styling throughout site */ -a { - transition: color 0.2s ease, opacity 0.2s ease; -} - -a:hover { - opacity: 0.8; + -webkit-text-size-adjust: 100%; } body { margin: 0; - background-attachment: fixed; - background-size: 100% auto; - background-clip: border-box; - background-color: var(--bg-base); + background-color: var(--bg); + color: var(--ink-2); + font-family: var(--font-body); + font-size: 1.0625rem; + line-height: 1.55; } -* { - font-family: "Roboto Condensed", sans-serif; -} - -#fullIframe { - width:100vw; - height: 100vh; -} - -#menu { - height: 2rem; - display: none; - filter: brightness(0) saturate(100%) invert(72%) sepia(0%) saturate(1%) hue-rotate(151deg) brightness(95%) contrast(86%); - float: right; - margin-right: 1rem; - margin-top: .25rem; - cursor: pointer; -} - -/* #defaultCanvas0 { +#bg { position: fixed; - top: 0; - left: 0; - position: fixed; - z-index: -69; -} */ - -#map { - height: 100%; - width: 100vw; - position: fixed; - top: 0; - left: 0; - z-index: -69; + inset: 0; + z-index: 0; + pointer-events: none; } -.leaflet-layer, -.leaflet-control-zoom-in, -.leaflet-control-zoom-out, -.leaflet-control-attribution { - filter: brightness(500%) contrast(130%); +#bg canvas { + display: block; } -.leaflet-tile-pane { - filter: brightness(50%); +.site { + position: relative; + z-index: 1; + min-height: 100vh; + display: flex; + flex-direction: column; + background-color: var(--surface); } -h1 { - color: var(--text-heading); +main { + flex: 1 0 auto; } -h2, h3, h4, h5 { - color: var(--text-secondary); +img, +svg { + max-width: 100%; + height: auto; } -h5 { - margin: 0.3em 0 0 0; - font-size: 1rem; - font-weight: normal; -} - -h2 p, h3 p { - margin: 0 -} - -strong { - color: var(--text-heading); -} - -ul { +h1, h2, h3, h4, h5, h6 { + color: var(--ink); + font-weight: 600; + line-height: 1.2; margin: 0; - padding-left: 1.5em; - color: var(--text-secondary); } -li { - margin-bottom: 0.5em; - color: var(--text-secondary); +p { + margin: 0 0 1em; } -p, li, span { - color: var(--text-body); - font-size: 1rem; +p:last-child { + margin-bottom: 0; } -strong { - color: var(--text-heading); +strong, +b { + color: var(--ink); + font-weight: 600; } -span { - font-size: .8rem; +ul, +ol { + margin: 0 0 1em; + padding-left: 1.25em; } -a, a p { - color: var(--text-link); - text-decoration: none; +li + li { + margin-top: 0.25em; +} + +a { + color: var(--ink); + text-decoration: underline; + text-decoration-color: rgba(var(--accent-rgb), 0.7); + text-decoration-thickness: 1px; + text-underline-offset: 0.18em; + transition: color 0.15s ease, text-decoration-color 0.15s ease; +} + +a:hover { + color: var(--accent-bright); + text-decoration-color: currentColor; +} + +a:focus-visible, +button:focus-visible, +[tabindex]:focus-visible { + outline: 2px solid var(--accent-bright); + outline-offset: 2px; +} + +hr { + border: 0; + border-top: 1px solid var(--rule); + margin: 2rem 0; +} + +code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.9em; + color: var(--ink); } table { - color: lightgray; border-collapse: collapse; -} - -th, td { - border-right: dotted 1px lightgray; - padding: 0 1em; -} - -tr { - border: solid 1px lightgray; -} - -.transparent { - opacity: 0 !important; -} - -.unsetPos { - position: unset !important; -} - -.growBottom { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.growBottom > :last-child { - flex-grow: 1; -} - -.header h1 { - display: inline-block; - transition: .4s; - margin-bottom: 0px; - margin-top: .5rem; - margin-left: 1rem; - font-size: 2rem; -} - -.header { - margin-bottom: 0px; - margin-left: 0rem; - background-color: var(--bg-base); - z-index: 99; - position: sticky; - top: 0; - left: 0; - transition: .4s; - border-bottom: solid .2em rgba(var(--accent-rgb), 0.5); - overflow: hidden; -} - -#homeIcon { - margin: auto; - display: block; - /* width: 5em; */ - width: 8em; - /* height: 30em; */ - /* max-height: 70vh; */ - max-width: 70vw; -} - -#homeName { - margin-top: .25em; -} - -#name-container { - display: inline-block; - cursor: pointer; - max-width: calc(100% - 3em); - margin: 0.2em; -} - -.pointer { - cursor: pointer; -} - -.name { - display: inline; - color: #bbbaba; - background: url("../icons/neonfinal3.svg") 0/5rem no-repeat; - background-size: contain; - padding-left: min(10vw, 3.5rem); - font-size: xx-large; -} - -.glitch { - display: inline; -} - -.textGrad { - font-family: "shuttlex" !important; -} - -.navControl { - max-height: 57px; - float: right; - margin-right: 0; - transition: .4s; -} - -.navBar { - display: flex; - padding-left: 0px; - margin: 0px; - gap: 0; -} - -.navElement + .navElement { - border-left: 1px solid rgba(var(--accent-rgb), 0.4); -} - -.navElement { - display: inline-block; - text-align: center; - background: transparent; - padding: 0 1.4em; - border-bottom: 2px solid transparent; - cursor: pointer; - position: relative; - transition: border-color 0.25s ease, background-color 0.25s ease; -} - -.navElement:hover { - border-bottom-color: var(--accent); - background-color: rgba(var(--accent-rgb), 0.08); -} - -.navElement * { - color: var(--text-secondary); - margin: 0px; - display: block; - padding: .7rem 0; - transition: color 0.25s ease; - font-size: 0.95rem; - letter-spacing: 0.04em; - text-transform: uppercase; -} - -.navElement:hover * { - color: var(--text-heading); -} - -#home { - /* height: 100vh; */ - overflow-y: hidden; -} - -.selfcenter { - align-self: center; -} - -.langstats { - width: min(100%, 350px); - /* height: 165px; */ - border: none; - display: block; -} - -.diamonds { - background: black; - background-image: linear-gradient(rgba(0, 0, 0, .5), rgba(0, 0, 0, .75)), url('../chesscom-embed/diamonds.png'); -} - -.concentratedHead { - display: inline-block; - padding-right: 4rem; - border-bottom: #0f0f0f solid 5px; - color: white; - position: relative; - padding-bottom: 0.5em; - margin-bottom: 1em; -} - -.foreground { - position: fixed; - top: 0; - left: 0; - min-width: 200vw; - min-height: 200vh; - overflow: hidden; - background-color: rgba(var(--bg-card-rgb), 0.85); - z-index: -1; -} - -.foregroundContent { - padding-left: 8rem; - padding-right: 2em; - padding-bottom: 2em; - padding-top: 2em; -} - -.foregroundContent p { - line-height: 1.5em; -} - -.concentratedHead::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 60px; - height: 3px; - background: linear-gradient(90deg, rgba(var(--accent-rgb), 0.8) 0%, transparent 100%); -} - -.homeground { - background-color: rgb(0, 0, 0, .6); -} - -.homegroundContent { - padding-top: 1em; - padding-left: 0em; -} - -.noBackground { - background: none; -} - -.relative { - position: relative; -} - -.noPad { - padding-left: 0px !important; - padding-right: 0px !important; -} - -.centerContent { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100vh; -} - -.centerContent>div { - margin-top: 30vh; - margin-left: auto; - margin-right: auto; - text-align: center; -} - -#HomeContent { - display: inline-block; - flex-grow: 1; - min-width: 65em; -} - -#nametag { - padding: 2em 3em; - background-color: rgb(44, 44, 44, .75); - text-align: center; -} - -#nametag h2{ - font-size: medium; -} - -#nametag p { - text-shadow: none; - color: var(--text-secondary); - margin: 0; -} - -#nametagContainer { - display: flex; width: 100%; - position: absolute; - left: 0; - justify-content: center; - z-index: -1; + font-family: var(--font-data); } -.nomargin { - margin: 0; +th, +td { + text-align: left; + padding: 0.6em 0.75em; + border-bottom: 1px solid var(--rule); + vertical-align: top; +} + +th { + color: var(--ink-3); + font-weight: 400; + font-size: 0.85rem; + letter-spacing: 0.02em; +} + +thead th { + border-bottom-color: var(--rule-strong); +} + +.label { + font-family: var(--font-data); + color: var(--ink-3); + font-size: 0.85em; } .flex { display: flex; - justify-content: space-around; } -.equalitems { - column-gap: 1em; +button, +.button { + font: inherit; + font-family: var(--font-data); + font-size: 0.95rem; + color: var(--ink); + background: transparent; + border: 1px solid var(--rule-strong); + padding: 0.35em 0.9em; + cursor: pointer; + border-radius: 2px; + transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease; } -.equalitems > * { - flex: 1 1 0; +button:hover, +.button:hover { + border-color: var(--accent-bright); + color: var(--accent-bright); } -.vFlex { - display: flex; - flex-direction: column; - height: 100%; +button:disabled { + opacity: 0.5; + cursor: default; } -.wrap { - flex-wrap: wrap; - max-width: 100%; -} - -#aboutCards > * { - margin: .25em; -} - -.spaceBetween { - justify-content: space-between; -} - -.start { - justify-content: left; -} - -.hSpacer { - width: 2em; - height: 0; -} - -.onRight { - overflow: scroll; - margin: auto; -} - -.onRight>* { - margin: auto; - margin-bottom: 5px; -} - -.bgi { - background-size: contain; - background-repeat: no-repeat; - background-position-y: center; - height: 100%; - width: 100%; - position: absolute; - z-index: 0; -} - -#Vertical { - margin-left: auto; - margin-right: auto; - width: 0; - height: 3rem; - border-right: var(--accent) solid 1px; -} - -.sideimg { - float: right; +/* ------------------------------------------------------------------ + Header + ------------------------------------------------------------------ */ +.site-header { + position: sticky; top: 0; - right: 0; + z-index: 50; + display: flex; + align-items: stretch; + justify-content: space-between; + gap: 1rem; + padding: 0 var(--gutter); + height: 3.5rem; + background-color: var(--surface-solid); + border-bottom: 1px solid var(--rule); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 0.75rem; + text-decoration: none; + color: var(--ink); + white-space: nowrap; + min-width: 0; +} + +.brand:hover { + color: var(--ink); +} + +.brand-mark { + width: 1.875rem; + height: 1.875rem; + flex-shrink: 0; +} + +.brand-name { + font-family: var(--font-display); + font-size: 1.2rem; + letter-spacing: 0.04em; + line-height: 1; + padding-top: 0.15em; +} + +.brand-tagline { + font-family: var(--font-data); + font-size: 0.85rem; + color: var(--ink-3); + padding-left: 0.75rem; + border-left: 1px solid var(--rule-strong); + line-height: 1.2; +} + +.brand:hover .brand-tagline { + color: var(--accent-bright); +} + +.site-nav { + display: flex; + align-items: stretch; + gap: 0; +} + +.site-nav a { + display: inline-flex; + align-items: center; + font-family: var(--font-data); + font-size: 1rem; + color: var(--ink-2); + text-decoration: none; + padding: 0 1rem; + margin-bottom: -1px; /* sit on the header rule */ + border-bottom: 2px solid transparent; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.site-nav a:last-child { + padding-right: 0; +} + +.site-nav a:hover { + color: var(--ink); +} + +.site-nav a[aria-current="page"] { + color: var(--ink); + border-bottom-color: var(--accent-bright); +} + +.menu-toggle { + display: none; + align-self: center; +} + +/* ------------------------------------------------------------------ + Page structure + ------------------------------------------------------------------ */ +.page { + width: min(100% - 2 * var(--gutter), var(--page-width)); + margin: 0 auto; + padding: 2.5rem 0 4rem; +} + +.page-head { + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--rule-strong); + margin-bottom: 0.5rem; +} + +.display { + font-family: var(--font-display); + font-weight: 400; + font-size: clamp(2rem, 4.5vw, 3rem); + letter-spacing: 0.01em; + color: var(--ink); + margin: 0 0 0.75rem; +} + +.lede { + max-width: var(--measure); + font-size: 1.15rem; + line-height: 1.5; + color: var(--ink-2); + margin: 0; +} + +.section { + padding-top: 2.5rem; +} + +.section-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + padding-bottom: 0.5rem; + margin-bottom: 1.25rem; + border-bottom: 1px solid var(--rule); +} + +.section-head h2 { + font-size: 1.25rem; + font-weight: 600; +} + +.section-note { + margin: 0; + font-family: var(--font-data); + font-size: 0.9rem; + color: var(--ink-3); + text-align: right; +} + +.section-note a { + color: var(--ink-2); +} + +.prose { + max-width: var(--measure); +} + +.prose p { + margin-bottom: 1em; +} + +.prose p:last-child { + margin-bottom: 0; +} + +.two-col { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0 3rem; + align-items: start; +} + +.two-col .section { + min-width: 0; +} + +/* ------------------------------------------------------------------ + Home + ------------------------------------------------------------------ */ +.hero { + background-color: rgba(20, 20, 20, 0.35); + border-bottom: 1px solid var(--rule); +} + +.hero-inner { + width: min(100% - 2 * var(--gutter), var(--page-width)); + margin: 0 auto; + padding: clamp(4rem, 12vh, 7rem) 0 clamp(3rem, 8vh, 5rem); +} + +.hero .display { + font-size: clamp(2.25rem, 6vw, 4rem); +} + +.hero .lede { + font-size: 1.25rem; +} + +.hero-links { + display: flex; + flex-wrap: wrap; + gap: 0.25rem 1.5rem; + margin: 1.75rem 0 0; + font-family: var(--font-data); + font-size: 1.05rem; +} + +.now-list { + margin: 0; +} + +.now-item { + display: grid; + grid-template-columns: 9rem 1fr; + gap: 1rem; + padding: 1rem 0; + border-bottom: 1px solid var(--rule); +} + +.now-item:last-child { + border-bottom: 0; +} + +.now-item dt { + font-family: var(--font-data); + color: var(--ink-3); + padding-top: 0.15em; +} + +.now-item dd { + margin: 0; + max-width: var(--measure); +} + +.shelf-row { + list-style: none; + padding: 0; + margin: 0 0 0.5rem; + display: flex; + gap: 0.6rem; + flex-wrap: wrap; +} + +.shelf-row li { + margin: 0; +} + +.shelf-row img { display: block; - max-width: min(40rem, 80%); - max-height: min(30rem, 100%); + width: 60px; + height: 90px; + object-fit: cover; + border: 1px solid var(--rule); } -.col { - width: calc(50% - 3rem); - margin-bottom: 3em; - float: left; +.shelf-row a:hover img { + border-color: var(--accent-bright); } -.rightCol { - float: left; - margin-right: 3em; +.shelf-more { + font-family: var(--font-data); + font-size: 0.95rem; } -.boxedImg { - border: .5rem solid rgba(22, 22, 22, .8); - max-width: calc(90% - 1rem); - display: block; - margin-left: auto; - margin-right: auto; +.selected-list { + list-style: none; + padding: 0; + margin: 0; + counter-reset: selected; +} + +.selected-list li { + display: grid; + grid-template-columns: 2.5rem 14rem 1fr; + gap: 1rem; + padding: 0.75rem 0; + border-bottom: 1px solid var(--rule); + margin: 0; + counter-increment: selected; +} + +.selected-list li::before { + content: counter(selected, decimal-leading-zero); + font-family: var(--font-data); + color: var(--ink-3); + font-size: 0.9rem; + padding-top: 0.2em; +} + +.selected-list li:last-child { + border-bottom: 0; +} + +.selected-list a { + color: var(--ink); +} + +.selected-list span { + color: var(--ink-2); +} + +.colophon { + display: grid; + grid-template-columns: 1fr auto; + gap: 2.5rem; + align-items: start; +} + +.colophon-text { + max-width: var(--measure); +} + +.titles { + margin-bottom: 1.5rem; } .blinkies { - width: 150px; - height: 20px; + display: grid; + grid-template-columns: repeat(2, 150px); + gap: 0.5rem; + margin: 0; } -#skillstag::after { - content: "Click for details..."; +.blinkies img { + width: 150px; + height: 20px; + image-rendering: pixelated; +} + +.colophon-figure { + margin: 0; + text-align: center; +} + +.colophon-figure img { display: block; - font-size: small; - color: var(--text-secondary); + max-width: 180px; +} + +figcaption { + font-family: var(--font-data); + font-size: 0.85rem; + color: var(--ink-3); + margin-top: 0.5rem; +} + +/* ------------------------------------------------------------------ + Work + ------------------------------------------------------------------ */ +.feature { + scroll-margin-top: 4.5rem; + padding: 0.5rem 0 1rem; + border-bottom: 1px solid var(--rule); + margin-bottom: 2rem; + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-template-areas: + "head" + "body" + "toggle"; + column-gap: 2.5rem; +} + +.feature.has-figure { + grid-template-columns: minmax(0, 1fr) minmax(16rem, 22rem); + /* Any extra height the figure needs goes to the body row, never the header row, + so the body text does not shift when the panel opens or closes. */ + grid-template-rows: auto 1fr auto; + grid-template-areas: + "head figure" + "body figure" + "toggle toggle"; +} + +.feature:last-of-type { + border-bottom: 0; + margin-bottom: 0; +} + +.feature-head { + grid-area: head; + margin-bottom: 1rem; +} + +.feature h3 { + font-size: 1.5rem; + margin-bottom: 0.4rem; +} + +.feature-tagline { + color: var(--ink-2); + font-size: 1.1rem; + max-width: var(--measure); + margin-bottom: 0; +} + +/* Collapsed regions: clipped with a fade at the bottom edge. */ +.feature-clip { + position: relative; + overflow: hidden; + transition: max-height 0.35s ease; +} + +.feature-clip::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2.5rem; + background: linear-gradient(to bottom, rgba(20, 20, 20, 0), rgba(20, 20, 20, 0.96)); + pointer-events: none; + transition: opacity 0.25s ease; +} + +.feature.expanded .feature-clip::after { + opacity: 0; +} + +/* The body peeks a line and a half; the figure shows its top edge. */ +.feature-body { + grid-area: body; + align-self: start; + max-height: 2.5rem; +} + +.feature-figure { + grid-area: figure; + align-self: start; + margin: 0.4rem 0 0; + max-height: 10rem; +} + +.feature .prose { + color: var(--ink-2); +} + +.feature-stack { + margin: 1.25rem 0 0; + font-family: var(--font-data); + color: var(--ink-2); +} + +.feature-stack .label { + margin-right: 0.5em; +} + +.feature-figure img { + display: block; + width: 100%; + border: 1px solid var(--rule); +} + +/* Centered chevron; shrinks into a small up-arrow once open. */ +.feature-expand { + grid-area: toggle; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + border: 0; + border-radius: 0; + background: transparent; + padding: 0.5rem 0 0.75rem; + color: var(--ink-3); + cursor: pointer; + transition: color 0.15s ease, padding 0.3s ease; +} + +.feature-expand::before { + content: ""; + width: 0.8em; + height: 0.8em; + border-right: 2px solid currentColor; + border-bottom: 2px solid currentColor; + transform: translateY(-25%) rotate(45deg); + transition: transform 0.3s ease, width 0.3s ease, height 0.3s ease; +} + +.feature-expand:hover { + color: var(--accent-bright); +} + +.feature.expanded .feature-expand { + padding: 1rem 0 0.25rem; +} + +.feature.expanded .feature-expand::before { + width: 0.45em; + height: 0.45em; + transform: translateY(25%) rotate(225deg); +} + +.feature .when-open, +.feature.expanded .when-closed { + display: none; +} + +.feature.expanded .when-open { + display: inline; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +.project-list { + list-style: none; + padding: 0; + margin: 0; +} + +.project { + scroll-margin-top: 4.5rem; + display: grid; + grid-template-columns: 11rem minmax(0, 1fr); + gap: 1.5rem; + padding: 1.25rem 0; + border-bottom: 1px solid var(--rule); + margin: 0; +} + +.project:last-child { + border-bottom: 0; +} + +.project-thumb { + aspect-ratio: 16 / 10; + overflow: hidden; +} + +.project-thumb img { + border: 1px solid var(--rule); + box-sizing: border-box; +} + +.project-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center top; + display: block; +} + +.project-thumb-empty { + width: 100%; + height: 100%; +} + +.project h3 { + font-size: 1.1rem; + margin-bottom: 0.2rem; +} + +.project-meta { + font-family: var(--font-data); + font-size: 0.9rem; + color: var(--ink-3); + margin-bottom: 0.5rem; + text-transform: lowercase; +} + +.project-meta .sep { + margin: 0 0.35em; +} + +.status-complete { color: var(--ok); } +.status-WIP { color: var(--warn); } +.status-incomplete { color: var(--bad); } + +.project-desc { + max-width: var(--measure); + margin-bottom: 0.5rem; +} + +.project-links { + font-family: var(--font-data); + margin: 0; + display: flex; + gap: 1.25rem; +} + +/* ------------------------------------------------------------------ + About + ------------------------------------------------------------------ */ +.about-bio { + padding-top: 1.5rem; +} + +.filters { + display: flex; + gap: 1.25rem; +} + +.filter-btn { + border: 0; + padding: 0; + border-radius: 0; + color: var(--ink-3); + border-bottom: 1px solid transparent; +} + +.filter-btn:hover { + color: var(--ink); +} + +.filter-btn[aria-pressed="true"] { + color: var(--ink); + border-bottom-color: var(--accent-bright); +} + +.timeline { + list-style: none; + padding: 0; + margin: 0; +} + +.timeitem { + display: grid; + grid-template-columns: 11rem minmax(0, 1fr); + gap: 1.5rem; + padding: 1.25rem 0; + border-bottom: 1px solid var(--rule); + margin: 0; +} + +.timeitem:last-child { + border-bottom: 0; +} + +.timeitem[hidden] { + display: none; +} + +.timeitem-date { + font-family: var(--font-data); + color: var(--ink-3); + margin: 0; + padding-top: 0.2em; +} + +.timeitem h3 { + font-size: 1.1rem; + margin-bottom: 0.5rem; +} + +.timeitem-content { + max-width: var(--measure); + color: var(--ink-2); +} + +.timeitem-org { + color: var(--ink-3); + font-family: var(--font-data); + margin-bottom: 0.5rem; +} + +.timeitem-content ul { + margin: 0; + padding-left: 1.1em; +} + +.timeline-empty { + color: var(--ink-3); + padding: 1rem 0; } #skillTree { - border-radius: 0.5em; - background: rgba(var(--bg-card-rgb), 0.6); - font-size: x-large; - margin-bottom: 10px; - padding: 1em 1.25em; + font-size: 1.05rem; } .skill { - padding: 0.2em 0; - color: var(--text-heading); + padding: 0.15em 0; } -.skill.activeSkill > .skillname { - color: rgb(170, 170, 170); +.skill > .skillname { + display: inline-block; + cursor: pointer; + color: var(--ink); + padding: 0.15em 0; +} + +.skill > .skillname::before { + content: "+"; + display: inline-block; + width: 1.25em; + color: var(--ink-3); + font-family: var(--font-data); +} + +.skill.activeSkill > .skillname::before { + content: "\2212"; +} + +.skill[data-length="0"] > .skillname { + cursor: default; + color: var(--ink-2); +} + +.skill[data-length="0"] > .skillname::before { + content: ""; } .skill > .skillname:hover { - color: rgb(170, 170, 170); + color: var(--accent-bright); +} + +.skill[data-length="0"] > .skillname:hover { + color: var(--ink-2); } .skill-children { display: grid; grid-template-rows: 0fr; - transition: grid-template-rows 0.3s ease; + transition: grid-template-rows 0.25s ease; } .skill.activeSkill > .skill-children { @@ -593,265 +965,267 @@ tr { .skill-children-inner { overflow: hidden; + padding-left: 1.25em; } .skill-children-inner > .skill { - font-size: large !important; - padding-left: .5em; + font-size: 0.95em; } -.skill > .skillname { - text-decoration: underline; - text-underline-offset: 2px; - cursor: pointer; - transition: color 0.2s ease; - padding: 0.2em 0; - display: inline-block; +.cert-group { + margin-bottom: 1.75rem; } -.skill > .skillname:hover { - color: #ddd; +.cert-group h3 { + font-size: 1.05rem; + margin-bottom: 0.2rem; } -.skill > .skillname::after { - content: " \2192"; - font-size: 0.85em; - margin-left: 0.2em; - text-decoration: none; +.cert-provider { + font-family: var(--font-data); + font-size: 0.9rem; + color: var(--ink-3); + margin-bottom: 0.5rem; } -.skill.activeSkill > .skillname::after { - content: " \2190"; -} - -.skill[data-length="0"].activeSkill > .skillname, .skill[data-length="0"] > .skillname:hover, .skill.activeSkill > .skillname:hover { - color: var(--text-heading); -} - -.skill[data-length="0"] > .skillname::after { - content: "" -} - -.skill[data-length="0"] > .skillname { - text-decoration: none; - cursor: default; -} - - -/* Project List Grid */ -.projectList { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 1.25em; - width: 100%; -} - -/* Project Card */ -.project { - position: relative; - background: rgba(var(--bg-card-rgb), 0.92); - border: 1px solid var(--border-subtle); - border-radius: 0.75em; - overflow: hidden; - display: flex; - flex-direction: column; - transition: border-color 0.25s ease, box-shadow 0.25s ease, transform 0.2s ease; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); -} - -.project:hover { - border-color: rgba(var(--accent-rgb), 0.35); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35); - transform: translateY(-2px); -} - -/* Project Image */ -.projImageWrap { - position: relative; - width: 100%; - height: 180px; - overflow: hidden; - background: #0c0c0c; - flex-shrink: 0; -} - -.projImageWrap img { - width: 100%; - height: 100%; - object-fit: cover; - object-position: center top; - filter: brightness(0.88); - transition: filter 0.3s ease; - display: block; -} - -.project:hover .projImageWrap img { - filter: brightness(1); -} - -/* Placeholder for projects without an image */ -.projImagePlaceholder { - width: 100%; - height: 100%; - background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.06) 25%, transparent 25%) -4px 0, - linear-gradient(225deg, rgba(var(--accent-rgb), 0.06) 25%, transparent 25%) -4px 0, - linear-gradient(315deg, rgba(var(--accent-rgb), 0.06) 25%, transparent 25%) 4px 0, - linear-gradient(45deg, rgba(var(--accent-rgb), 0.06) 25%, transparent 25%) 4px 0; - background-size: 8px 8px; - background-color: rgba(30, 30, 30, 0.6); -} - -/* Status indicator (inline, in card body) */ -.proj-status { - display: inline-block; - font-size: 0.68rem; - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; - padding: 0.15em 0.5em; - border-radius: 0.2em; - flex-shrink: 0; -} - -.proj-status.complete { color: var(--status-online); background: rgba(76, 175, 80, 0.1); } -.proj-status.WIP { color: var(--status-degraded); background: rgba(255, 193, 7, 0.1); } -.proj-status.incomplete { color: var(--status-offline); background: rgba(244, 67, 54, 0.1); } - -/* Project Content */ -.projContent { - padding: 1.2em 1.4em 1.4em; - display: flex; - flex-direction: column; - flex: 1; - gap: 0.5em; -} - -.projTitleRow { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 0.75em; -} - -.project h3 { - margin: 0; - font-size: 1rem; - font-weight: 600; - color: var(--text-heading); - line-height: 1.35; -} - -/* Project category tags */ -.projTags { - display: flex; - flex-wrap: wrap; - gap: 0.35em; -} - -.proj-tag { - font-size: 0.65rem; - font-weight: 500; - letter-spacing: 0.04em; - text-transform: capitalize; - padding: 0.1em 0.45em; - border-radius: 0.2em; - color: var(--text-secondary); - background: rgba(var(--accent-rgb), 0.06); -} - -.projDesc { - font-size: 0.82rem; - line-height: 1.6; - color: var(--text-secondary); - margin: 0; - flex: 1; -} - -/* Project Links */ -.projLinks { - display: flex; - flex-wrap: wrap; - gap: 0.5em; - margin-top: auto; - padding-top: 0.25em; -} - -.proj-link { - display: inline-flex; - align-items: center; - gap: 0.4em; - color: var(--text-secondary); - text-decoration: none; - font-size: 0.8rem; - transition: color 0.2s ease; -} - -.proj-link:hover { - color: var(--text-heading); - text-decoration: underline; -} - -.proj-link img.projectLink { - height: 1em; - width: auto; - filter: invert(1) opacity(0.65); +.cert-list { + list-style: none; padding: 0; margin: 0; - transition: filter 0.2s ease, opacity 0.2s ease; } -.proj-link:hover img.projectLink { - filter: invert(1) opacity(0.9); -} - -.grow { - flex-grow: 1; -} - -.tab { - display: inline; - float: right; -} - -.bottomBox a, .placeholding { - vertical-align: middle; - display: inline; - text-align: left; - height: unset; -} - -.footer { - position: fixed; - bottom: 0; - left: 0; -} - -#contentStuffer { +.cert-list li { display: flex; - flex-direction: column; - min-height: 100vh; + justify-content: space-between; + align-items: baseline; + gap: 1rem; + padding: 0.4em 0; + border-bottom: 1px solid var(--rule); + margin: 0; } -.socials { - margin-left: 1rem; - width: 0px; +.cert-list li:last-child { + border-bottom: 0; } -.socials img { - height: 2rem; - margin-bottom: 1rem; - filter: invert(29%) sepia(20%) saturate(1786%) hue-rotate(314deg) brightness(101%) contrast(110%); - transition: .2s; +.cert-code { + font-family: var(--font-data); + font-size: 0.85rem; + color: var(--ink-3); + white-space: nowrap; } -.socials img:hover { - opacity: .5; +/* ------------------------------------------------------------------ + Status + ------------------------------------------------------------------ */ +.status-summary { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 2rem; + align-items: center; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--rule); } -#vertLine { - border-right: 1px solid var(--accent); - margin-left: 1rem; - height: 10rem; +.status-summary-main { + display: flex; + align-items: center; + gap: 1rem; +} + +.summary-icon { + font-size: 1.75rem; + line-height: 1; + color: var(--ink-3); +} + +.summary-icon.operational { color: var(--ok); } +.summary-icon.partial { color: var(--warn); } +.summary-icon.major { color: var(--bad); } + +.summary-title { + font-size: 1.25rem; +} + +.summary-subtitle { + margin: 0.15rem 0 0; + color: var(--ink-3); + font-family: var(--font-data); +} + +.status-facts { + display: flex; + gap: 2rem; + margin: 0; + font-family: var(--font-data); +} + +.status-facts dt { + font-size: 0.85rem; + color: var(--ink-3); +} + +.status-facts dd { + margin: 0; + color: var(--ink); +} + +.table-wrap { + overflow-x: auto; +} + +.status-table th[scope="row"] { + color: var(--ink); + font-size: 1rem; + white-space: nowrap; +} + +.status-table td { + color: var(--ink-2); +} + +.status-table .num { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.status-table .uptime { + white-space: nowrap; +} + +.status-table .uptime strong { + font-weight: 400; +} + +.state-indicator { + display: inline-flex; + align-items: center; + gap: 0.5em; + white-space: nowrap; +} + +.state-dot { + width: 0.6em; + height: 0.6em; + border-radius: 50%; + display: inline-block; + background: var(--ink-3); +} + +.state-dot.online { background: var(--ok); } +.state-dot.degraded { background: var(--warn); } +.state-dot.offline { background: var(--bad); } +.state-dot.loading { background: var(--ink-3); } + +.table-note { + font-family: var(--font-data); + font-size: 0.85rem; + color: var(--ink-3); + margin-top: 0.75rem; +} + +.text-excellent { color: var(--ok); } +.text-good { color: var(--ok); } +.text-fair { color: var(--warn); } +.text-poor { color: var(--bad); } +.text-muted { color: var(--ink-3); } + +.status-error { + color: var(--bad); + border: 1px solid var(--bad); + padding: 0.75rem 1rem; + margin-bottom: 1.5rem; +} + +/* ------------------------------------------------------------------ + Books + ------------------------------------------------------------------ */ +.cover-row { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.cover-row li { + margin: 0; +} + +.cover-row img { + display: block; + width: 80px; + height: 120px; + object-fit: cover; + border: 1px solid var(--rule); +} + +.cover-row a:hover img { + border-color: var(--accent-bright); +} + +.review-list { + list-style: none; + padding: 0; + margin: 0; +} + +.review { + display: grid; + grid-template-columns: 80px minmax(0, 1fr); + gap: 1.5rem; + padding: 1.25rem 0; + border-bottom: 1px solid var(--rule); + margin: 0; + scroll-margin-top: 4rem; +} + +.review:last-child { + border-bottom: 0; +} + +.review:target h3 { + color: var(--accent-bright); +} + +.review-cover { + display: block; + width: 80px; + height: 120px; + object-fit: cover; + border: 1px solid var(--rule); +} + +.review h3 { + font-size: 1.1rem; + margin-bottom: 0.4rem; +} + +.review-body p { + max-width: var(--measure); +} + +.review-link { + font-family: var(--font-data); + margin: 0.5rem 0 0; +} + +/* ------------------------------------------------------------------ + Misc pages + ------------------------------------------------------------------ */ +.duck { + display: block; + max-width: 100%; + margin-top: 1.5rem; +} + +.fPage { + min-height: 70vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem var(--gutter); } .neon { @@ -865,20 +1239,20 @@ tr { .neonBox h1 { font-size: min(4em, 12vw); + font-weight: 400; } .neonBox h3 { font-size: min(2em, 6vw); + font-weight: 400; } .neonBox { border: 0.2rem solid #fff; border-radius: 2rem; padding: 2em; - margin: 2em; box-shadow: 0 0 0.2rem #fff, 0 0 0.2rem #fff, 0 0 2rem var(--neon-blue), 0 0 0.8rem var(--neon-blue), 0 0 2.8rem var(--neon-blue), inset 0 0 1.3rem var(--neon-blue); - /* background-blend-mode: ; */ text-align: center; } @@ -896,1358 +1270,111 @@ tr { } } -.fPage { +#fullIframe { + width: 100vw; height: 100vh; - width: 100%; - display: flex; - position: absolute; - justify-content: center; - background-position: center; - background-repeat: no-repeat; - background-size: cover; -} - -#desktopSpacer { - height: min(20em, 30vh); -} - -.homeSubContent { - background-color: rgba(var(--bg-card-rgb), 0.85); - padding: 2.5em 6em; - border-top: solid 2px rgba(var(--accent-rgb), 0.25); - border-radius: 0.5em 0.5em 0 0; - box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3); -} - -#homeProjects { - padding: 1em; -} - -.homeProject { - width: 20em; - height:5em; - -} - -.heightBox { - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; -} - -.fullHeight { - height: 100%; -} - -.chess { - max-width: 300px; - border: none; + border: 0; } +/* Chess.com embed (partials/chess.html, currently unused on any page) */ #chessProfile { - border-radius: 0.5em; - border-top: solid 3px rgba(var(--accent-rgb), 0.5); width: 300px; - height: calc(125px + 2em); - color: #AAAAAA; + color: var(--ink-2); display: grid; grid-template-columns: 1fr 1fr; cursor: pointer; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border: 1px solid var(--rule); + font-family: var(--font-data); } .chessInfo { - background-color: rgba(0, 0, 0, .3); - ; - margin: 1em; -} - -.chessRapid::after, .chessBlitz::after, .chessBullet::after, .chessPuzzles::after { - font-size: xx-small; - float: right; - margin-right: 5px; - margin-top: 1rem; -} - -.chessStat { - display: inline; - vertical-align: center; -} - -.chessStat::after { - font-size: xx-small; - float: right; -} - -.chessPuzzles::after { - content: 'Tactics'; -} - -.chessBullet::after { - content: "Bullet"; -} - -.chessBlitz::after { - content: "Blitz"; -} - -.chessRapid::after { - content: "Rapid"; -} - -.bottomtext { - font-size: x-small; - margin: 3px; - color: white; - text-align: right; -} - -.black { - background-color: black; -} - -.identity { - margin-right: 0px; -} - -.identity h5 { - margin: 0px; - margin-top: -25px; - background: black; - z-index: 50; - position: relative; - width: 90%; - margin-left: 10px; - font-weight: bold; - font-size: small; -} - -.pfpContainer { - display: flex; - justify-content: center; - height: 100%; -} - -.pfpContainer>div { - display: flex; - flex-direction: column; - justify-content: center; -} - -.pfp { - border-radius: 50%; - width: 100px; - text-align: center; -} - -.vContainer { - display: flex; - flex-direction: column; - max-height: 100%; - max-width: 100%; - justify-content: space-around; + padding: 1em; } .chessIcon { display: inline-block; - font-family: Chess New; + font-family: "Chess New"; font-size: x-large; width: 25px; text-align: center; } -.rapid { - color: #6c9d41; +.chessStat { + display: inline; } -.blitz { - color: #f7c045; -} - -.bullet { - color: #a58a45; -} - -.puzzles { - color: #db7e3b; -} - -.bookshelf { - max-width: 360px; - padding: 10px; - border: solid 2px #553; - border-radius: 0.5em; - background-image: url("../photos/wood.jpg"); - cursor: pointer; - transition: transform 0.3s ease, box-shadow 0.3s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); -} - -.bookshelf:hover { - transform: translateY(-3px); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5); -} - -.bookshelfHeader { - color: lightgray; - margin: .5em; - border-bottom: solid black 3px; - flex-grow: 1; -} - -.bookContainer { - display: flex; - justify-content: space-around; - flex-wrap: wrap; - gap: 1em; -} - -.bookContainer > a { - max-width: min(90px, 24%); - } - -.book { - display: inline-block; -} - -.book img { - display: block; - margin: auto; - width: 75px; - height: 112px; - transition: transform 0.3s ease, box-shadow 0.3s ease; - border-radius: 2px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); -} - -.book img:hover { - transform: translateY(-5px) scale(1.05); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5); -} - -.book h4 { - font-size: x-small; - color: lightgray; - margin-bottom: 0; - text-align: center; -} - -.bookReview { - width: min(100%, 30em); - padding: 1.5em; - border-top: solid 4px rgba(var(--accent-rgb), 0.5); - border-radius: 0.5em; - background: rgba(var(--bg-card-rgb), 0.85); - flex-grow: 1; - transition: transform 0.3s ease, box-shadow 0.3s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); -} - -.bookReview:hover { - transform: translateY(-3px); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); -} - -.bookcover { - width: 6em; -} - -.booklist { - /* margin: min(10em, 10%); - margin-bottom: 2em; - margin-top: 2em; */ - justify-content: left; - gap: 1.5em; -} - -.boxed { - background-color: rgba(var(--bg-card-rgb), 0.85); - border-top: solid 4px rgba(var(--accent-rgb), 0.5); - border-radius: 0.5em; - padding: 1.5em; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); -} - -.emPad { - padding: 1em; -} - -.timeitem { - transition: .4s; - overflow-y: clip; - height: auto; - max-height: 500px; -} - -.timeitem a { - color: inherit; - font-family: inherit; - font-size: inherit; - text-decoration: underline; -} - -.datetext { - margin: .25em; - font-size: small; - margin-left: -1.5em; - font-family: robotoreg; -} - -.smallImg { - display: inline-block; - width: 75px; -} - -.hover-container { - position: relative; -} - -.hover-container .hover-element { - position: absolute; - top: 3em; - left: -50%; - padding: 1em; - border-radius: 1em; - width: 20em; - background-color: var(--bg-base); - opacity: 0; - transition: 1s; - z-index: -1; -} - -.hover-container:hover .hover-element { - opacity: 1; - z-index: 10; -} - -@font-face { - font-family: Chess New; - src: url('../fonts/chessglyph-new.0cc8115c.woff2'); - font-display: swap; -} - -@media screen and (max-width: 600px) { - #nametagContainer { - width: unset; - } -} - -@media screen and (max-width: 1400px) { - - .projectList { - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - } - - #homepage { - flex-direction: column; - } - - #HomeContent { - min-width: 0; - } - - /* iOS gotta render gaussian blurs differently so better uhh make rasters for mobile users */ - - /* Be sure to uncomment this if we ever stop using jebwin memes */ - /* #homeIcon { - content: url("../icons/rasterlogo.png"); - } */ - - .name { - font-size: min(6vw, 2em); - background: url("../icons/min.rasterLogo.avif") 0/5rem no-repeat; - background-size: contain; - } - - .mobileV { - flex-direction: column; - } - - #menu { - display: unset; - } - - .header { - background-color: var(--bg-base); - border-bottom: solid 3px rgba(var(--accent-rgb), 0.4); - } - - .footer { - position: unset; - } - - #footerSpacer { - flex-grow: 1; - } - - #aboutCards { - flex-direction: column; - } - - #aboutCards > * { - margin: .25em auto; - } - - .header h1 { - font-size: 1.5rem; - } - - .navBar { - flex-direction: column; - background-color: rgba(var(--bg-card-rgb), 0.95); - border-top: 1px solid rgba(var(--accent-rgb), 0.3); - } - - .navElement { - width: 100%; - padding: 0; - text-align: left; - border-bottom: none; - border-left: 3px solid transparent; - transition: border-color 0.25s ease, background-color 0.25s ease; - } - - .navElement:hover { - border-bottom: none; - border-left-color: var(--accent); - background-color: rgba(var(--accent-rgb), 0.1); - } - - .navElement + .navElement { - border-top: 1px solid rgba(var(--accent-rgb), 0.25); - border-left: none; - } - - .navElement * { - padding: .6rem 1.2rem; - text-align: left; - } - - .navControl { - max-height: 0px; - margin: 0; - float: none; - overflow: hidden; - } - - .vertOnMobile { - flex-direction: column; - } - - .sideimg { - position: relative; - } - - body { - background-size: auto 100%; - } - - .foregroundContent { - /* centered objects will need to remove 1.5 of this to center */ - padding-left: 1em; - padding-right: 1em; - padding-bottom: 36px; - padding-top: 3em; - } - - .homegroundContent { - padding-top: 5em; - } - - .homeSubContent { - padding: 1em; - } - - .onRight { - margin: unset; - margin-top: 3rem; - } - - .onRight>* { - margin: auto; - } - - .col { - width: 100%; - margin-bottom: 0; - } - - .rightCol { - position: inherit; - margin-right: inherit; - } - - .footer { - position: relative; - width: 100%; - display: inline-block; - background-color: var(--accent); - } - - .socials { - display: flex; - justify-content: space-around; - padding-top: .5rem; - padding-bottom: .5rem; - margin-right: auto; - margin-left: auto; - width: min(100vw, 400px); - } - - .socials * { - display: inline; - text-align: center; - padding-left: 0px; - padding-right: 0px; - } - - .socials img { - height: 2rem; - margin-bottom: 0px; - filter: none; - } - - #vertLine { - display: none; - height: 0px; - } - - .card-grid { - grid-template-columns: 1fr; - } - - .cert-grid { - grid-template-columns: 1fr; - } - - .legend-items { - flex-direction: column; - gap: 0.5em; - } - - .summary-content { - flex-direction: column; - text-align: center; - } - - .summary-indicator { - flex-direction: column; - text-align: center; - } -} - -@media screen and (max-width: 600px) { - .projectList { - grid-template-columns: 1fr; - } - - .projImageWrap { - height: 160px; - } -} - -/* Reusable Card & State Components */ -.page-subtitle { - text-align: center; - color: var(--text-secondary); - margin-top: -10px; - margin-bottom: 2em; - font-size: 1rem; -} - -/* Timeline */ -.timeline { - position: relative; - padding-left: 2em; -} - -.timeline::before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 2px; - background: linear-gradient(to bottom, rgba(var(--accent-rgb), 0.05), rgba(var(--accent-rgb), 0.7) 10%, rgba(var(--accent-rgb), 0.7) 90%, rgba(var(--accent-rgb), 0.05)); -} - -.timeitem { - position: relative; - margin-bottom: 1.5em; - transition: .4s; - overflow-y: clip; - height: auto; - max-height: 500px; -} - -.timeitem::before { - content: ''; - position: absolute; - left: -2.15em; - top: 0.5em; - width: 10px; - height: 10px; +.pfp { border-radius: 50%; - background: var(--bg-base); - border: 2px solid rgba(var(--accent-rgb), 0.6); - z-index: 1; - transition: background 0.25s ease, border-color 0.25s ease; + width: 100px; } -.timeitem:hover::before { - background: rgba(var(--accent-rgb), 0.4); - border-color: var(--accent); +.rapid { color: #6c9d41; } +.blitz { color: #f7c045; } +.bullet { color: #a58a45; } +.puzzles { color: #db7e3b; } + +/* ------------------------------------------------------------------ + Footer + ------------------------------------------------------------------ */ +.site-footer { + border-top: 1px solid var(--rule); + margin-top: auto; } -.timeitem-inner { - position: relative; - padding: 1em 1.25em; -} - -.timeitem-header { - display: flex; - flex-direction: column; - gap: 0.25em; - margin-bottom: 0.5em; -} - -.timeitem-header strong { - color: var(--text-heading); - font-size: 1.05rem; -} - -.timeitem-header .datetext { - font-size: 0.8rem; - color: var(--text-secondary); - font-family: 'Courier New', Courier, monospace; -} - -.timeitem-content { - color: var(--text-body); - font-size: 0.9rem; - line-height: 1.6; - margin-bottom: 0.75em; -} - -.timeitem-tags { +.footer-inner { + width: min(100% - 2 * var(--gutter), var(--page-width)); + margin: 0 auto; + padding: 1.5rem 0 2rem; display: flex; flex-wrap: wrap; - gap: 0.4em; + align-items: baseline; + gap: 0.5rem 2rem; + font-family: var(--font-data); + font-size: 0.95rem; } -.tag { - display: inline-block; - padding: 0.15em 0.6em; - border-radius: 0.25em; - font-size: 0.7rem; - font-family: 'Courier New', Courier, monospace; - letter-spacing: 0.05em; - text-transform: uppercase; - border: 1px solid rgba(var(--accent-rgb), 0.35); - color: var(--text-secondary); - background: rgba(var(--accent-rgb), 0.07); -} - -.tag.pinned { - border-color: rgba(var(--accent-rgb), 0.7); - background: rgba(var(--accent-rgb), 0.15); - color: var(--text-heading); -} - -.tag.experience { color: #6cb4ee; } -.tag.education { color: #8bc34a; } -.tag.technical { color: #ce93d8; } - -/* Timeline filter buttons */ -.timeline-filters { - display: flex; - flex-wrap: wrap; - gap: 0.5em; - margin-bottom: 1.5em; -} - -.filter-btn { - padding: 0.35em 0.9em; - border: 1px solid rgba(var(--accent-rgb), 0.3); - border-radius: 0.25em; - background: transparent; - color: var(--text-secondary); - font-family: 'Courier New', Courier, monospace; - font-size: 0.78rem; - letter-spacing: 0.05em; - text-transform: uppercase; - cursor: pointer; - transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease; -} - -.filter-btn:hover, .filter-btn.active { - border-color: rgba(var(--accent-rgb), 0.7); - background: rgba(var(--accent-rgb), 0.12); - color: var(--text-heading); -} - -.filter-btn.active { - border-color: var(--accent); -} - -.timeitem.hidden { - display: none; -} - -.timeline.collapsed::after { - content: ''; - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 8em; - background: linear-gradient(to bottom, transparent, rgba(var(--bg-card-rgb), 0.97)); - pointer-events: none; -} - -.timeline-expand-wrap { - position: relative; - z-index: 2; - display: flex; - justify-content: center; - margin-top: 1em; - margin-bottom: 2.5em; -} - -.timeline-expand-wrap.overlap { - margin-top: -3.75em; -} - -.timeline-expand-btn { - padding: 0.65em 2em; - border: 1px solid rgba(var(--accent-rgb), 0.5); - border-radius: 2em; - background: rgba(var(--accent-rgb), 0.2); - color: var(--text-heading); - font-family: 'Courier New', Courier, monospace; - font-size: 0.85rem; - font-weight: bold; - letter-spacing: 0.05em; - text-transform: uppercase; - flex-grow: 1; - cursor: pointer; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35); - transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease; -} - -.timeline-expand-btn:hover { - background: rgba(var(--accent-rgb), 0.35); - border-color: var(--accent); - transform: translateY(-1px); -} - -.timeline-expand-btn.hidden { - display: none; -} - -/* Summary Card */ -.summary-card { - padding: 2em; - margin-bottom: 2em; -} - -.summary-content { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 2em; -} - -.summary-indicator { - display: flex; - align-items: center; - gap: 1.5em; -} - -.summary-icon { - font-size: 3rem; - line-height: 1; - animation: spin 2s linear infinite; -} - -.summary-icon.operational { - color: var(--status-online); - animation: pulse-icon 1.5s ease-in-out infinite; -} - -.summary-icon.partial { - color: var(--status-degraded); - animation: pulse-icon 1.5s ease-in-out infinite; -} - -.summary-icon.major { - color: var(--status-offline); - animation: pulse-icon 1.5s ease-in-out infinite; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -@keyframes pulse-icon { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -.summary-title { +.footer-name { margin: 0; - font-size: 1.5rem; - color: var(--text-heading); - font-weight: 600; + color: var(--ink); } -.metric-box { - display: flex; - flex-direction: column; - align-items: center; - padding: 1em 1.5em; - border-radius: 0.5em; - min-width: 80px; -} - -.metric-number { - font-size: 2rem; - font-weight: bold; - color: var(--text-heading); - line-height: 1; -} - -.metric-label { - color: var(--text-secondary); - font-size: 0.9rem; -} - -.metric-box .metric-label { - font-size: 0.85rem; - margin-top: 0.3em; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -#refreshBtn { - background: rgba(var(--accent-rgb), 0.8); - color: var(--text-heading); - border: none; - padding: 0.5em 1.5em; - border-radius: 0.3em; - cursor: pointer; - font-size: 0.9rem; - transition: 0.3s; -} - -#refreshBtn:hover:not(:disabled) { - background: var(--accent); -} - -#refreshBtn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Card Grid */ -.card-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 1.5em; - margin-bottom: 2em; -} - -/* Info Card */ -.info-card { - background: rgba(var(--bg-card-rgb), 0.85); - border-radius: 0.5em; - padding: 1.5em; - border-top: solid 4px rgba(var(--accent-rgb), 0.5); - transition: 0.3s; -} - -.info-card.online { - border-top-color: rgba(76, 175, 80, 0.8); -} - -.info-card.degraded { - border-top-color: rgba(255, 193, 7, 0.8); -} - -.info-card.offline { - border-top-color: rgba(244, 67, 54, 0.8); -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1em; - padding-bottom: 0.5em; - border-bottom: 1px solid var(--border-subtle); -} - -.card-header h3 { - margin: 0; - font-size: 1.1rem; - color: var(--text-heading); -} - -/* State Indicators */ -.state-indicator { - display: flex; - align-items: center; - gap: 0.5em; -} - -.state-text { - font-size: 0.85rem; - color: var(--text-secondary); -} - -.state-dot { - width: 12px; - height: 12px; - border-radius: 50%; - display: inline-block; -} - -.state-dot.online, .state-dot.complete { - background: var(--status-online); - box-shadow: 0 0 10px var(--status-online); -} - -.state-dot.degraded, .state-dot.WIP { - background: var(--status-degraded); - box-shadow: 0 0 10px var(--status-degraded); -} - -.state-dot.offline, .state-dot.incomplete { - background: var(--status-offline); - box-shadow: 0 0 10px var(--status-offline); -} - -.state-dot.loading { - background: var(--text-secondary); - animation: pulse 1.5s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.5; - } -} - -.card-body { - display: flex; - flex-direction: column; - gap: 0.5em; -} - -.metric-row { - display: flex; - justify-content: space-between; - padding: 0.3em 0; -} - -.metric-value { - color: var(--text-heading); - font-size: 0.9rem; - font-weight: bold; -} - -.info-box { - background: rgba(var(--bg-card-rgb), 0.85); - border-radius: 0.5em; - padding: 1.5em; - border: solid 2px rgba(var(--accent-rgb), 0.5); - margin-bottom: 2em; -} - -.info-box h4 { - margin-top: 0; - color: var(--text-heading); -} - -.legend-items { - display: flex; - gap: 2em; - flex-wrap: wrap; -} - -.legend-item { - display: flex; - align-items: center; - gap: 0.5em; - color: var(--text-secondary); - font-size: 0.9rem; -} - -.note-text { - text-align: center; - font-size: 0.85rem; - color: var(--text-secondary); - font-style: italic; -} - -.card-footer { - margin-top: 1em; - padding-top: 0.8em; - border-top: 1px solid var(--border-subtle); -} - -.detail-label { - color: var(--text-secondary); - font-size: 0.85rem; - margin-bottom: 0.3em; - font-weight: bold; -} - -.detail-values { - color: var(--text-heading); - font-size: 0.85rem; - line-height: 1.5; -} - -.detail-values strong { - font-weight: 600; -} - -/* State color utilities */ -.text-excellent { - color: var(--status-online) !important; -} - -.text-good { - color: var(--status-good) !important; -} - -.text-fair { - color: var(--status-degraded) !important; -} - -.text-poor { - color: var(--status-offline) !important; -} - -.text-muted { - color: #888 !important; -} -/* Certifications */ -.cert-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1.5em; - margin-top: 1.5em; -} - -/* Deep-dive project panels (collapsed by default) */ -.feature-projects { - display: flex; - flex-direction: column; - gap: 2em; - margin-bottom: 3.5em; -} - -.feature-project { - position: relative; - padding: 0; - overflow: hidden; - border-top: none; - border-radius: 0.75em; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); -} - -.feature-project::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, var(--accent) 0%, rgba(var(--accent-rgb), 0.85) 40%, rgba(var(--accent-rgb), 0.15) 100%); - z-index: 2; -} - -.feature-project-content { - position: relative; - z-index: 1; -} - -.feature-project-media { - position: absolute; - top: 0; - bottom: 0; - right: 0; - width: 620px; - background-size: cover; - background-position: right bottom; - z-index: 0; -} - -@media screen and (max-width: 1100px) { - .feature-project-media { - display: none; - } -} - -.feature-project-media::after { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient( - to right, - rgba(var(--bg-card-rgb), 1) 0%, - rgba(var(--bg-card-rgb), 1) 50%, - rgba(var(--bg-card-rgb), 0.75) 62%, - rgba(var(--bg-card-rgb), 0.15) 82%, - rgba(var(--bg-card-rgb), 0) 96% - ); -} - -.feature-project-header { - position: relative; - overflow: hidden; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1.5em; - padding: 1.75em 2em; - cursor: pointer; - transition: background 0.2s ease; -} - -.feature-project-heading { - position: relative; - z-index: 1; - max-width: 46em; -} - -.feature-project-header:hover { - background: rgba(var(--accent-rgb), 0.06); -} - -.feature-project-header h3 { - margin: 0 0 0.35em 0; - color: var(--text-heading); - font-size: 1.55rem; -} - -.feature-project-tagline { - margin: 0; - color: var(--text-secondary); - font-size: 1rem; -} - -.feature-project-toggle { - position: relative; - z-index: 1; - flex-shrink: 0; - width: 2.4em; - height: 2.4em; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid rgba(var(--accent-rgb), 0.4); - border-radius: 50%; - background: rgba(var(--bg-card-rgb), 0.85); - color: var(--text-heading); - font-size: 1.4rem; - line-height: 1; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - transition: transform 0.3s ease, border-color 0.2s ease, background 0.2s ease; -} - -.feature-project.expanded .feature-project-toggle { - transform: rotate(45deg); - border-color: var(--accent); - background: rgba(var(--accent-rgb), 0.18); -} - -.feature-project-body { - display: grid; - grid-template-rows: 0fr; - transition: grid-template-rows 0.3s ease; -} - -.feature-project.expanded .feature-project-body { - grid-template-rows: 1fr; -} - -.feature-project-body-inner { - overflow: hidden; -} - -.feature-project-prose { - max-width: 46em; - padding: 1.5em 2em 1.75em; - border-top: 1px solid var(--border-subtle); - font-size: 1.05rem; - line-height: 1.75; -} - -.feature-project-prose p + p { - margin-top: 1em; -} - -.feature-project-prose code { - background: rgba(var(--accent-rgb), 0.12); - padding: 0.1em 0.4em; - border-radius: 0.25em; - font-size: 0.9em; -} - -.feature-project-stack { - display: flex; - flex-wrap: wrap; - gap: 0.6em; - padding: 0 2em 2em; -} - -.feature-project-stack-item { - padding: 0.35em 0.9em; - border: 1px solid rgba(var(--accent-rgb), 0.35); - border-radius: 2em; - background: rgba(var(--accent-rgb), 0.07); - color: var(--text-secondary); - font-family: 'Courier New', Courier, monospace; - font-size: 0.8rem; - letter-spacing: 0.03em; -} - -@media screen and (max-width: 900px) { - .feature-project-header { - padding: 1.5em; - } - - .feature-project-prose { - padding: 1.25em 1.5em 1.5em; - } - - .feature-project-stack { - padding: 0 1.5em 1.5em; - } -} - -.skills-certs-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2em; - align-items: start; -} - -.skills-certs-grid .skills-col #skillTree { - font-size: large; -} - -@media screen and (max-width: 900px) { - .skills-certs-grid { - grid-template-columns: 1fr; - } -} - -.cert-group { - margin-bottom: 1.5em; - min-width: 0; -} - -.cert-group-provider { - font-size: 0.75rem; - color: var(--text-secondary); - margin: 0 0 0.25em 0; - letter-spacing: 0.08em; -} - -.cert-program-badge { - display: inline-flex; - align-items: center; - max-width: 100%; - padding: 0.35em 0.9em; - margin-bottom: 1em; - border: 1px solid rgba(var(--accent-rgb), 0.75); - background: rgba(var(--accent-rgb), 0.18); - color: var(--text-heading) !important; - font-size: 0.78rem; - letter-spacing: 0.09em; - text-transform: uppercase; - font-family: 'Courier New', Courier, monospace; - transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; - opacity: 1 !important; -} - -.cert-program-badge:hover { - background: rgba(var(--accent-rgb), 0.32); - border-color: rgb(var(--accent-rgb)); - box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.35); - opacity: 1 !important; -} - -.cert-list { +.footer-links { list-style: none; padding: 0; - margin: 0.5em 0 0 0; + margin: 0; display: flex; - flex-direction: column; - gap: 0.35em; + flex-wrap: wrap; + gap: 0.25rem 1.25rem; } -.cert-list li { +.footer-links li { margin: 0; } -.cert-item { - display: flex; - align-items: center; - gap: 0.6em; - padding: 0.5em 0.75em; - border-left: 3px solid rgba(var(--accent-rgb), 0.65); - background: rgba(0, 0, 0, 0.25); - color: var(--text-secondary) !important; - font-size: 0.88rem; - text-decoration: underline; - text-decoration-color: rgba(var(--accent-rgb), 0.35); - text-underline-offset: 3px; - transition: border-color 0.2s ease, background 0.2s ease, padding-left 0.2s ease, color 0.2s ease; - opacity: 1 !important; +.footer-links a { + color: var(--ink-2); + text-decoration: none; } -.cert-item::before { - content: '›'; - color: rgb(var(--accent-rgb)); - font-size: 1.25em; - line-height: 1; - flex-shrink: 0; - transition: transform 0.2s ease; +.footer-links a:hover { + color: var(--accent-bright); } -.cert-item:hover { - border-left-color: rgb(var(--accent-rgb)); - background: rgba(var(--accent-rgb), 0.12); - padding-left: 1em; - color: var(--text-heading) !important; - text-decoration-color: rgba(var(--accent-rgb), 0.6); - opacity: 1 !important; +.footer-note { + margin: 0; + margin-left: auto; + color: var(--ink-3); } -.cert-item:hover::before { - transform: translateX(3px); -} - -/* SPA Loading Bar */ +/* ------------------------------------------------------------------ + Chrome: loading bar, skip link + ------------------------------------------------------------------ */ #loading-bar { position: fixed; top: 0; left: 0; - height: 3px; - background: linear-gradient(90deg, var(--accent) 0%, #fff 50%, var(--accent) 100%); - background-size: 200% 100%; - box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.6); + height: 2px; + background: var(--accent-bright); z-index: 2000; width: 0%; opacity: 0; @@ -2261,7 +1388,6 @@ tr { #loading-bar.active { width: 70%; - animation: loading-shimmer 2s infinite linear; } #loading-bar.finish { @@ -2270,32 +1396,171 @@ tr { transition: width 0.2s ease, opacity 0.4s ease 0.1s; } -@keyframes loading-shimmer { - from { background-position: 200% 0; } - to { background-position: -200% 0; } -} - -/* Skip-to-content link (accessible navigation) */ .skip-link { position: fixed; top: -100%; left: 50%; transform: translateX(-50%); z-index: 10000; - background: #000; - color: #fff; + background: var(--bg); + color: var(--ink); padding: 0.75rem 1.5rem; - font-size: 0.95rem; - font-weight: 600; text-decoration: none; - border: 2px solid currentColor; - border-radius: 0 0 4px 4px; + border: 1px solid var(--rule-strong); white-space: nowrap; transition: top 0.15s ease; } .skip-link:focus { top: 0; - outline: 2px solid #4E9A26; - outline-offset: 0; +} + +/* ------------------------------------------------------------------ + Responsive + ------------------------------------------------------------------ */ +@media (max-width: 900px) { + .two-col { + grid-template-columns: 1fr; + } + + .feature.has-figure { + grid-template-columns: minmax(0, 1fr); + grid-template-areas: + "head" + "figure" + "body" + "toggle"; + } + + .feature-figure { + max-width: 28rem; + margin-bottom: 1rem; + } + + .feature:not(.expanded) .feature-figure { + max-height: 6rem; + } + + .status-summary { + grid-template-columns: 1fr; + gap: 1rem; + align-items: start; + } + + .status-facts { + flex-wrap: wrap; + gap: 1rem 2rem; + } + + .colophon { + grid-template-columns: 1fr; + } + + .colophon-figure { + text-align: left; + } +} + +@media (max-width: 720px) { + body { + font-size: 1rem; + } + + .site-header { + flex-wrap: wrap; + height: auto; + min-height: 3.25rem; + } + + .menu-toggle { + display: inline-block; + margin: 0.5rem 0; + } + + .site-nav { + display: none; + flex-basis: 100%; + flex-direction: column; + gap: 0; + border-top: 1px solid var(--rule); + padding: 0.25rem 0 0.5rem; + } + + .site-nav a, + .site-nav a:last-child { + padding: 0.6rem 0.75rem; + margin: 0; + border-bottom: 0; + border-left: 2px solid transparent; + } + + .site-nav a[aria-current="page"] { + border-left-color: var(--accent-bright); + } + + .brand-tagline { + display: none; + } + + .site-header.open .site-nav { + display: flex; + } + + + .section-head { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .section-note { + text-align: left; + } + + .now-item, + .timeitem, + .project { + grid-template-columns: 1fr; + gap: 0.5rem; + } + + .project-thumb { + max-width: 20rem; + } + + .selected-list li { + grid-template-columns: 2.5rem 1fr; + } + + .selected-list li span { + grid-column: 2; + } + + .review { + grid-template-columns: 60px minmax(0, 1fr); + gap: 1rem; + } + + .review-cover { + width: 60px; + height: 90px; + } + + .footer-note { + margin-left: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + .neon { + animation: none; + } + + .skill-children { + transition: none; + } } diff --git a/src/static/js/idler.js b/src/static/js/idler.js index 6219843..df39c89 100755 --- a/src/static/js/idler.js +++ b/src/static/js/idler.js @@ -49,7 +49,8 @@ class Ball { function setup() { frameRate(15); const pixels = screenHeight * screenWidth; - createCanvas(screenWidth, screenHeight); + const canvas = createCanvas(screenWidth, screenHeight); + canvas.parent('bg'); for (let i = 0; i < pixels * density; i++) { balls.push(new Ball( random(screenWidth), diff --git a/src/static/js/responsive.js b/src/static/js/responsive.js index 92f8181..1e0c66e 100755 --- a/src/static/js/responsive.js +++ b/src/static/js/responsive.js @@ -1,23 +1,23 @@ function toggleMenu(collapse) { - if (window.innerWidth < 1400) { - const menu = document.querySelector(".navControl"); - const bar = document.querySelector(".header"); - const btn = document.getElementById("menu"); - const isCollapsed = !menu.style.maxHeight || menu.style.maxHeight === "0px"; - - if (isCollapsed && !collapse) { - menu.style.maxHeight = `${menu.scrollHeight + 10}px`; - bar.style.borderBottomWidth = "0px"; - if (btn) btn.setAttribute("aria-expanded", "true"); - } else { - menu.style.maxHeight = "0px"; - bar.style.borderBottomWidth = "3px"; - if (btn) btn.setAttribute("aria-expanded", "false"); - } - } + const header = document.getElementById("siteHeader"); + const btn = document.getElementById("menu"); + if (!header) return; + const open = collapse ? false : !header.classList.contains("open"); + header.classList.toggle("open", open); + if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false"); } -async function goto(location, { push = true } = {}) { +function markCurrentPage(location) { + document.querySelectorAll(".site-nav a[data-page]").forEach(function (link) { + if (link.dataset.page === location) { + link.setAttribute("aria-current", "page"); + } else { + link.removeAttribute("aria-current"); + } + }); +} + +async function goto(location, { push = true, hash = "" } = {}) { const loadingBar = document.getElementById('loading-bar'); if (loadingBar) { @@ -61,18 +61,20 @@ async function goto(location, { push = true } = {}) { oldScript.parentNode.replaceChild(newScript, oldScript); }); - if (window.location.href.includes("#")) { - const id = decodeURIComponent(window.location.hash.substring(1)); - const el = document.getElementById(id); - if (el) el.scrollIntoView(); + const target = hash || (push ? "" : decodeURIComponent(window.location.hash.substring(1))); + const targetEl = target ? document.getElementById(target) : null; + if (targetEl) { + revealTarget(targetEl); + targetEl.scrollIntoView({ behavior: "instant", block: "start" }); } else { window.scrollTo({ top: 0, left: 0, behavior: "instant" }); } toggleMenu(true); + markCurrentPage(location); document.querySelector("title").textContent = metadata["title"]; if (push) { - history.pushState(null, null, metadata["canonical"]); + history.pushState(null, null, metadata["canonical"] + (hash ? "#" + hash : "")); } } catch (err) { @@ -93,15 +95,64 @@ async function goto(location, { push = true } = {}) { } } -function backButton() { - const path = window.location.pathname; - goto(path.substring(1), { push: false }); +// Featured project panels: collapsed to a short peek until expanded. +function setFeatureExpanded(feature, expanded) { + const clips = feature.querySelectorAll(".feature-clip"); + const btn = feature.querySelector(".feature-expand"); + if (expanded) { + feature.classList.add("expanded"); + clips.forEach(function (el) { + el.style.maxHeight = el.scrollHeight + "px"; + }); + // Once open, let the regions size themselves so late-loading content is never clipped. + setTimeout(function () { + if (!feature.classList.contains("expanded")) return; + clips.forEach(function (el) { + el.style.maxHeight = "none"; + }); + }, 400); + } else { + // Pin the current heights, force a reflow, then let CSS animate down to the peek. + clips.forEach(function (el) { + el.style.maxHeight = el.scrollHeight + "px"; + }); + void feature.offsetHeight; + feature.classList.remove("expanded"); + clips.forEach(function (el) { + el.style.maxHeight = ""; + }); + } + if (btn) btn.setAttribute("aria-expanded", expanded ? "true" : "false"); } -function toggleFeatureProject(header) { - const panel = header.closest(".feature-project"); - const expanded = panel.classList.toggle("expanded"); - header.setAttribute("aria-expanded", expanded ? "true" : "false"); +function toggleFeature(btn) { + const feature = btn.closest(".feature"); + if (feature) setFeatureExpanded(feature, !feature.classList.contains("expanded")); +} + +// Expand a collapsed panel (or the one containing the target) before jumping to it. +function revealTarget(el) { + const feature = el.closest(".feature"); + if (feature && !feature.classList.contains("expanded")) { + setFeatureExpanded(feature, true); + feature.querySelectorAll(".feature-clip").forEach(function (el) { + el.style.maxHeight = "none"; + }); + } +} + +document.addEventListener("DOMContentLoaded", function () { + if (!window.location.hash) return; + const el = document.getElementById(decodeURIComponent(window.location.hash.substring(1))); + if (el) { + revealTarget(el); + el.scrollIntoView({ behavior: "instant", block: "start" }); + } +}); + +function backButton() { + const path = window.location.pathname; + goto(path.substring(1) || "home", { push: false }); } function activeSkill(obj) { diff --git a/src/static/js/status.js b/src/static/js/status.js index dff2791..77cb9db 100644 --- a/src/static/js/status.js +++ b/src/static/js/status.js @@ -22,14 +22,14 @@ function updateStatusDisplay(data) { if (data.last_check) { const lastCheck = new Date(data.last_check); const lastUpdateEl = document.getElementById('lastUpdate'); - if (lastUpdateEl) lastUpdateEl.textContent = `Last checked: ${lastCheck.toLocaleString()}`; + if (lastUpdateEl) lastUpdateEl.textContent = lastCheck.toLocaleString(); } if (data.next_check) { const nextCheckEl = document.getElementById('nextUpdate'); if (nextCheckEl) { const nextCheck = new Date(data.next_check); - nextCheckEl.textContent = `Next check: ${nextCheck.toLocaleString()}`; + nextCheckEl.textContent = nextCheck.toLocaleString(); } } @@ -43,7 +43,7 @@ function updateStatusDisplay(data) { const refreshBtn = document.getElementById('refreshBtn'); if (refreshBtn) { refreshBtn.disabled = false; - refreshBtn.textContent = 'Refresh Now'; + refreshBtn.textContent = 'Refresh now'; } } @@ -112,7 +112,7 @@ function updateServiceCard(service) { formatUptime(service.uptime['7d'], '7d'), formatUptime(service.uptime['30d'], '30d'), formatUptime(service.uptime.all_time, 'All'), - ].join(' | '); + ].join(' · '); } if (checksDisplay && service.total_checks !== undefined) { @@ -188,9 +188,9 @@ function showError(message) { const errorDiv = document.createElement('div'); errorDiv.className = 'status-error'; errorDiv.textContent = message; - errorDiv.style.cssText = 'background: rgba(244, 67, 54, 0.2); color: #f44336; padding: 1em; margin: 1em 0; border-radius: 0.5em; text-align: center;'; + - const container = document.querySelector('.foregroundContent'); + const container = document.querySelector('.page'); if (container) { container.insertBefore(errorDiv, container.firstChild); setTimeout(function() { errorDiv.remove(); }, 5000); @@ -201,7 +201,7 @@ function refreshStatus() { const refreshBtn = document.getElementById('refreshBtn'); if (refreshBtn) { refreshBtn.disabled = true; - refreshBtn.textContent = 'Checking...'; + refreshBtn.textContent = 'Checking'; } fetchStatus(); } diff --git a/src/static/json/books.json b/src/static/json/books.json index c736877..3f9e4a0 100755 --- a/src/static/json/books.json +++ b/src/static/json/books.json @@ -1,18 +1,21 @@ { "selection": [ - "The Rational Optimist", - "The End of the World is Just the Beginning", - "When to Rob a Bank", + "The Rational Optimist", + "Project Hail Mary", "Freakonomics", "The Accidental Superpower", - "Verbal Judo", - "Zero To One" + "The Martian" ], "books": { - "Fooled By Randomness": { - "filename": "fooledbyrandomness.jpg", - "link": "https://www.amazon.com/Fooled-Randomness-Hidden-Chance-Markets-dp-B006Q7VYC4/dp/B006Q7VYC4/ref=dp_ob_title_bk", - "review": "A lengthy compendium on probabilistic reasoning that helped kick off a curiosity of indefinite computation. There's more ancient philosophy than a book like this really needs but the occasional brazen punchline from the contemporary anecdotes make it bearable." + "Project Hail Mary": { + "filename": "hailmary.jpg", + "link": "https://www.amazon.com/Project-Hail-Mary-Andy-Weir/dp/0593135202", + "review": "This must be the first hard-sci-fi book I have read and I absolutely loved it. When I watched the movie I concluded that both executed on an inspiring story with an endearing tone but only the book held the creative practicality that really made it a favorite." + }, + "The Martian": { + "filename": "themartian.jpg", + "link": "https://www.amazon.com/Martian-Andy-Weir/dp/0553418025", + "review": "I picked this up after Project Hail Mary looking for more creative uses of hard scifi. On that account The Martian surpassed expectations - partially because the plot didn't have to contend with fantasy elements. I aspire to one day use a radioactive space heater as a hot tub power source, just as my hero, Mark Watney, once did." }, "The Rational Optimist": { "filename": "ratOpt.jpg", @@ -39,6 +42,11 @@ "link": "https://freakonomics.com/books/", "review": "More like the other Freakonomics books than I expected (cracked storytelling), which is still excellent, but I wished there was greater insights into seeing past conventional wisdom, which is what thinking like a freak means. Still a great book." }, + "Fooled By Randomness": { + "filename": "fooledbyrandomness.jpg", + "link": "https://www.amazon.com/Fooled-Randomness-Hidden-Chance-Markets-dp-B006Q7VYC4/dp/B006Q7VYC4/ref=dp_ob_title_bk", + "review": "A lengthy compendium on probabilistic reasoning that helped kick off a curiosity of indefinite computation. There's more ancient philosophy than a book like this really needs but the occasional brazen punchline from the contemporary anecdotes make it bearable." + }, "The Tyranny of Metrics": { "filename": "TyrannyOfMetrics.jpg", "link": "https://www.amazon.com/Tyranny-Metrics-Jerry-Z-Muller/dp/0691174954", @@ -144,11 +152,6 @@ "link": "https://www.goodreads.com/book/show/8034188-where-good-ideas-come-from", "review": "I got this book at a recycling center. I didn't want to read it or like it. Unfortnuately, it's pretty good. 200 pages of considerate review of how innovation comes to be + suggestions to expand the utility of your ideas (I've adopted several!)" }, - "Make Your Bed": { - "filename": "makeYourBed.jpg", - "link": "https://www.amazon.com/Make-Your-Bed-Little-Things/dp/1455570249", - "review": "Something small to read on a rainy day or flight. Valuable advice condensed into personal stories that stretch beyond anecdotes." - }, "12 Rules for Life": { "filename": "12RulesForLife.jpg", "link": "https://www.amazon.com/12-Rules-Life-Antidote-Chaos/dp/0345816021/", @@ -163,6 +166,11 @@ "filename": "HitchhikersGuideToTheGalaxy.jpeg", "link": "https://www.amazon.com/Hitchhikers-Guide-Galaxy-Douglas-Adams/dp/0345418913", "review": "It's alright. It felt like an aimless journey without defined boundaries that reveled in that fact for irony and wit points." + }, + "The Adventures of Sherlock Holmes": { + "filename": "sherlockholmes.jpg", + "link": "https://www.amazon.com/Adventures-Sherlock-Holmes-Arthur-Conan/dp/0141034332", + "review": "Had there not been half a dozen TV shows and movies about Sherlock Holmes all pulling from the same source material, I might have been intrigued. Instead, I felt like I was proofreading an old English fan rewrite of a book that I had already read. By today's standards, the outcomes were very telegraphed. Still enjoyed thinking through the deductions, though." } } } diff --git a/src/static/json/features.json b/src/static/json/features.json new file mode 100644 index 0000000..8bf140a --- /dev/null +++ b/src/static/json/features.json @@ -0,0 +1,16 @@ +{ + "homelab": { + "title": "AI Homelab and Self Hosted Web Tooling", + "tagline": "A self-hosted cluster serving Mixture-of-Experts models beyond their rated VRAM and independent web services", + "body": "

I refined a method to pool GPUs from different vendors and generations into a single inference backend of roughly 20 GB over Vulkan. FreeToken, a bleeding-edge MoE inference server, fronts the cluster and handles the elastic inference, running Mixture-of-Experts models with far more total parameters than the cluster's VRAM would normally allow. My own scripts handle KV caching and system-prompt compaction, which is what makes the setup harness-capable.

The same rig trains as well as it serves, having refined Qwen SLMs and diffusion image models. All this is hosted alongside my personal web assets, like this website. I voluntarily subject myself to the maintenance of production-grade systems on every level of the tech stack to ensure that I survive the Big Tech apocalypse.

", + "stack": ["FreeToken", "ComfyUI", "PEFT / TRL", "Docker", "Cloudflare", "Reverse Proxy"], + "image": "photos/deepdives/homelab-dashboard.png", + "caption": "Cluster dashboard during a long inference run." + }, + "capstone": { + "title": "Subatomic Particle Tracing and Anomaly Control", + "tagline": "My Master's Capstone: Building a Diffusion Cloud Chamber and using stereo reconstruction to classify Muons and Alpha/Beta particles.", + "body": "

My ongoing Data Science Capstone Project combines existing utilities for cheap quantum demonstrations and real world measuring techniques to create effective sensing of subatomic phenomena without breaking the bank or into the IAEA

", + "stack": ["Ego"] + } +} diff --git a/src/static/json/pages.json b/src/static/json/pages.json index 7fdbcd3..b4a8ff8 100755 --- a/src/static/json/pages.json +++ b/src/static/json/pages.json @@ -7,7 +7,7 @@ }, "status": { "template": "status.html", - "title":"Andrew Simonson - Status Page", + "title": "Andrew Simonson - Status Page", "description": "Status page for my services", "canonical": "/status" }, @@ -25,7 +25,7 @@ }, "duck": { "template": "duck.html", - "title":"You've been ducked!", + "title": "You've been ducked!", "description": "Face it, you've been ducked", "canonical": "/duck" }, @@ -38,31 +38,31 @@ "ecolab": { "title": "Data Scientist / Data Engineer — Ecolab", "date": "January 2024 – Present", - "content": "Data Scientist / Data Engineer
Ecolab · Saint Paul, MN

• Primary model developer of RushReady from inception through multimillion-dollar product commercialization
• Engineered near-real-time data platform with Delta Live Tables and automated MLOps lifecycle across QSR brands
• Built explainable anomaly detection with XGBoost/SHAP attribution and brand-configurable recommendations, increasing speed of service by 20%
• Represented Ecolab as liaison in Microsoft's 100-member AI accelerator program", + "content": "

Ecolab · Saint Paul, MN

", "classes": "experience" }, "rit_ms": { "title": "M.S. Data Science — RIT", "date": "Dec 2026 (expected)", - "content": "Master of Science in Data Science
Rochester Institute of Technology · Rochester, NY

• Focus on probability theory, statistical learning, and Bayesian methods
• Capstone: Fall 2026", + "content": "

Rochester Institute of Technology · Rochester, NY

", "classes": "education" }, "dow_chemical": { "title": "Data Engineer — Dow Chemical", "date": "January 2023 – May 2023", - "content": "Data Engineer
Dow Chemical · Freeport, TX

• Independently built reactive chemistry analysis Flask app encoding adiabatic correction and exotherm-detection logic across 9 test types
• Architected object-model abstraction, decoupling test-type parsers from report generation and replacing 4 legacy VBA tools
• Product became the interdepartmental data standard, saving >1,020 hours (~0.65 FTE) annually", + "content": "

Dow Chemical · Freeport, TX

", "classes": "experience" }, "rit_bs": { "title": "B.S. Computer Science & Data Science — RIT", "date": "Dec 2024", - "content": "B.S. in Computer Science · B.S. in Data Science (Dual Cluster)
Rochester Institute of Technology · Rochester, NY

• GPA 3.63 · Dean's List
• Minor in International Relations
• MicroMasters in Data Science – UC San Diego (edX)", + "content": "

Rochester Institute of Technology · Rochester, NY

", "classes": "education" }, "csh": { "title": "E-Board Member & Presenter — CSH", "date": "Aug 2021 – Present", - "content": "E-Board Member & Presenter
Computer Science House · Rochester, NY

• Presented seminars on web scraping, analytics, and GIS
• Built and maintained web services for 80+ members", + "content": "

Computer Science House · Rochester, NY

", "classes": "experience technical" } } diff --git a/src/static/json/projects.json b/src/static/json/projects.json index d547e96..6002baa 100755 --- a/src/static/json/projects.json +++ b/src/static/json/projects.json @@ -25,28 +25,28 @@ ] ] }, - "PsyCom - Physical Combinatorics": { + "PhysCom - Physical Combinatorics": { "status": "WIP", "classes": "programming", "content": "Experimental innovation engine operating on physical attributes and limitations of proven existing technologies with further AI review." }, "Antietam-Conococheague Watershed Monitoring": { "status": "complete", - "classes": "geospacial", + "classes": "geospatial", "bgi": "watershedTemps.png", - "content": "Live geospacial analysis of Maryland's Antietam and Conococheague sub-watersheds, monitoring water quality and temperatures through the summer months for governmental environment health review boards." + "content": "Live geospatial analysis of Maryland's Antietam and Conococheague sub-watersheds, monitoring water quality and temperatures through the summer months for governmental environment health review boards." }, "Automotive Brand Valuation Analysis": { "status": "complete", "classes": "programming", "bgi": "automotiveBrandAnalysis.png", - "content": "Brand valuation analysis of the used car market, measuring value decay by mileage to extrapolate qualities such as percieved reliability and persistent value of luxury features." + "content": "Brand valuation analysis of the used car market, measuring value decay by mileage to extrapolate qualities such as perceived reliability and persistent value of luxury features." }, "RIT Hotspots": { "status": "incomplete", - "classes": "pinned geospacial programming", + "classes": "pinned geospatial programming", "bgi": "hotspotsrit.png", - "content": "Live crowd migration map using RIT occupancy data. It seems RIT didn't like me exposing their surveilance state but since they didn't want to talk to me about it they instead changed the service response schema a few times. When that didn't stop me they just shut down the whole service. Nerds.", + "content": "Live crowd migration map using RIT occupancy data. It seems RIT didn't like me exposing their surveillance state but since they didn't want to talk to me about it they instead changed the service response schema a few times. When that didn't stop me they just shut down the whole service. Nerds.", "links": [ ["github", "https://github.com/asimonson1125/hotspotsrit", "git repo"] ] @@ -55,14 +55,14 @@ "status": "complete", "classes": "pinned programming", "bgi": "calorimeterAnalysis.png", - "content": "An analytical toolkit designed for reactive chemistry analysis, especially calorimetry. Works include automatic analysis, alerting unusual and dangerous results derived from a wide range of testing envrionments and equipment", + "content": "An analytical toolkit designed for reactive chemistry analysis, especially calorimetry. Works include automatic analysis, alerting unusual and dangerous results derived from a wide range of testing environments and equipment", "links": [] }, "Geography of Alternative Energy": { "status": "complete", - "classes": "pinned geospacial", + "classes": "pinned geospatial", "bgi": "energyGeography.png", - "content": "An ArcGIS geospacial analysis comparing the difference in effectiveness of wind, solar, and geothermal energy across the continental 48 United States.", + "content": "An ArcGIS geospatial analysis comparing the difference in effectiveness of wind, solar, and geothermal energy across the continental 48 United States.", "links": [ [ "globe", diff --git a/src/static/photos/books/hailmary.jpg b/src/static/photos/books/hailmary.jpg new file mode 100644 index 0000000..ef5a4c9 Binary files /dev/null and b/src/static/photos/books/hailmary.jpg differ diff --git a/src/static/photos/books/sherlockholmes.jpg b/src/static/photos/books/sherlockholmes.jpg new file mode 100644 index 0000000..1a5295d Binary files /dev/null and b/src/static/photos/books/sherlockholmes.jpg differ diff --git a/src/static/photos/books/themartian.jpg b/src/static/photos/books/themartian.jpg new file mode 100644 index 0000000..a32fd10 Binary files /dev/null and b/src/static/photos/books/themartian.jpg differ diff --git a/src/static/photos/extradimensional.avif b/src/static/photos/extradimensional.avif new file mode 100644 index 0000000..7911737 Binary files /dev/null and b/src/static/photos/extradimensional.avif differ diff --git a/src/static/photos/omnidimensional.avif b/src/static/photos/omnidimensional.avif new file mode 100644 index 0000000..5211022 Binary files /dev/null and b/src/static/photos/omnidimensional.avif differ diff --git a/src/static/photos/test.png b/src/static/photos/test.png new file mode 100644 index 0000000..370381e Binary files /dev/null and b/src/static/photos/test.png differ diff --git a/src/templates/about.html b/src/templates/about.html index c1d7078..89d8c49 100644 --- a/src/templates/about.html +++ b/src/templates/about.html @@ -1,211 +1,132 @@ {% block content %} -
-
-
-
-

About

-

Bio, timeline & credentials

-
+
+
+

About

+

+ I'm Andrew Simonson, a data scientist at Ecolab and a graduate data + science student at Rochester Institute of Technology, where I recently + finished a B.S. in Computer Science with a minor in international + relations and a focus on probability theory. +

+
+ +
+

+ At work I build the models and the pipelines behind them. At home I run + the servers. In between, I get bored and put things on this website, + self-built and self-hosted as a sandbox for + webdev experiments. This is what unprofessional development looks like. +

+

+ There is also a résumé for paper-minded people. +

-
-
- -
-

- I'm Andrew Simonson, - a Data Scientist at Ecolab and a graduate Data - Science student at - Rochester Institute of Technology, having - recently completed the Computer Science BS program - (international relations minor) with a focus on probability - theory. -
-
- I get bored and throw random stuff on this website.
- This is what unprofessional development looks like. -

-
-

- I also have a - resume - for unexplained reasons. -

-
+
+
+
+

Skills

+

Click a group to expand it

+
+ {% from 'partials/skills.html' import skills %} + {{ skills(var['skillList']) }} +
-
-

Career & Education Timeline

-
+
+
+

Certifications

+

+ Full list on LinkedIn +

+
- +
+

Data Science MicroMasters

+

UC San Diego on edX · + program certificate +

+ +
-
- {% for name, item in var['timeline'].items() %} -
-
-
- {{ item.date }} - {{ item.title }} -
-
- {{ item.content | safe }} -
- {% if item.classes %} -
- {% for cls in item.classes.split() %} - {{ cls | replace('_', ' ') | title }} - {% endfor %} -
- {% endif %} -
-
- {% endfor %} -
+ -
- -
- -
-

Skills & Certifications

-
- - -
+
+

Entrepreneurship

+

Rochester Institute of Technology

+ +
+
+ +
+
+

Timeline

+

+ + + +

+
+ +
    + {% for name, item in var['timeline'].items() %} +
  1. +

    {{ item.date }}

    +
    +

    {{ item.title }}

    +
    {{ item.content | safe }}
    +
    +
  2. + {% endfor %} +
+ +
{% endblock %} diff --git a/src/templates/books.html b/src/templates/books.html index 10cc9d4..e857b4d 100755 --- a/src/templates/books.html +++ b/src/templates/books.html @@ -1,48 +1,58 @@ {% block content %} -
-
-

"You can't judge a book by its cover but you can judge a person by their bookshelf" - Me, today.

-

My Favorites

-
- {% for i in var.books.selection %} - -

All Reviews

-

Hover to reveal review
Read to reveal KNOWLEDGE

-
- {% for i in var.books.books %} -
-
-
-
- {{ i }} cover -
+
+

{{ i }}

+

{{ var.books.books[i].review }}

+
-
-
-

{{ i }}

-

{{ var.books.books[i].review }}

-
- Book Source -
-
-
- {% endfor %} -
+ + {% endfor %} + +
{% endblock %} diff --git a/src/templates/duck.html b/src/templates/duck.html index a5ee302..1896032 100755 --- a/src/templates/duck.html +++ b/src/templates/duck.html @@ -1,11 +1,12 @@ {% block content %} -
-
-

What the duck?

+
+
+

What the duck?

+
mega spinney duck
diff --git a/src/templates/error.html b/src/templates/error.html index 22b23e2..57938a8 100755 --- a/src/templates/error.html +++ b/src/templates/error.html @@ -1,12 +1,9 @@ {% block content %} -
-
-
-

ERROR {{error}}

-

-

{{message}}

-
+
+

ERROR {{error}}

+

+

{{message}}

{% endblock %} diff --git a/src/templates/header.html b/src/templates/header.html index 3125692..7238bab 100755 --- a/src/templates/header.html +++ b/src/templates/header.html @@ -7,7 +7,7 @@ href="{{ url_for('static', filename='icons/withBackground.svg') }}" /> - + @@ -29,7 +29,7 @@ property="twitter:image" content="{{ url_for('static', filename='icons/rasterLogoCircle.png') }}" /> - + @@ -75,7 +75,6 @@ /> - {# #} {{ var['title'] }} @@ -83,50 +82,47 @@ {% block header %} -
+ -
-
-
-
-
- {# Andrew Simonoson #} -
-
- -
+
+ {% endblock %} {% block content%} -
{% include var['template'] %}
+
{% include var['template'] %}
{% endblock %} {% block footer %} -
-
{% include 'partials/socials.html' %}
+ {% endblock %}
diff --git a/src/templates/home.html b/src/templates/home.html index 494e90d..75d87d1 100755 --- a/src/templates/home.html +++ b/src/templates/home.html @@ -1,62 +1,123 @@ -{% block content %} {% macro nameplate() %} -
-

oh no.

-

I did not plan for visitors.

+{% block content %} +
+
+

Andrew Simonson

+

+ Data scientist at Ecolab and a graduate student at RIT. + I build end-to-end decision machines: from data pipelines that process scale, to epistemic models which understand, to user interfaces that convey. +

+ +
+
-


+
-
-
- Biblically Accurate Seraphim -
+
+
+

Now

+

September 2026

-
-{% endmacro %} +
+
+
At work
+
+ Primary model developer on RushReady at Ecolab: explainable anomaly + detection with XGBoost and SHAP, plus brand-configurable recommendations + for quick-service restaurants, all running on a near-real-time Delta + Live Tables platform. +
+
+
+
At school
+
+ M.S. in Data Science at Rochester Institute of Technology, focused on + probability theory, statistical learning, and Bayesian methods. + Capstone in fall 2026. +
+
+
+
At home
+
+ Running a two-GPU homelab + that serves Mixture-of-Experts models to coding agents over local + endpoints, and using the same rig to fine-tune small models. +
+
+
+
On the shelf
+
+
    + {% for title in var.books.selection[:5] %} + {% set book = var.books.books[title] %} +
  • + + {{ title }} + +
  • + {% endfor %} +
+ All reviews +
+
+
+ -
-
-
{{ nameplate() }}
- -
-
- Pepsi Addict - Secret Message - They took my blood but it wasn't DNA, it was USA - Bob the Builder gif -
-
- You've reached the website for Andrew Simonson's digital shenanigans. -

Now What?

-

- Go back and find the link that I originally shared. Or poke around. Be your own person.
- I'll grant myself some titles while I'm at it: -

-
    -
  • Wicked Wizard of the West
  • -
  • Enemy of Node.js, Hater of Bloat
  • -
  • Load-Bearing Coconut
  • -
  • Creator and Harnesser of Energy
  • -
-
-
- {# -
-
- {% from 'partials/chess.html' import chess %} {{ - chess('asimonson1125') }} -
-
-
- stabby -
-
-
- #} +
+
+

Selected work

+

Everything

- {% endblock %} +
    + {% for id, f in var.features.items() %} +
  1. + {{ f.title }} + {{ f.summary or f.tagline }} +
  2. + {% endfor %} +
+ +
+
+

What now?

+
+
+
+

+ I wasn't really expecting anyone to read this far. + If you're looking for shared resources you've taken a wrong turn. +

+ From here, we'll rekindle the future. +

+

+ Pepsi Addict + If you can read this you are too close to the monitor + They took my blood but it wasn't DNA, it was USA + Bob the Builder: can we build it? +

+
+
+ Seraphim over a globe +
+
+
+
+
+{% endblock %} diff --git a/src/templates/partials/feature_project.html b/src/templates/partials/feature_project.html index f3fc928..e028328 100644 --- a/src/templates/partials/feature_project.html +++ b/src/templates/partials/feature_project.html @@ -1,30 +1,32 @@ -{% macro feature_project(id, title, tagline, body, stack, image=none) %} -
-
- -
-
-
- {{ body | safe }} -
- {% if stack %} -
- {% for item in stack %} - {{ item }} - {% endfor %} -
- {% endif %} -
-
+{% macro feature_project(id, title, tagline, body, stack, image=none, caption='') %} +
+
+

{{ title }}

+

{{ tagline }}

-
+ {% if image %} +
+ {{ caption or title }} + {% if caption %}
{{ caption }}
{% endif %} +
+ {% endif %} +
+
+ {{ body | safe }} +
+ {% if stack %} +

Built with {{ stack | join(', ') }}

+ {% endif %} +
+ + {% endmacro %} diff --git a/src/templates/partials/project.html b/src/templates/partials/project.html index 1dc3065..a43f0cb 100755 --- a/src/templates/partials/project.html +++ b/src/templates/partials/project.html @@ -1,39 +1,27 @@ {% macro project(title, classes, status, bgi, content, links) %} -
-
+{% set tags = classes.split() | reject('equalto', 'pinned') | list %} +
  • +
    {% if bgi|length > 0 %} - {% set path = url_for('static', filename='photos/projects/' + bgi) %} - Ref image for {{ title }} project + {% else %} -
    + {% endif %}
    -
    -
    -

    {{ title }}

    - {{ status }} -
    - {% if classes %} -
    - {% for cls in classes.split() %} - {% if cls != 'pinned' %}{{ cls }}{% endif %} - {% endfor %} -
    - {% endif %} -

    {{ content }}

    +
    +

    {{ title }}

    +

    + {{ status }} + {% if tags %} {{ tags | join(', ') }}{% endif %} +

    +

    {{ content }}

    {% if links %} - +

    {% endif %}
    -
    +
  • {% endmacro %} diff --git a/src/templates/projects.html b/src/templates/projects.html index ad6e823..25bc9d3 100755 --- a/src/templates/projects.html +++ b/src/templates/projects.html @@ -1,49 +1,33 @@ {% block content %} -
    -
    -
    -
    -

    Work

    -

    Projects & experiments

    +
    +
    +

    Work

    +

    + Research, tooling, and experiments. Sometimes I forget to add things to this list, message me for missing details. +

    +
    + +
    +
    +

    Highlights

    -
    -
    -
    - - -

    Highlights

    -
    - {% from 'partials/feature_project.html' import feature_project %} - {{ feature_project( - 'homelab', - 'AI Homelab', - 'Self-hosted GPU cluster serving MoE models beyond their rated VRAM, with a full LoRA fine-tuning pipeline on top', - '

    I\'ve refined a method to pool GPUs from different vendors and generations — an RTX 5070 and a Radeon RX 6600 XT — into a single ~20GB inference backend over Vulkan, so neither card\'s limits bottleneck the other. FreeToken, a bleeding-edge MoE inference server, fronts the cluster and handles the actual elastic inference, running Mixture-of-Experts models with far more total parameters than the cluster\'s VRAM would normally allow. My custom scripts resolve KV caching and system prompt compaction that make the setup harness-capable, so agentic tools like Claude Code can hit it over Anthropic- and OpenAI-compatible endpoints exactly like they would a paid API. GPU layer placement alone took generation from ~150s a call to ~88ms/token.

    The same rig trains as well as it serves. I built a complete LoRA fine-tuning pipeline on it — base model, training config, and adapter. Used for fine-tuning Qwen3-1.7B on patent data, and training a Stable Diffusion LoRA set on curated, deduplicated image data.

    ', - ['RTX 5070', 'RX 6600 XT', 'Ollama', 'FreeToken', 'ComfyUI', 'PEFT / TRL'], - 'photos/deepdives/homelab-dashboard.png' - ) }} - - {{ feature_project( - 'siteinfra', - 'Self-Hosted Web Tooling & Maintenance', - 'I designed, deployed, and still actively operate this site\'s stack — the app, the edge, and its own health monitoring', - '

    This portfolio runs on infrastructure I designed, deployed, and still operate myself — the Flask app and its container, the Cloudflare edge and reverse proxy in front of it, DNS and tunneling, and the uptime monitor watching all of it (live at /status). None of that is a managed host\'s job here; it\'s mine.

    What actually proves it\'s production-capable is the maintenance loop, not the launch. I put it through a real adversarial review periodically — most recently a live pass with Claude Code against both the deployed site and the source — and fix what it finds. The latest pass closed two real issues same-day: a database-query pattern on an internal endpoint that could be hammered into needless load, and a self-built redirect that could momentarily hand a browser plaintext HTTP instead of HTTPS. Nothing catastrophic came up — no injection, no auth bypass, no data exposure — which is exactly the point of doing this on a schedule instead of waiting for something to break.

    ', - ['Flask', 'Docker', 'PostgreSQL', 'Reverse Proxy', 'Cloudflare'] - ) }} -
    - - -

    All Projects

    -
    - {% from 'partials/project.html' import project %} {% for i in - var["projects"] %} {{ project(i, var["projects"][i]["classes"], - var["projects"][i]["status"], var["projects"][i]["bgi"], - var["projects"][i]["content"], var["projects"][i]["links"]) }} {% endfor - %} -
    + {% from 'partials/feature_project.html' import feature_project %} + {% for id, f in var.features.items() %} + {{ feature_project(id, f.title, f.tagline, f.body, f.stack, f.image, f.caption) }} + {% endfor %} + +
    +
    +

    All projects

    +

    {{ var.projects | length }} entries

    -
    +
      + {% from 'partials/project.html' import project %} + {% for title, item in var.projects.items() %} + {{ project(title, item.classes, item.status, item.bgi, item.content, item.links) }} + {% endfor %} +
    +
    - {% endblock %} diff --git a/src/templates/status.html b/src/templates/status.html index 30f459c..95ac4f0 100644 --- a/src/templates/status.html +++ b/src/templates/status.html @@ -1,100 +1,80 @@ {% block content %} -
    -
    -

    Service Status Monitor

    +
    +
    +

    Status

    +

    + Uptime for the services I host myself, checked every minute from the + server. This page refreshes itself on the same schedule. +

    +
    -
    -
    +
    +
    +
    - Last checked: Loading... -
    - Next check: -- -
    - -
    -
    - -

    Status Legend

    -
    -
    - - Operational (response successful) -
    -
    - - Degraded (timeout or errors) -
    -
    - - Offline (unreachable) +

    Checking systems

    +

    Loading service status

    -
    +
    +
    +
    Online
    +
    -- of --
    +
    +
    +
    Last checked
    +
    Loading
    +
    +
    +
    Next check
    +
    --
    +
    +
    + + - -
    -
    -
    - -
    -

    Checking Systems...

    -
    Loading service status
    -
    -
    -
    -
    - -- - Online -
    -
    - -- - Total -
    -
    +
    +
    +

    Services

    +

    Uptime over 24 hours, 7 days, 30 days, and all time

    -
    - - - -
    -

    About This Monitor

    -
      -
    • Check Frequency: Services are checked automatically every minute from the server
    • -
    • Page Refresh: This page auto-refreshes every minute to show latest data
    • -
    -
    +
    + + + + + + + + + + + + + {% for service in var.services %} + + + + + + + + + {% endfor %} + +
    ServiceStateResponseCodeChecksUptime
    {{ service.name }} + + + Loading + + ------Loading
    +
    +

    + operational   + degraded or timed out   + unreachable +

    +