From 1a2fde4a542e9714b339042df88f62aedd57704e Mon Sep 17 00:00:00 2001 From: Yuval Grimberg Date: Sun, 8 Nov 2026 23:07:07 +0200 Subject: [PATCH] site: make every interactive part an explicit state machine Interactive state was spread across booleans, timers, classes and DOM nodes, which caused races and stale UI. A small helper (assets/js/state-machine.js) gives each part one named state with guarded transitions, mirrored onto -state> and listed by PortfolioState.snapshot(). - Overlay stack: Escape closes only the top overlay, one focus trap at a time - Fix confirmed bugs in the drawer, Clippy, labs, sims, copy and contact flows - docs/site/STATE.md documents every machine; new smoke test covers the fixes Co-Authored-By: Claude Opus 5.5 --- .github/copilot-instructions.md | 1 + README.md | 2 + assets/css/output.css | 2 +- assets/js/fly-brain.js | 39 ++++- assets/js/index-page.js | 263 ++++++++++++++++++++------------ assets/js/lab-experiments.js | 25 ++- assets/js/path-playground.js | 71 +++++++-- assets/js/portfolio-labs.js | 86 ++++++++--- assets/js/site-shell.js | 61 +++++--- assets/js/state-machine.js | 152 ++++++++++++++++++ docs/README.md | 2 +- docs/site/STATE.md | 70 +++++++++ fly101.html | 3 +- index.html | 19 +-- photo.html | 3 +- tests/smoke.spec.js | 73 +++++++++ 16 files changed, 689 insertions(+), 183 deletions(-) create mode 100644 assets/js/state-machine.js create mode 100644 docs/site/STATE.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index db55506ee5..4da7609170 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,6 +14,7 @@ - Homepage behavior: `assets/js/index-page.js` - Photography page behavior: `assets/js/photo-page.js` - Lab loader and navigation: `assets/js/portfolio-labs.js` (lazy-imports `path-playground.js` and `lab-experiments.js`, which pull in three.js) +- Interactive state: `assets/js/state-machine.js`. Every interactive part is an explicit machine mirrored to `-state>`; `PortfolioState.snapshot()` returns them all. Read and extend the table in `docs/site/STATE.md` instead of adding booleans, timers or your own Escape/focus-trap handlers. ## CSS Layout diff --git a/README.md b/README.md index 0d3ebcd86c..6125e2043f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ This repo is not React and is not driven by Jekyll templates in its current main - `index.html` and `photo.html` are the main live surfaces. - Shared shell behavior lives in `assets/js/site-shell.js`. +- Interactive state lives in explicit state machines (`assets/js/state-machine.js`), mirrored to `-state>`. See `docs/site/STATE.md` for the full table and the rules. - Page-specific behavior lives in: - `assets/js/index-page.js` - `assets/js/photo-page.js` @@ -107,6 +108,7 @@ assets/ lab-experiments.js lab-models.mjs fly-brain.js + state-machine.js vendor/ clippyjs/ three/ diff --git a/assets/css/output.css b/assets/css/output.css index a7b0c74a79..585ff2bc0c 100644 --- a/assets/css/output.css +++ b/assets/css/output.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--leading-tight:1.25;--leading-relaxed:1.625;--radius-lg:.5rem;--radius-2xl:1rem;--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--animate-bounce:bounce 1s infinite;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-tech-dark:#05070a;--color-tech-cyan:#58c7bc;--color-tech-purple:#d8aa4f;--color-tech-green:#9bd66b;--color-tech-pink:#ff766d;--font-display:"Bahnschrift","Aptos Display","Segoe UI Variable","Segoe UI",system-ui,sans-serif;--font-tech:"Cascadia Mono","JetBrains Mono",Consolas,Monaco,monospace;--animate-fade-in:fadeIn .6s ease-in-out;--animate-float:float 6s ease-in-out infinite}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing)*0)}.-top-10{top:calc(var(--spacing)*-10)}.top-0{top:calc(var(--spacing)*0)}.top-1\/2{top:50%}.-right-10{right:calc(var(--spacing)*-10)}.right-0{right:calc(var(--spacing)*0)}.-bottom-1{bottom:calc(var(--spacing)*-1)}.-bottom-10{bottom:calc(var(--spacing)*-10)}.bottom-8{bottom:calc(var(--spacing)*8)}.-left-10{left:calc(var(--spacing)*-10)}.left-0{left:calc(var(--spacing)*0)}.left-1\/2{left:50%}.left-5{left:calc(var(--spacing)*5)}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-5{margin-top:calc(var(--spacing)*5)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-8{margin-top:calc(var(--spacing)*8)}.mt-10{margin-top:calc(var(--spacing)*10)}.mt-12{margin-top:calc(var(--spacing)*12)}.mt-14{margin-top:calc(var(--spacing)*14)}.mt-20{margin-top:calc(var(--spacing)*20)}.mt-auto{margin-top:auto}.mr-2{margin-right:calc(var(--spacing)*2)}.mr-3{margin-right:calc(var(--spacing)*3)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-5{margin-bottom:calc(var(--spacing)*5)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.mb-10{margin-bottom:calc(var(--spacing)*10)}.mb-12{margin-bottom:calc(var(--spacing)*12)}.mb-16{margin-bottom:calc(var(--spacing)*16)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-0\.5{height:calc(var(--spacing)*.5)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-10{height:calc(var(--spacing)*10)}.h-11{height:calc(var(--spacing)*11)}.h-16{height:calc(var(--spacing)*16)}.h-24{height:calc(var(--spacing)*24)}.h-32{height:calc(var(--spacing)*32)}.h-72{height:calc(var(--spacing)*72)}.h-96{height:calc(var(--spacing)*96)}.h-full{height:100%}.max-h-56{max-height:calc(var(--spacing)*56)}.min-h-11{min-height:calc(var(--spacing)*11)}.min-h-\[20px\]{min-height:20px}.min-h-\[72svh\]{min-height:72svh}.min-h-\[84svh\]{min-height:84svh}.w-0{width:calc(var(--spacing)*0)}.w-1{width:calc(var(--spacing)*1)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-6{width:calc(var(--spacing)*6)}.w-11{width:calc(var(--spacing)*11)}.w-16{width:calc(var(--spacing)*16)}.w-72{width:calc(var(--spacing)*72)}.w-96{width:calc(var(--spacing)*96)}.w-\[calc\(100vw-4rem\)\]{width:calc(100vw - 4rem)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-fade-in{animation:var(--animate-fade-in)}.animate-float{animation:var(--animate-float)}.animate-pulse{animation:var(--animate-pulse)}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-5{gap:calc(var(--spacing)*5)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}.gap-10{gap:calc(var(--spacing)*10)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing)*6)}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-800{border-color:var(--color-gray-800)}.border-tech-cyan\/20{border-color:#58c7bc33}@supports (color:color-mix(in lab, red, red)){.border-tech-cyan\/20{border-color:color-mix(in oklab,var(--color-tech-cyan)20%,transparent)}}.border-tech-cyan\/30{border-color:#58c7bc4d}@supports (color:color-mix(in lab, red, red)){.border-tech-cyan\/30{border-color:color-mix(in oklab,var(--color-tech-cyan)30%,transparent)}}.border-tech-cyan\/40{border-color:#58c7bc66}@supports (color:color-mix(in lab, red, red)){.border-tech-cyan\/40{border-color:color-mix(in oklab,var(--color-tech-cyan)40%,transparent)}}.border-transparent{border-color:#0000}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.border-white\/15{border-color:#ffffff26}@supports (color:color-mix(in lab, red, red)){.border-white\/15{border-color:color-mix(in oklab,var(--color-white)15%,transparent)}}.border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab, red, red)){.border-white\/20{border-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab, red, red)){.bg-black\/30{background-color:color-mix(in oklab,var(--color-black)30%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-gray-600{background-color:var(--color-gray-600)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800\/60{background-color:#1e293999}@supports (color:color-mix(in lab, red, red)){.bg-gray-800\/60{background-color:color-mix(in oklab,var(--color-gray-800)60%,transparent)}}.bg-tech-cyan{background-color:var(--color-tech-cyan)}.bg-tech-cyan\/10{background-color:#58c7bc1a}@supports (color:color-mix(in lab, red, red)){.bg-tech-cyan\/10{background-color:color-mix(in oklab,var(--color-tech-cyan)10%,transparent)}}.bg-tech-dark{background-color:var(--color-tech-dark)}.bg-tech-dark\/60{background-color:#05070a99}@supports (color:color-mix(in lab, red, red)){.bg-tech-dark\/60{background-color:color-mix(in oklab,var(--color-tech-dark)60%,transparent)}}.bg-tech-dark\/95{background-color:#05070af2}@supports (color:color-mix(in lab, red, red)){.bg-tech-dark\/95{background-color:color-mix(in oklab,var(--color-tech-dark)95%,transparent)}}.bg-tech-green{background-color:var(--color-tech-green)}.bg-tech-purple{background-color:var(--color-tech-purple)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white)60%,transparent)}}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-cyan{--tw-gradient-from:var(--color-tech-cyan);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-dark{--tw-gradient-from:var(--color-tech-dark);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-green{--tw-gradient-from:var(--color-tech-green);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-purple{--tw-gradient-from:var(--color-tech-purple);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-purple\/10{--tw-gradient-from:#d8aa4f1a}@supports (color:color-mix(in lab, red, red)){.from-tech-purple\/10{--tw-gradient-from:color-mix(in oklab,var(--color-tech-purple)10%,transparent)}}.from-tech-purple\/10{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.via-tech-dark{--tw-gradient-via:var(--color-tech-dark);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-cyan{--tw-gradient-to:var(--color-tech-cyan);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-cyan\/10{--tw-gradient-to:#58c7bc1a}@supports (color:color-mix(in lab, red, red)){.to-tech-cyan\/10{--tw-gradient-to:color-mix(in oklab,var(--color-tech-cyan)10%,transparent)}}.to-tech-cyan\/10{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-dark{--tw-gradient-to:var(--color-tech-dark);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-green{--tw-gradient-to:var(--color-tech-green);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-pink{--tw-gradient-to:var(--color-tech-pink);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-7{padding-inline:calc(var(--spacing)*7)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.py-12{padding-block:calc(var(--spacing)*12)}.py-16{padding-block:calc(var(--spacing)*16)}.py-20{padding-block:calc(var(--spacing)*20)}.py-24{padding-block:calc(var(--spacing)*24)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-20{padding-top:calc(var(--spacing)*20)}.pt-24{padding-top:calc(var(--spacing)*24)}.pb-16{padding-bottom:calc(var(--spacing)*16)}.pb-24{padding-bottom:calc(var(--spacing)*24)}.text-center{text-align:center}.text-left{text-align:left}.font-display{font-family:var(--font-display)}.font-mono{font-family:var(--font-mono)}.font-tech{font-family:var(--font-tech)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.break-words{overflow-wrap:break-word}.whitespace-pre-wrap{white-space:pre-wrap}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-tech-cyan{color:var(--color-tech-cyan)}.text-tech-dark{color:var(--color-tech-dark)}.text-tech-green{color:var(--color-tech-green)}.text-tech-pink{color:var(--color-tech-pink)}.text-tech-purple{color:var(--color-tech-purple)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.opacity-10{opacity:.1}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.mix-blend-multiply{mix-blend-mode:multiply}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-red-500{--tw-ring-color:var(--color-red-500)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-xl{--tw-blur:blur(var(--blur-xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}@media (hover:hover){.group-hover\:w-full:is(:where(.group):hover *){width:100%}.group-hover\:translate-y-1:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:scale-105:is(:where(.group):hover *),.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-tech-cyan:hover{border-color:var(--color-tech-cyan)}.hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-tech-cyan:hover{background-color:var(--color-tech-cyan)}.hover\:bg-tech-cyan\/10:hover{background-color:#58c7bc1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tech-cyan\/10:hover{background-color:color-mix(in oklab,var(--color-tech-cyan)10%,transparent)}}.hover\:bg-tech-cyan\/20:hover{background-color:#58c7bc33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tech-cyan\/20:hover{background-color:color-mix(in oklab,var(--color-tech-cyan)20%,transparent)}}.hover\:bg-tech-green\/20:hover{background-color:#9bd66b33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tech-green\/20:hover{background-color:color-mix(in oklab,var(--color-tech-green)20%,transparent)}}.hover\:bg-tech-purple\/20:hover{background-color:#d8aa4f33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tech-purple\/20:hover{background-color:color-mix(in oklab,var(--color-tech-purple)20%,transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.hover\:from-tech-cyan:hover{--tw-gradient-from:var(--color-tech-cyan);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.hover\:from-tech-pink:hover{--tw-gradient-from:var(--color-tech-pink);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.hover\:to-tech-green:hover{--tw-gradient-to:var(--color-tech-green);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.hover\:to-tech-purple:hover{--tw-gradient-to:var(--color-tech-purple);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.hover\:text-tech-cyan:hover{color:var(--color-tech-cyan)}.hover\:text-tech-green:hover{color:var(--color-tech-green)}.hover\:text-tech-purple:hover{color:var(--color-tech-purple)}.hover\:text-white:hover{color:var(--color-white)}}@media (min-width:40rem){.sm\:mb-6{margin-bottom:calc(var(--spacing)*6)}.sm\:mb-8{margin-bottom:calc(var(--spacing)*8)}.sm\:mb-12{margin-bottom:calc(var(--spacing)*12)}.sm\:min-h-screen{min-height:100vh}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}:where(.sm\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}.sm\:p-6{padding:calc(var(--spacing)*6)}.sm\:px-4{padding-inline:calc(var(--spacing)*4)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:px-8{padding-inline:calc(var(--spacing)*8)}.sm\:py-4{padding-block:calc(var(--spacing)*4)}.sm\:py-16{padding-block:calc(var(--spacing)*16)}.sm\:pt-20{padding-top:calc(var(--spacing)*20)}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}@media (min-width:48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:py-24{padding-block:calc(var(--spacing)*24)}.md\:pt-28{padding-top:calc(var(--spacing)*28)}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.md\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.md\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.md\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[0\.9fr_1\.1fr\]{grid-template-columns:.9fr 1.1fr}.lg\:grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.lg\:gap-8{gap:calc(var(--spacing)*8)}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}.lg\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.lg\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.lg\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}}}.gradient-text{color:#d8aa4f;-webkit-text-fill-color:currentColor}.glass-effect{background:linear-gradient(#f6f1e811,#f6f1e809),#0b0f12c7;border:1px solid #f6f1e81f;box-shadow:0 18px 48px #02050847}.glass-effect.rounded-2xl,.project-card.rounded-2xl,.cv-gate.rounded-2xl,.glass-effect.rounded-xl{border-radius:.5rem}.skip-link{z-index:60;color:#fff;background:#05070af2;border:1px solid #38d8c873;border-radius:.75rem;padding:.75rem 1rem;font-weight:600;transition:transform .2s;position:absolute;top:1rem;left:1rem;transform:translateY(-200%)}.skip-link:focus{transform:translateY(0)}h1,h2,h3{letter-spacing:0;text-wrap:balance;font-family:Bahnschrift,Aptos Display,Segoe UI Variable,system-ui,sans-serif}.btn{color:#fff;background-color:#1a202c;border-radius:5px;padding:10px 20px;text-decoration:none;transition:background-color .3s,transform .3s}.btn:hover{background-color:#4a5568;transform:scale(1.05)}.gradient-bg{color:#fff;background:linear-gradient(135deg,#1a202c,#366ecd)}social-links a{color:#fff;margin:0 10px;transition:color .3s}social-links a:hover{color:gold}social-links{z-index:2000;position:relative}#goose-animation{z-index:1000}h1{color:#f6f1e8}body{color:#f6f1e8;background:linear-gradient(90deg,#f6f1e806 1px,#0000 1px) 0 0/40px 40px,linear-gradient(#f6f1e805 1px,#0000 1px) 0 0/40px 40px,linear-gradient(#05070a 0%,#0c1114 44%,#070908 100%)}html{color-scheme:dark;scroll-behavior:smooth}.touch-target{touch-action:manipulation;min-width:46px;min-height:46px}.anchor-section{scroll-margin-top:5.5rem}.mobile-menu-button-open{z-index:70;position:fixed;top:1rem;right:calc(min(24rem,100vw - 4rem) + .5rem);translate:none}#mobile-menu{z-index:60}.project-action-link{border-radius:.75rem;align-items:center;gap:.35rem;min-height:44px;padding:.5rem .75rem;display:inline-flex}.project-icon-link,.footer-social-link{touch-action:manipulation;border-radius:9999px;justify-content:center;align-items:center;min-width:44px;min-height:44px;display:inline-flex}.footer-link-list a{touch-action:manipulation;align-items:center;min-width:44px;min-height:44px;display:inline-flex}.drawer-scroll-region{overscroll-behavior:contain}.hero-photo-bg{background-position:50%;background-repeat:no-repeat;background-size:cover}.hero-signal-grid{opacity:.42;background-color:#0000;background-image:linear-gradient(#38d8c817 1px,#0000 1px),linear-gradient(90deg,#f2b84b12 1px,#0000 1px);background-position:0 0,0 0;background-repeat:repeat,repeat;background-size:72px 72px;background-attachment:scroll,scroll;background-origin:padding-box,padding-box;background-clip:border-box,border-box;-webkit-mask-image:linear-gradient(90deg,#000 0% 56%,#0000 100%);mask-image:linear-gradient(90deg,#000 0% 56%,#0000 100%)}.hire-command-center{background:linear-gradient(135deg,#05070af7,#0e1214ed)}.hire-grid-plane{opacity:.34;background-color:#0000;background-image:linear-gradient(#9bd66b14 1px,#0000 1px),linear-gradient(90deg,#38d8c814 1px,#0000 1px);background-position:0 0,0 0;background-repeat:repeat,repeat;background-size:44px 44px;background-attachment:scroll,scroll;background-origin:padding-box,padding-box;background-clip:border-box,border-box;position:absolute;inset:0;-webkit-mask-image:linear-gradient(#0000,#000 12% 86%,#0000);mask-image:linear-gradient(#0000,#000 12% 86%,#0000)}.hire-role-tabs{grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem;display:grid}.hire-role-tab,.hire-command-button,.hire-contact-link,.roast-button,.retro-link-button,.retro-game-action,.contact-primary-button,.contact-email-strip button,.cv-unlock-button,.cv-public-link,.cv-secondary-link{touch-action:manipulation;border-radius:9999px;justify-content:center;align-items:center;font-weight:900;display:inline-flex}.hire-role-tab{color:#d1d5db;background:#ffffff0e;border:1px solid #ffffff21;min-height:48px;transition:transform .18s,border-color .18s,background-color .18s,color .18s}.hire-role-tab:hover,.hire-role-tab:focus-visible,.hire-role-tab.is-active{color:#f6f1e8;background:#2dd4bf29;border-color:#2dd4bf94;transform:translateY(-1px)}.hire-console{background:#080b0deb;border:1px solid #58c7bc42;border-radius:.5rem;overflow:hidden;box-shadow:0 28px 80px #02050861}.hire-console-header,.hire-console-actions{border-bottom:1px solid #ffffff1a;justify-content:space-between;align-items:center;gap:1rem;padding:1rem 1.25rem;display:flex}.hire-console-actions{border-top:1px solid #ffffff1a;border-bottom:0;flex-wrap:wrap}.hire-status-dot{background:#84cc16;border-radius:9999px;width:.7rem;height:.7rem;box-shadow:0 0 20px #84cc16db}.hire-console-body{padding:clamp(1.25rem,3vw,2rem)}.hire-metric{background:#ffffff0e;border:1px solid #ffffff1a;border-radius:.5rem;min-height:6.5rem;padding:1rem}.hire-metric-label{color:#94a3b8;letter-spacing:.12em;text-transform:uppercase;margin-bottom:.45rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;display:block}.hire-metric strong{color:#f6f1e8;line-height:1.25}.hire-proof-list{color:#d1d5db;gap:.8rem;display:grid}.hire-proof-list li{padding-left:1.5rem;position:relative}.hire-proof-list li:before{content:">";color:#84cc16;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-weight:900;position:absolute;left:0}.hire-command-button{color:#2dd4bf;background:#2dd4bf29;border:1px solid #2dd4bf61;min-height:46px;padding:.75rem 1rem}.hire-contact-link{color:#05070a;background:#f2b84b;min-height:46px;padding:.75rem 1rem}.retro-archive-section{background:linear-gradient(90deg,#d8aa4f0e 1px,#0000 1px) 0 0/54px 54px,linear-gradient(#58c7bc0b 1px,#0000 1px) 0 0/54px 54px,linear-gradient(135deg,#05070afa,#0d1117f0)}.retro-archive-header{grid-template-columns:minmax(0,1fr) minmax(18rem,.42fr);align-items:end;gap:2rem;margin-bottom:2.5rem;display:grid}.retro-archive-note,.retro-request-card,.retro-policy-card,.retro-game-card{background:#f6f1e80d;border:1px solid #ffffff1c;border-radius:.5rem}.retro-archive-note{color:#d1d5db;padding:1.25rem;line-height:1.55}.retro-policy-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:.8rem;margin-bottom:3rem;display:grid}.retro-policy-card{min-height:5.5rem;padding:1rem}.retro-policy-value{color:#f2b84b;font-size:1.75rem;font-weight:900;line-height:1;display:block}.retro-policy-label{color:#9ca3af;letter-spacing:.08em;text-transform:uppercase;margin-top:.45rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;display:block}.retro-section-title{justify-content:space-between;align-items:end;gap:2rem;margin-bottom:1.25rem;display:flex}.retro-section-title h3{color:#f6f1e8;margin-top:.35rem;font-size:clamp(1.55rem,2.4vw,2.35rem);font-weight:900}.retro-section-title>p{color:#9ca3af;max-width:30rem}.retro-catalog-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem;display:grid}.retro-game-card{grid-template-rows:auto auto auto 1fr auto;gap:1rem;min-height:23rem;padding:1.25rem;transition:transform .18s,border-color .18s,background-color .18s;display:grid}.retro-game-card:hover,.retro-game-card:focus-within{background:#f6f1e812;border-color:#38d8c86b;transform:translateY(-3px)}.retro-card-topline{justify-content:space-between;align-items:center;gap:.75rem;display:flex}.retro-card-icon{background:#05070a75;border:1px solid #ffffff1f;border-radius:.5rem;place-items:center;width:3rem;height:3rem;font-size:1.65rem;display:inline-grid}.retro-source-pill{color:#38d8c8;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;background:#38d8c81a;border:1px solid #38d8c852;border-radius:9999px;align-items:center;min-height:1.75rem;padding:.28rem .62rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.68rem;font-weight:800;display:inline-flex}.retro-game-card h3{color:#f6f1e8;font-size:1.35rem;font-weight:900}.retro-game-card p{color:#d1d5db;font-size:.94rem;line-height:1.62}.retro-card-meta{gap:.55rem;display:grid}.retro-card-meta div{grid-template-columns:4.3rem minmax(0,1fr);align-items:start;gap:.75rem;display:grid}.retro-card-meta dt{color:#9ca3af;letter-spacing:.08em;text-transform:uppercase;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.68rem}.retro-card-meta dd{color:#f6f1e8;font-size:.84rem}.retro-link-button,.retro-game-action{color:#38d8c8;background:#38d8c829;border:1px solid #38d8c861;gap:.45rem;min-height:46px;padding:.75rem 1rem}.retro-link-button:hover,.retro-link-button:focus-visible,.retro-game-action:hover,.retro-game-action:focus-visible{color:#f6f1e8;background:#38d8c83d}.retro-game-action{width:100%}.retro-request-card{grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:1.5rem;padding:1.25rem;display:grid}.retro-request-card h3{color:#f6f1e8;margin-bottom:.35rem;font-size:1.65rem;font-weight:900}.retro-request-card p{color:#d1d5db}.contact-command-section{background:linear-gradient(90deg,#f2b84b0d 1px,#0000 1px) 0 0/64px 64px,linear-gradient(#38d8c80f 1px,#0000 1px) 0 0/64px 64px,#05070a}.contact-hero-email{background:#05070ab8;border:1px solid #f2b84b61;border-radius:.5rem;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:1rem;max-width:58rem;margin:-1.25rem auto 2rem;padding:.9rem 1rem;display:grid}.contact-hero-email span,.contact-hero-email small{color:#9ca3af;letter-spacing:.1em;text-transform:uppercase;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;font-weight:900}.contact-hero-email a{color:#f6f1e8;overflow-wrap:anywhere;font-weight:900}.contact-hero-email a:hover,.contact-hero-email a:focus-visible{color:#f2b84b}.contact-command-grid{grid-template-columns:minmax(0,.95fr) minmax(0,1.05fr);align-items:start;gap:1rem;display:grid}.contact-direct-panel,.contact-draft-card,.contact-support-panel,.cv-gate{border-radius:.5rem;padding:clamp(1.25rem,2.6vw,2rem)}.contact-direct-panel{min-height:100%}.contact-direct-panel h3,.contact-draft-card h3,.contact-support-panel h3,.cv-gate h3{color:#f6f1e8;margin-bottom:.75rem;font-size:clamp(1.45rem,2vw,2rem);font-weight:900}.contact-direct-copy,.contact-card-header p,.contact-support-panel p,.cv-gate p{color:#d1d5db;line-height:1.65}.contact-email-strip{background:#f2b84b1a;border:1px solid #f2b84b6b;border-radius:.5rem;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:.85rem;margin:1.5rem 0 .4rem;padding:.85rem;display:grid}.contact-email-strip span:first-child{color:#f2b84b;letter-spacing:.1em;text-transform:uppercase;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;font-weight:900}.contact-email-strip strong{color:#f6f1e8;overflow-wrap:anywhere;min-width:0}.contact-email-strip button,.contact-primary-button{min-height:46px;padding:.72rem 1rem}.contact-email-strip button{color:#f6f1e8;background:#05070a94;border:1px solid #f6f1e82e;gap:.4rem}.contact-copy-status{color:#9bd66b;min-height:1.35rem;font-size:.9rem}.contact-action-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem;margin-top:1.2rem;display:grid}.contact-action-card{color:#f6f1e8;background:#ffffff0e;border:1px solid #ffffff24;border-radius:.5rem;gap:.25rem;min-height:7.4rem;padding:1rem;transition:transform .18s,border-color .18s,background-color .18s;display:grid}.contact-action-card:hover,.contact-action-card:focus-visible{background:#38d8c81a;border-color:#38d8c87a;transform:translateY(-2px)}.contact-action-card span{color:#38d8c8;font-weight:900}.contact-action-card small{color:#9ca3af}.contact-draft-card{gap:1rem;display:grid}.contact-card-header{margin-bottom:.25rem}.contact-field-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;display:grid}.contact-draft-card label,.cv-gate label{color:#d1d5db;margin-bottom:.45rem;font-size:.9rem;font-weight:800;display:block}.contact-draft-card input,.contact-draft-card select,.contact-draft-card textarea{color:#f6f1e8;background:#05070aad;border:1px solid #ffffff29;border-radius:.5rem;outline:0;width:100%;padding:.85rem .95rem}.contact-draft-card textarea{resize:vertical}.contact-draft-card input:focus,.contact-draft-card select:focus,.contact-draft-card textarea:focus{border-color:#38d8c89e;box-shadow:0 0 0 3px #38d8c821}.contact-primary-button{color:#05070a;background:#f2b84b;gap:.65rem}.contact-zero-backend-note{color:#9ca3af;font-size:.9rem}.contact-support-panel{border-color:#9bd66b3d}.contact-availability{color:#9bd66b;align-items:center;gap:.6rem;margin-bottom:1rem;display:flex}.contact-availability span{background:#9bd66b;border-radius:9999px;width:.8rem;height:.8rem;box-shadow:0 0 20px #9bd66bb8}.contact-support-panel ul{color:#d1d5db;gap:.45rem;margin-top:1rem;display:grid}.contact-support-panel li{padding-left:1.25rem;position:relative}.contact-support-panel li:before{content:">";color:#9bd66b;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-weight:900;position:absolute;left:0}.cv-gate{border-color:#f2b84b3d}.cv-gate-row{grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.75rem;display:grid}.cv-gate-input{color:#f6f1e8;background:#05070a9e;border:1px solid #ffffff29;border-radius:.85rem;outline:0;width:100%;min-height:46px;padding:.75rem .95rem}.cv-gate-input:focus{border-color:#38d8c89e;box-shadow:0 0 0 3px #38d8c821}.cv-unlock-button,.cv-public-link{color:#05070a;background:#f2b84b;min-height:46px;padding:.75rem 1rem}.cv-secondary-link{color:#38d8c8;background:#38d8c817;border:1px solid #38d8c852;min-height:46px;padding:.75rem 1rem}.cv-unlocked-panel{flex-wrap:wrap;gap:.75rem;display:flex}.cv-unlocked-panel[hidden]{display:none}.portfolio-fab-stack{z-index:55;flex-direction:column;align-items:flex-end;gap:.5rem;display:flex;position:fixed;bottom:1rem;right:1rem}.portfolio-fab{color:#fff;touch-action:manipulation;border:1px solid #ffffff29;border-radius:9999px;justify-content:center;align-items:center;gap:.5rem;min-height:44px;padding:.65rem .9rem;font-weight:800;transition:transform .2s,background-color .2s,border-color .2s;display:inline-flex;box-shadow:0 14px 36px #02050857}.portfolio-fab:hover,.portfolio-fab:focus-visible{transform:translateY(-2px)}.portfolio-fab-primary{color:#05070a;background:#f2b84beb}.portfolio-fab-secondary{background:#080b11c7}.clippy,.clippy-balloon{pointer-events:none}.command-palette{z-index:1100;place-items:end center;padding:1rem;display:grid;position:fixed;inset:0}.command-palette.hidden{display:none}.command-palette-backdrop{background:#000000a3;position:absolute;inset:0}.command-shell{z-index:1;background:#080b11f5;border:1px solid #2dd4bf47;border-radius:.5rem;grid-template-rows:auto minmax(220px,1fr) auto auto;width:min(900px,100%);max-height:min(760px,100svh - 2rem);display:grid;position:relative;overflow:hidden;box-shadow:0 24px 80px #0000007a}.command-shell-header{border-bottom:1px solid #ffffff1a;justify-content:space-between;align-items:center;gap:1rem;padding:1rem 1.1rem;display:flex}.command-kicker{color:#2dd4bf;letter-spacing:.16em;text-transform:uppercase;margin:0 0 .25rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.74rem}.command-shell h2{color:#fff;margin:0;font-size:clamp(1.2rem,2vw,1.75rem)}.command-close{color:#fff;background:#ffffff14;border-radius:9999px;justify-content:center;align-items:center;font-size:1.7rem;line-height:1;display:inline-flex}.command-output{color:#d1d5db;padding:1rem 1.1rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.9rem;line-height:1.55;overflow-y:auto}.command-line{margin-bottom:1rem}.command-input-line{color:#f59e0b}.command-response{white-space:pre-wrap}.command-response a{color:#2dd4bf;text-underline-offset:3px;text-decoration:underline}.command-response strong{color:#fff}.command-form{background:#ffffff09;border-top:1px solid #ffffff1a;grid-template-columns:auto 1fr auto;align-items:center;gap:.7rem;padding:.85rem 1.1rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;display:grid}.command-form span{color:#84cc16;font-weight:800}.command-form input{color:#fff;background:0 0;border:0;outline:0;min-width:0}.command-form button,.command-suggestions button{color:#2dd4bf;background:#2dd4bf24;border-radius:9999px;padding:.55rem .85rem;font-weight:800}.command-suggestions{flex-wrap:wrap;gap:.5rem;padding:0 1.1rem 1rem;display:flex}.project-visual{isolation:isolate;place-items:center;min-height:12rem;display:grid;position:relative;overflow:hidden}.project-visual:before{content:"";opacity:.35;background-color:#0000;background-image:linear-gradient(#ffffff1f 1px,#0000 1px),linear-gradient(90deg,#ffffff1a 1px,#0000 1px);background-position:0 0,0 0;background-repeat:repeat,repeat;background-size:36px 36px;background-attachment:scroll,scroll;background-origin:padding-box,padding-box;background-clip:border-box,border-box;position:absolute;inset:0}.project-visual-road{background:linear-gradient(135deg,#f59e0bc7,#111827f2)}.project-visual-orbit{background:linear-gradient(135deg,#2dd4bfad,#2563ebb8)}.project-visual-tool{background:linear-gradient(135deg,#84cc16a3,#111827f0)}.project-visual-agent{background:linear-gradient(135deg,#f43f5ea8,#2dd4bf6b)}.project-visual-icon{z-index:1;font-size:4.5rem;transition:transform .3s;position:relative}.project-card:hover .project-visual-icon{transform:scale(1.08)rotate(-2deg)}.project-visual-label{z-index:1;color:#fff;letter-spacing:.12em;text-transform:uppercase;background:#080b11b8;border-radius:9999px;padding:.35rem .7rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;position:absolute;top:1rem;left:1rem}.repo-badge,.repo-chip{white-space:nowrap;border-radius:9999px;align-items:center;display:inline-flex}.repo-badge{color:#f59e0b;background:#f59e0b29;padding:.35rem .65rem;font-size:.72rem;font-weight:900}.repo-chip{color:#d1d5db;background:#ffffff12;padding:.28rem .6rem;font-size:.76rem}.reviews-section{border-block:1px solid #ffffff0f}.review-grid{grid-template-columns:repeat(6,minmax(0,1fr));gap:2rem;display:grid}.review-card{text-align:center;background:#ffffff0e;border:1px solid #ffffff1c;border-radius:.5rem;flex-direction:column;grid-column:span 2;justify-content:center;min-height:21.35rem;padding:2rem;transition:transform .18s,border-color .18s,background-color .18s;display:flex}.review-card:hover{background:#ffffff13;border-color:#f59e0b6b;transform:translateY(-4px)}.review-card-accent{background:linear-gradient(#f59e0b29,#ffffff0e)}.review-card-wide{grid-column:span 3;min-height:18.35rem}.review-mark{margin-bottom:1rem;font-size:2.2rem}.review-card h3{color:#f6f1e8;margin-bottom:.85rem;font-size:1.25rem}.review-card p{color:#d1d5db;line-height:1.65}.review-author{color:#9ca3af;margin-top:1rem;font-size:.9rem}.review-note{color:#6b7280;margin-top:.25rem;font-size:.78rem;font-style:italic}.review-roast-teaser{background:#080b119e;border:1px solid #2dd4bf38;border-radius:.5rem;grid-template-columns:1fr auto;align-items:center;gap:1.5rem;padding:1.5rem;display:grid}.roast-button{color:#fb7185;background:#f43f5e29;border:1px solid #f43f5e70;min-height:46px;padding:.75rem 1rem}.goose-desk-scene{isolation:isolate;width:148px;height:148px}.goose-frame-fallback,.goose-desk-goose{object-fit:contain;filter:drop-shadow(0 18px 28px #00000057);width:100%;height:100%}.goose-desk-goose{z-index:2;transform-origin:50% 88%;animation:3.8s ease-in-out infinite goose-waddle;position:relative}.goose-desk-bubble{z-index:3;color:#f2b84b;background:#05070ac7;border:1px solid #f6f1e838;border-radius:9999px;padding:.25rem .55rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;font-weight:900;position:absolute;top:.2rem;right:-.45rem;transform:rotate(7deg)}.goose-desk-shadow{z-index:1;filter:blur(4px);background:#00000057;border-radius:9999px;height:.72rem;position:absolute;bottom:.18rem;left:1.1rem;right:1.1rem}.goose-terror-trigger{color:#f2b84b;background:#f2b84b29;border:1px solid #f2b84b7a;border-radius:9999px;justify-content:center;align-items:center;min-height:46px;padding:.65rem 1rem;font-weight:900;display:inline-flex}.goose-terror-trigger:hover,.goose-terror-trigger:focus-visible,.goose-terror-trigger[aria-pressed=true]{color:#f6f1e8;background:#f2b84b47}.goose-terror-layer{z-index:1200;pointer-events:none;position:fixed;inset:0;overflow:hidden}.goose-terror-toolbar{top:calc(4.8rem + env(safe-area-inset-top));color:#f6f1e8;pointer-events:auto;background:#05070adb;border:1px solid #f2b84b85;border-radius:9999px;align-items:center;gap:.75rem;padding:.45rem .55rem .45rem .9rem;display:inline-flex;position:fixed;right:1rem;box-shadow:0 16px 46px #0000005c}.goose-terror-toolbar span{color:#f2b84b;letter-spacing:.1em;text-transform:uppercase;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;font-weight:900}.goose-terror-toolbar button{color:#05070a;background:#f2b84b;border-radius:9999px;min-height:40px;padding:.55rem .8rem;font-weight:900}.goose-terror-goose{filter:drop-shadow(0 20px 26px #0000006b);pointer-events:none;width:clamp(82px,9vw,132px);height:auto;transition:left .85s ease-in-out,top .85s ease-in-out,transform .2s;animation:.72s steps(2,end) infinite goose-terror-bob;position:fixed}.goose-terror-goose.goose-terror-flipped{transform:scaleX(-1)}.goose-terror-note{color:#10151a;width:min(210px,100vw - 2rem);min-height:7rem;transform:rotate(var(--note-rotate,-2deg));pointer-events:none;background:#f2e7a8;border:1px solid #10151a2e;border-radius:.35rem;padding:1.05rem .85rem .85rem;position:fixed;box-shadow:0 18px 42px #00000057}.goose-terror-note:nth-of-type(2n){--note-rotate:2deg}.goose-terror-note strong{letter-spacing:.08em;text-transform:uppercase;margin-bottom:.45rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.68rem;display:block}.goose-terror-note span{font-weight:900;line-height:1.35;display:block}.goose-terror-note button{color:#10151a;pointer-events:auto;font-size:1.25rem;font-weight:900;position:absolute;top:.25rem;right:.35rem}.goose-terror-footprint{background:#f2b84b75;border-radius:9999px;width:.72rem;height:.42rem;animation:5.2s forwards goose-footprint-fade;position:fixed;transform:rotate(-18deg)}@keyframes goose-terror-bob{0%,to{translate:0}50%{translate:0 -6px}}@keyframes goose-footprint-fade{0%{opacity:.75}to{opacity:0;transform:translateY(10px)rotate(-18deg)}}@keyframes goose-waddle{0%,to{transform:translate(0)translateY(0)rotate(-2deg)}25%{transform:translate(-3px)translateY(-2px)rotate(2deg)}50%{transform:translate(2px)translateY(0)rotate(-1deg)}75%{transform:translate(4px)translateY(-3px)rotate(2deg)}}@media (prefers-reduced-motion:reduce){.goose-desk-goose,.goose-terror-goose,.goose-terror-footprint{animation:none}.goose-terror-goose{transition:none}}@media (max-width:640px){.anchor-section{padding-bottom:8rem}.contact-command-section.anchor-section{padding-top:3rem}.contact-command-hero{margin-bottom:2rem}.contact-hero-email{text-align:center;grid-template-columns:1fr;gap:.35rem;margin-top:-.75rem}.contact-direct-panel,.contact-draft-card,.contact-support-panel,.cv-gate{padding:1.25rem}.portfolio-fab-stack{left:.75rem;right:.75rem;bottom:calc(.75rem + env(safe-area-inset-bottom));grid-template-columns:1fr 1fr 1.2fr;align-items:stretch;display:grid}#hero [data-command-open],.hero-scroll-cue{display:none}.portfolio-fab{width:100%;padding-inline:.55rem;font-size:.82rem}.command-palette{place-items:end center;padding:0}.command-shell{border-radius:18px 18px 0 0;max-height:92svh}.command-output{font-size:.82rem}.goose-terror-toolbar{top:calc(4.5rem + env(safe-area-inset-top));justify-content:space-between;left:.75rem;right:.75rem}.goose-terror-toolbar span{font-size:.64rem}.goose-terror-note{width:min(190px,100vw - 2rem)}}@media (max-width:900px){.retro-archive-header,.retro-request-card{grid-template-columns:1fr}.retro-policy-grid,.retro-catalog-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.contact-command-grid{grid-template-columns:1fr}.retro-section-title{flex-direction:column;align-items:start;gap:.65rem}.review-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.review-card,.review-card-wide{grid-column:span 1}.review-roast-teaser{grid-template-columns:1fr}}@media (max-width:640px){.hire-role-tabs,.retro-policy-grid,.retro-catalog-grid,.review-grid,.contact-action-grid,.contact-field-grid{grid-template-columns:1fr}.retro-game-card{min-height:auto}.retro-card-meta div{grid-template-columns:1fr;gap:.2rem}.retro-link-button,.retro-game-action{width:100%}.hire-console-header,.hire-console-actions{flex-direction:column;align-items:stretch}.cv-gate-row{grid-template-columns:1fr}.cv-unlocked-panel{flex-direction:column}.cv-unlock-button,.cv-public-link,.cv-secondary-link{width:100%}}.scroll-animation{opacity:0;will-change:transform,opacity;transition:none;transform:translateY(20px)scale(.95)}.scroll-animation.visible{opacity:1;transition:transform .6s ease-out,opacity .6s ease-out;transform:translateY(0)scale(1)}@keyframes zoomInOut{0%{opacity:0;transform:scale(.8)}50%{opacity:1;transform:scale(1.1)}to{opacity:1;transform:scale(1)}}#navbar.glass-effect{-webkit-backdrop-filter:blur(14px)saturate(1.15);backdrop-filter:blur(14px)saturate(1.15);border-width:0 0 1px;box-shadow:0 10px 30px #02050859}[tabindex="-1"]:focus{outline:none}@media (min-width:641px){.portfolio-fab{gap:0;min-width:46px;height:46px;padding:0 .85rem}.portfolio-fab span:last-child{white-space:nowrap;opacity:0;max-width:0;transition:opacity .18s;overflow:hidden}.portfolio-fab:hover span:last-child,.portfolio-fab:focus-visible span:last-child{opacity:1;max-width:8rem;margin-left:.5rem}}@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}.skill-spec-grid{grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem;display:grid}.skill-spec{--skill-accent:#58c7bc;border:1px solid #f6f1e81f;border-top:2px solid var(--skill-accent);background:linear-gradient(#f6f1e80d,#f6f1e804),#0b1014;border-radius:.5rem;flex-direction:column;padding:1.25rem 1.25rem 1rem;display:flex}.skill-spec-cyan{--skill-accent:#58c7bc}.skill-spec-brass{--skill-accent:#d8aa4f}.skill-spec-green{--skill-accent:#9bd66b}.skill-spec-coral{--skill-accent:#ff766d}.skill-spec header{justify-content:space-between;align-items:center;margin-bottom:.9rem;display:flex}.skill-spec-icon{width:2.5rem;height:2.5rem;color:var(--skill-accent);border:1px solid #f6f1e824;border-radius:.4rem;place-items:center;display:inline-grid}.skill-spec-icon svg{width:1.6rem;height:1.6rem}.skill-spec-index{letter-spacing:.16em;color:#8a9894;font-family:Cascadia Mono,JetBrains Mono,Consolas,monospace;font-size:.75rem}.skill-spec h3{color:#f6f1e8;margin-bottom:.6rem;font-size:1.2rem;font-weight:800}.skill-spec ul{flex:1;margin-bottom:.9rem}.skill-spec li{color:#d4dbd6;border-bottom:1px dashed #f6f1e817;padding:.4rem 0;font-size:.95rem}.skill-spec li:before{content:"▸ ";color:var(--skill-accent)}.skill-spec-proof{color:#8a9894;font-size:.82rem}.skill-spec-proof a{color:var(--skill-accent);text-underline-offset:3px;-webkit-text-decoration:underline #f6f1e840;text-decoration:underline #f6f1e840}.skill-spec-proof a:hover{text-decoration-color:currentColor}@media (max-width:640px){.skill-spec ul{flex-wrap:wrap;gap:.4rem;display:flex}.skill-spec li{border:1px solid #f6f1e81f;border-radius:9999px;padding:.25rem .65rem;font-size:.82rem}.skill-spec li:before{content:none}}.retro-archive-note p+p{margin-top:.6rem}.retro-ledger{color:#c7c9c6;margin-top:.8rem;font-family:Cascadia Mono,JetBrains Mono,Consolas,monospace;font-size:.82rem;line-height:1.7}.retro-ledger strong{color:#f2b84b}.retro-ledger span{white-space:nowrap}.retro-swipe-hint{display:none}@media (max-width:640px){.retro-swipe-hint{color:#8a9894;text-align:right;margin:-1rem 0 .75rem;font-family:Cascadia Mono,JetBrains Mono,Consolas,monospace;font-size:.78rem;display:block}.retro-game-grid.retro-catalog-grid{overscroll-behavior-x:contain;scroll-snap-type:x mandatory;scrollbar-width:thin;gap:.75rem;margin-inline:-1rem;padding:.25rem 1rem 1rem;display:flex;overflow-x:auto}.retro-catalog-grid .retro-game-card{scroll-snap-align:center;flex:0 0 84%}.retro-catalog-grid .retro-game-card:hover,.retro-catalog-grid .retro-game-card:focus-within{transform:none}}.secret-key-map{text-align:left;gap:.55rem;font-size:.875rem;display:grid}.secret-key-map div{border-bottom:1px dashed #f6f1e817;grid-template-columns:minmax(0,auto) 1fr;align-items:baseline;gap:.75rem;padding-bottom:.55rem;display:grid}.secret-key-map dd{color:#b8c0bc}.secret-key-map kbd{color:#f6f1e8;text-align:center;white-space:nowrap;background:#05070a99;border:1px solid #f6f1e833;border-bottom-width:2px;border-radius:.3rem;min-width:1.6rem;padding:.1rem .45rem;font-family:Cascadia Mono,JetBrains Mono,Consolas,monospace;font-size:.75rem;display:inline-block}.project-visual-schematic{--tone:88,199,188;background:radial-gradient(120% 140% at 50% 0%,rgba(var(--tone),.12),transparent 62%),#0b1014}.project-visual-schematic[data-tone=brass]{--tone:216,170,79}.project-visual-schematic[data-tone=green]{--tone:155,214,107}.project-visual-schematic:before{opacity:.16}.project-visual-art{z-index:1;object-fit:cover;width:100%;height:100%;transition:transform .3s;position:absolute;inset:0}.project-card:hover .project-visual-art{transform:scale(1.03)}.project-visual-schematic .project-visual-label{z-index:2}@media (min-width:1024px){.project-card-wide{grid-column:1/-1;grid-template-columns:1fr 1fr;align-items:stretch;display:grid}}.project-visual-photo{background:#05070a}.brain-lab-stage{background:#05070a;min-height:34rem;position:relative;overflow:hidden}.brain-lab-stage img,.brain-lab-stage canvas{width:100%;height:100%;position:absolute;inset:0}.brain-lab-stage img{object-fit:contain;transition:opacity .4s}.brain-lab-stage.is-live img{opacity:0}.brain-lab-stage canvas{cursor:grab;touch-action:pan-y}.brain-lab-stage canvas:active{cursor:grabbing}.brain-lab-note{color:#9eafab;font-size:.82rem;line-height:1.5}.path-related-link{color:#d8aa4f;text-underline-offset:3px;font-size:.9rem;text-decoration:underline}@media (max-width:640px){.brain-lab-stage{min-height:26rem}}.doc-page{background:#05070a}.doc-nav{z-index:3;justify-content:space-between;align-items:center;padding:.75rem clamp(1rem,4vw,3rem);font-size:.875rem;display:flex;position:absolute;inset:0 0 auto}.doc-nav div{gap:1.25rem;display:flex}.doc-nav a{color:#c7cfcb;align-items:center;min-height:44px;display:inline-flex}.doc-nav a:hover{color:#f6f1e8}.doc-nav .doc-nav-home{color:#d8aa4f;font-size:1.4rem;font-weight:800}.doc-hero{background:#05070a;align-items:flex-end;min-height:min(100svh,56rem);display:flex;position:relative;overflow:hidden}.doc-hero-poster,#brain-canvas{width:100%;height:100%;position:absolute;inset:0}.doc-hero-poster{object-fit:contain;transition:opacity .6s}#brain-canvas{opacity:0;cursor:grab;touch-action:pan-y;transition:opacity .6s}#brain-canvas:active{cursor:grabbing}.doc-hero.is-live #brain-canvas{opacity:1}.doc-hero.is-live .doc-hero-poster{opacity:0}.doc-hero:after{content:"";z-index:1;pointer-events:none;background:linear-gradient(#0000,#05070af2 70%);position:absolute;inset:40% 0 0}.doc-hero-copy{z-index:2;pointer-events:none;max-width:40rem;padding:0 clamp(1rem,4vw,3rem) clamp(3.5rem,8vh,5rem);position:relative}.doc-eyebrow{color:#9eafab;margin-bottom:.9rem;font-size:.9rem}.doc-hero-copy h1{color:#f6f1e8;letter-spacing:-.02em;margin-bottom:.9rem;font-size:clamp(2.25rem,5.5vw,4rem);font-weight:600;line-height:1.05}.doc-hero-copy p:last-child{color:#d4dbd6;font-size:1.15rem;line-height:1.55}.doc-hero-note{z-index:2;color:#8a9894;font-size:.78rem;position:absolute;bottom:1.25rem;right:clamp(1rem,4vw,3rem)}.doc-body{max-width:62rem;margin:0 auto;padding:3rem clamp(1rem,4vw,2rem) 4rem}.doc-body>section,.doc-details{margin-top:4.5rem}.doc-figures{border-bottom:1px solid #f6f1e81f;grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));gap:1.5rem;padding-bottom:2rem;display:grid}.doc-figures div{flex-direction:column;display:flex}.doc-figures dd{color:#f6f1e8;letter-spacing:-.01em;font-size:2rem;font-weight:600}.doc-figures dt{color:#9eafab;order:1;font-size:.875rem}.doc-body h2{color:#f6f1e8;margin-bottom:1rem;font-size:1.75rem;font-weight:600}.doc-body h3{color:#f6f1e8;margin:2rem 0 .75rem;font-size:1.25rem;font-weight:600}.doc-body p,.doc-body li{color:#cdd5d1;line-height:1.7}.doc-body p{margin-bottom:.9rem}.doc-body strong{color:#f6f1e8;font-weight:600}.doc-body a{color:#8fd4cc;text-underline-offset:3px;-webkit-text-decoration:underline #8fd4cc59;text-decoration:underline #8fd4cc59}.doc-split{grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);align-items:center;gap:3rem;display:grid}.doc-body figure img{background:#05070a;border-radius:.25rem;width:100%;height:auto}.doc-body figcaption{color:#8a9894;margin-top:.6rem;font-size:.82rem;line-height:1.5}.doc-steps{gap:.5rem;max-width:44rem;margin-bottom:2rem;padding-left:1.3rem;list-style:decimal;display:grid}.doc-pair{grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;display:grid}.doc-results div{border-bottom:1px solid #f6f1e81a;justify-content:space-between;gap:1rem;padding:.75rem 0;display:flex}.doc-results dt{color:#cdd5d1}.doc-results dd{color:#f6f1e8;text-align:right;font-weight:600}.doc-body .doc-note{color:#9eafab;margin-top:.9rem;font-size:.92rem}.doc-details{border-top:1px solid #f6f1e81f;border-bottom:1px solid #f6f1e81f}.doc-details summary{color:#d4dbd6;cursor:pointer;align-items:center;min-height:3.5rem;font-weight:600;display:flex}.doc-details summary:before{content:"+";color:#9eafab;margin-right:.6rem}.doc-details[open] summary:before{content:"−"}.doc-details>div{padding-bottom:2rem}.doc-details ul,.doc-details ol{gap:.6rem;margin-bottom:1rem;padding-left:1.3rem;list-style:outside;display:grid}.doc-table-wrap{margin-bottom:1rem;overflow-x:auto}.doc-table-wrap table{border-collapse:collapse;width:100%;font-size:.95rem}.doc-table-wrap th,.doc-table-wrap td{color:#cdd5d1;text-align:left;border-bottom:1px solid #f6f1e81a;padding:.55rem .75rem .55rem 0}.doc-table-wrap th{color:#f6f1e8;font-weight:600}.doc-body .doc-sources{font-size:.9rem}.doc-sources h2{font-size:1.1rem}.doc-sources ul{gap:.4rem;margin-bottom:1rem;display:grid}@media (max-width:760px){.doc-split,.doc-pair{grid-template-columns:1fr}.doc-split{gap:1.5rem}.doc-hero-note{display:none}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@keyframes fadeIn{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes float{0%,to{transform:translateY(0)}50%{transform:translateY(-20px)}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--leading-tight:1.25;--leading-relaxed:1.625;--radius-lg:.5rem;--radius-2xl:1rem;--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--animate-bounce:bounce 1s infinite;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-tech-dark:#05070a;--color-tech-cyan:#58c7bc;--color-tech-purple:#d8aa4f;--color-tech-green:#9bd66b;--color-tech-pink:#ff766d;--font-display:"Bahnschrift","Aptos Display","Segoe UI Variable","Segoe UI",system-ui,sans-serif;--font-tech:"Cascadia Mono","JetBrains Mono",Consolas,Monaco,monospace;--animate-fade-in:fadeIn .6s ease-in-out;--animate-float:float 6s ease-in-out infinite}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing)*0)}.-top-10{top:calc(var(--spacing)*-10)}.top-0{top:calc(var(--spacing)*0)}.top-1\/2{top:50%}.-right-10{right:calc(var(--spacing)*-10)}.right-0{right:calc(var(--spacing)*0)}.-bottom-1{bottom:calc(var(--spacing)*-1)}.-bottom-10{bottom:calc(var(--spacing)*-10)}.bottom-8{bottom:calc(var(--spacing)*8)}.-left-10{left:calc(var(--spacing)*-10)}.left-0{left:calc(var(--spacing)*0)}.left-1\/2{left:50%}.left-5{left:calc(var(--spacing)*5)}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-5{margin-top:calc(var(--spacing)*5)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-8{margin-top:calc(var(--spacing)*8)}.mt-10{margin-top:calc(var(--spacing)*10)}.mt-12{margin-top:calc(var(--spacing)*12)}.mt-14{margin-top:calc(var(--spacing)*14)}.mt-20{margin-top:calc(var(--spacing)*20)}.mt-auto{margin-top:auto}.mr-2{margin-right:calc(var(--spacing)*2)}.mr-3{margin-right:calc(var(--spacing)*3)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-5{margin-bottom:calc(var(--spacing)*5)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.mb-10{margin-bottom:calc(var(--spacing)*10)}.mb-12{margin-bottom:calc(var(--spacing)*12)}.mb-16{margin-bottom:calc(var(--spacing)*16)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-0\.5{height:calc(var(--spacing)*.5)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-10{height:calc(var(--spacing)*10)}.h-11{height:calc(var(--spacing)*11)}.h-16{height:calc(var(--spacing)*16)}.h-24{height:calc(var(--spacing)*24)}.h-32{height:calc(var(--spacing)*32)}.h-72{height:calc(var(--spacing)*72)}.h-96{height:calc(var(--spacing)*96)}.h-full{height:100%}.max-h-56{max-height:calc(var(--spacing)*56)}.min-h-11{min-height:calc(var(--spacing)*11)}.min-h-\[20px\]{min-height:20px}.min-h-\[72svh\]{min-height:72svh}.min-h-\[84svh\]{min-height:84svh}.w-0{width:calc(var(--spacing)*0)}.w-1{width:calc(var(--spacing)*1)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-6{width:calc(var(--spacing)*6)}.w-11{width:calc(var(--spacing)*11)}.w-16{width:calc(var(--spacing)*16)}.w-72{width:calc(var(--spacing)*72)}.w-96{width:calc(var(--spacing)*96)}.w-\[calc\(100vw-4rem\)\]{width:calc(100vw - 4rem)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-fade-in{animation:var(--animate-fade-in)}.animate-float{animation:var(--animate-float)}.animate-pulse{animation:var(--animate-pulse)}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-5{gap:calc(var(--spacing)*5)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}.gap-10{gap:calc(var(--spacing)*10)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing)*6)}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-800{border-color:var(--color-gray-800)}.border-tech-cyan\/20{border-color:#58c7bc33}@supports (color:color-mix(in lab, red, red)){.border-tech-cyan\/20{border-color:color-mix(in oklab,var(--color-tech-cyan)20%,transparent)}}.border-tech-cyan\/30{border-color:#58c7bc4d}@supports (color:color-mix(in lab, red, red)){.border-tech-cyan\/30{border-color:color-mix(in oklab,var(--color-tech-cyan)30%,transparent)}}.border-tech-cyan\/40{border-color:#58c7bc66}@supports (color:color-mix(in lab, red, red)){.border-tech-cyan\/40{border-color:color-mix(in oklab,var(--color-tech-cyan)40%,transparent)}}.border-transparent{border-color:#0000}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.border-white\/15{border-color:#ffffff26}@supports (color:color-mix(in lab, red, red)){.border-white\/15{border-color:color-mix(in oklab,var(--color-white)15%,transparent)}}.border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab, red, red)){.border-white\/20{border-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab, red, red)){.bg-black\/30{background-color:color-mix(in oklab,var(--color-black)30%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-gray-600{background-color:var(--color-gray-600)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800\/60{background-color:#1e293999}@supports (color:color-mix(in lab, red, red)){.bg-gray-800\/60{background-color:color-mix(in oklab,var(--color-gray-800)60%,transparent)}}.bg-tech-cyan{background-color:var(--color-tech-cyan)}.bg-tech-cyan\/10{background-color:#58c7bc1a}@supports (color:color-mix(in lab, red, red)){.bg-tech-cyan\/10{background-color:color-mix(in oklab,var(--color-tech-cyan)10%,transparent)}}.bg-tech-dark{background-color:var(--color-tech-dark)}.bg-tech-dark\/60{background-color:#05070a99}@supports (color:color-mix(in lab, red, red)){.bg-tech-dark\/60{background-color:color-mix(in oklab,var(--color-tech-dark)60%,transparent)}}.bg-tech-dark\/95{background-color:#05070af2}@supports (color:color-mix(in lab, red, red)){.bg-tech-dark\/95{background-color:color-mix(in oklab,var(--color-tech-dark)95%,transparent)}}.bg-tech-green{background-color:var(--color-tech-green)}.bg-tech-purple{background-color:var(--color-tech-purple)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white)60%,transparent)}}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-cyan{--tw-gradient-from:var(--color-tech-cyan);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-dark{--tw-gradient-from:var(--color-tech-dark);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-green{--tw-gradient-from:var(--color-tech-green);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-purple{--tw-gradient-from:var(--color-tech-purple);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-tech-purple\/10{--tw-gradient-from:#d8aa4f1a}@supports (color:color-mix(in lab, red, red)){.from-tech-purple\/10{--tw-gradient-from:color-mix(in oklab,var(--color-tech-purple)10%,transparent)}}.from-tech-purple\/10{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.via-tech-dark{--tw-gradient-via:var(--color-tech-dark);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-cyan{--tw-gradient-to:var(--color-tech-cyan);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-cyan\/10{--tw-gradient-to:#58c7bc1a}@supports (color:color-mix(in lab, red, red)){.to-tech-cyan\/10{--tw-gradient-to:color-mix(in oklab,var(--color-tech-cyan)10%,transparent)}}.to-tech-cyan\/10{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-dark{--tw-gradient-to:var(--color-tech-dark);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-green{--tw-gradient-to:var(--color-tech-green);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-tech-pink{--tw-gradient-to:var(--color-tech-pink);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-7{padding-inline:calc(var(--spacing)*7)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.py-12{padding-block:calc(var(--spacing)*12)}.py-16{padding-block:calc(var(--spacing)*16)}.py-20{padding-block:calc(var(--spacing)*20)}.py-24{padding-block:calc(var(--spacing)*24)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-20{padding-top:calc(var(--spacing)*20)}.pt-24{padding-top:calc(var(--spacing)*24)}.pb-16{padding-bottom:calc(var(--spacing)*16)}.pb-24{padding-bottom:calc(var(--spacing)*24)}.text-center{text-align:center}.text-left{text-align:left}.font-display{font-family:var(--font-display)}.font-mono{font-family:var(--font-mono)}.font-tech{font-family:var(--font-tech)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.break-words{overflow-wrap:break-word}.whitespace-pre-wrap{white-space:pre-wrap}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-tech-cyan{color:var(--color-tech-cyan)}.text-tech-dark{color:var(--color-tech-dark)}.text-tech-green{color:var(--color-tech-green)}.text-tech-pink{color:var(--color-tech-pink)}.text-tech-purple{color:var(--color-tech-purple)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.opacity-10{opacity:.1}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.mix-blend-multiply{mix-blend-mode:multiply}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-red-500{--tw-ring-color:var(--color-red-500)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-xl{--tw-blur:blur(var(--blur-xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}@media (hover:hover){.group-hover\:w-full:is(:where(.group):hover *){width:100%}.group-hover\:translate-y-1:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:scale-105:is(:where(.group):hover *),.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-tech-cyan:hover{border-color:var(--color-tech-cyan)}.hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-tech-cyan:hover{background-color:var(--color-tech-cyan)}.hover\:bg-tech-cyan\/10:hover{background-color:#58c7bc1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tech-cyan\/10:hover{background-color:color-mix(in oklab,var(--color-tech-cyan)10%,transparent)}}.hover\:bg-tech-cyan\/20:hover{background-color:#58c7bc33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tech-cyan\/20:hover{background-color:color-mix(in oklab,var(--color-tech-cyan)20%,transparent)}}.hover\:bg-tech-green\/20:hover{background-color:#9bd66b33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tech-green\/20:hover{background-color:color-mix(in oklab,var(--color-tech-green)20%,transparent)}}.hover\:bg-tech-purple\/20:hover{background-color:#d8aa4f33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-tech-purple\/20:hover{background-color:color-mix(in oklab,var(--color-tech-purple)20%,transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.hover\:from-tech-cyan:hover{--tw-gradient-from:var(--color-tech-cyan);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.hover\:from-tech-pink:hover{--tw-gradient-from:var(--color-tech-pink);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.hover\:to-tech-green:hover{--tw-gradient-to:var(--color-tech-green);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.hover\:to-tech-purple:hover{--tw-gradient-to:var(--color-tech-purple);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.hover\:text-tech-cyan:hover{color:var(--color-tech-cyan)}.hover\:text-tech-green:hover{color:var(--color-tech-green)}.hover\:text-tech-purple:hover{color:var(--color-tech-purple)}.hover\:text-white:hover{color:var(--color-white)}}@media (min-width:40rem){.sm\:mb-6{margin-bottom:calc(var(--spacing)*6)}.sm\:mb-8{margin-bottom:calc(var(--spacing)*8)}.sm\:mb-12{margin-bottom:calc(var(--spacing)*12)}.sm\:min-h-screen{min-height:100vh}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}:where(.sm\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}.sm\:p-6{padding:calc(var(--spacing)*6)}.sm\:px-4{padding-inline:calc(var(--spacing)*4)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:px-8{padding-inline:calc(var(--spacing)*8)}.sm\:py-4{padding-block:calc(var(--spacing)*4)}.sm\:py-16{padding-block:calc(var(--spacing)*16)}.sm\:pt-20{padding-top:calc(var(--spacing)*20)}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}@media (min-width:48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:py-24{padding-block:calc(var(--spacing)*24)}.md\:pt-28{padding-top:calc(var(--spacing)*28)}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.md\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.md\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.md\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[0\.9fr_1\.1fr\]{grid-template-columns:.9fr 1.1fr}.lg\:grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.lg\:gap-8{gap:calc(var(--spacing)*8)}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}.lg\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.lg\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.lg\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}}}.gradient-text{color:#d8aa4f;-webkit-text-fill-color:currentColor}.glass-effect{background:linear-gradient(#f6f1e811,#f6f1e809),#0b0f12c7;border:1px solid #f6f1e81f;box-shadow:0 18px 48px #02050847}.glass-effect.rounded-2xl,.project-card.rounded-2xl,.cv-gate.rounded-2xl,.glass-effect.rounded-xl{border-radius:.5rem}.skip-link{z-index:60;color:#fff;background:#05070af2;border:1px solid #38d8c873;border-radius:.75rem;padding:.75rem 1rem;font-weight:600;transition:transform .2s;position:absolute;top:1rem;left:1rem;transform:translateY(-200%)}.skip-link:focus{transform:translateY(0)}h1,h2,h3{letter-spacing:0;text-wrap:balance;font-family:Bahnschrift,Aptos Display,Segoe UI Variable,system-ui,sans-serif}.btn{color:#fff;background-color:#1a202c;border-radius:5px;padding:10px 20px;text-decoration:none;transition:background-color .3s,transform .3s}.btn:hover{background-color:#4a5568;transform:scale(1.05)}.gradient-bg{color:#fff;background:linear-gradient(135deg,#1a202c,#366ecd)}social-links a{color:#fff;margin:0 10px;transition:color .3s}social-links a:hover{color:gold}social-links{z-index:2000;position:relative}#goose-animation{z-index:1000}h1{color:#f6f1e8}body{color:#f6f1e8;background:linear-gradient(90deg,#f6f1e806 1px,#0000 1px) 0 0/40px 40px,linear-gradient(#f6f1e805 1px,#0000 1px) 0 0/40px 40px,linear-gradient(#05070a 0%,#0c1114 44%,#070908 100%)}html{color-scheme:dark;scroll-behavior:smooth}.touch-target{touch-action:manipulation;min-width:46px;min-height:46px}.anchor-section{scroll-margin-top:5.5rem}.mobile-menu-button-open{z-index:70;position:fixed;top:1rem;right:calc(min(24rem,100vw - 4rem) + .5rem);translate:none}#mobile-menu{z-index:60}.project-action-link{border-radius:.75rem;align-items:center;gap:.35rem;min-height:44px;padding:.5rem .75rem;display:inline-flex}.project-icon-link,.footer-social-link{touch-action:manipulation;border-radius:9999px;justify-content:center;align-items:center;min-width:44px;min-height:44px;display:inline-flex}.footer-link-list a{touch-action:manipulation;align-items:center;min-width:44px;min-height:44px;display:inline-flex}.drawer-scroll-region{overscroll-behavior:contain}.hero-photo-bg{background-position:50%;background-repeat:no-repeat;background-size:cover}.hero-signal-grid{opacity:.42;background-color:#0000;background-image:linear-gradient(#38d8c817 1px,#0000 1px),linear-gradient(90deg,#f2b84b12 1px,#0000 1px);background-position:0 0,0 0;background-repeat:repeat,repeat;background-size:72px 72px;background-attachment:scroll,scroll;background-origin:padding-box,padding-box;background-clip:border-box,border-box;-webkit-mask-image:linear-gradient(90deg,#000 0% 56%,#0000 100%);mask-image:linear-gradient(90deg,#000 0% 56%,#0000 100%)}.hire-command-center{background:linear-gradient(135deg,#05070af7,#0e1214ed)}.hire-grid-plane{opacity:.34;background-color:#0000;background-image:linear-gradient(#9bd66b14 1px,#0000 1px),linear-gradient(90deg,#38d8c814 1px,#0000 1px);background-position:0 0,0 0;background-repeat:repeat,repeat;background-size:44px 44px;background-attachment:scroll,scroll;background-origin:padding-box,padding-box;background-clip:border-box,border-box;position:absolute;inset:0;-webkit-mask-image:linear-gradient(#0000,#000 12% 86%,#0000);mask-image:linear-gradient(#0000,#000 12% 86%,#0000)}.hire-role-tabs{grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem;display:grid}.hire-role-tab,.hire-command-button,.hire-contact-link,.roast-button,.retro-link-button,.retro-game-action,.contact-primary-button,.contact-email-strip button,.cv-unlock-button,.cv-public-link,.cv-secondary-link{touch-action:manipulation;border-radius:9999px;justify-content:center;align-items:center;font-weight:900;display:inline-flex}.hire-role-tab{color:#d1d5db;background:#ffffff0e;border:1px solid #ffffff21;min-height:48px;transition:transform .18s,border-color .18s,background-color .18s,color .18s}.hire-role-tab:hover,.hire-role-tab:focus-visible,.hire-role-tab.is-active{color:#f6f1e8;background:#2dd4bf29;border-color:#2dd4bf94;transform:translateY(-1px)}.hire-console{background:#080b0deb;border:1px solid #58c7bc42;border-radius:.5rem;overflow:hidden;box-shadow:0 28px 80px #02050861}.hire-console-header,.hire-console-actions{border-bottom:1px solid #ffffff1a;justify-content:space-between;align-items:center;gap:1rem;padding:1rem 1.25rem;display:flex}.hire-console-actions{border-top:1px solid #ffffff1a;border-bottom:0;flex-wrap:wrap}.hire-status-dot{background:#84cc16;border-radius:9999px;width:.7rem;height:.7rem;box-shadow:0 0 20px #84cc16db}.hire-console-body{padding:clamp(1.25rem,3vw,2rem)}.hire-metric{background:#ffffff0e;border:1px solid #ffffff1a;border-radius:.5rem;min-height:6.5rem;padding:1rem}.hire-metric-label{color:#94a3b8;letter-spacing:.12em;text-transform:uppercase;margin-bottom:.45rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;display:block}.hire-metric strong{color:#f6f1e8;line-height:1.25}.hire-proof-list{color:#d1d5db;gap:.8rem;display:grid}.hire-proof-list li{padding-left:1.5rem;position:relative}.hire-proof-list li:before{content:">";color:#84cc16;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-weight:900;position:absolute;left:0}.hire-command-button{color:#2dd4bf;background:#2dd4bf29;border:1px solid #2dd4bf61;min-height:46px;padding:.75rem 1rem}.hire-contact-link{color:#05070a;background:#f2b84b;min-height:46px;padding:.75rem 1rem}.retro-archive-section{background:linear-gradient(90deg,#d8aa4f0e 1px,#0000 1px) 0 0/54px 54px,linear-gradient(#58c7bc0b 1px,#0000 1px) 0 0/54px 54px,linear-gradient(135deg,#05070afa,#0d1117f0)}.retro-archive-header{grid-template-columns:minmax(0,1fr) minmax(18rem,.42fr);align-items:end;gap:2rem;margin-bottom:2.5rem;display:grid}.retro-archive-note,.retro-request-card,.retro-policy-card,.retro-game-card{background:#f6f1e80d;border:1px solid #ffffff1c;border-radius:.5rem}.retro-archive-note{color:#d1d5db;padding:1.25rem;line-height:1.55}.retro-policy-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:.8rem;margin-bottom:3rem;display:grid}.retro-policy-card{min-height:5.5rem;padding:1rem}.retro-policy-value{color:#f2b84b;font-size:1.75rem;font-weight:900;line-height:1;display:block}.retro-policy-label{color:#9ca3af;letter-spacing:.08em;text-transform:uppercase;margin-top:.45rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;display:block}.retro-section-title{justify-content:space-between;align-items:end;gap:2rem;margin-bottom:1.25rem;display:flex}.retro-section-title h3{color:#f6f1e8;margin-top:.35rem;font-size:clamp(1.55rem,2.4vw,2.35rem);font-weight:900}.retro-section-title>p{color:#9ca3af;max-width:30rem}.retro-catalog-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem;display:grid}.retro-game-card{grid-template-rows:auto auto auto 1fr auto;gap:1rem;min-height:23rem;padding:1.25rem;transition:transform .18s,border-color .18s,background-color .18s;display:grid}.retro-game-card:hover,.retro-game-card:focus-within{background:#f6f1e812;border-color:#38d8c86b;transform:translateY(-3px)}.retro-card-topline{justify-content:space-between;align-items:center;gap:.75rem;display:flex}.retro-card-icon{background:#05070a75;border:1px solid #ffffff1f;border-radius:.5rem;place-items:center;width:3rem;height:3rem;font-size:1.65rem;display:inline-grid}.retro-source-pill{color:#38d8c8;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;background:#38d8c81a;border:1px solid #38d8c852;border-radius:9999px;align-items:center;min-height:1.75rem;padding:.28rem .62rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.68rem;font-weight:800;display:inline-flex}.retro-game-card h3{color:#f6f1e8;font-size:1.35rem;font-weight:900}.retro-game-card p{color:#d1d5db;font-size:.94rem;line-height:1.62}.retro-card-meta{gap:.55rem;display:grid}.retro-card-meta div{grid-template-columns:4.3rem minmax(0,1fr);align-items:start;gap:.75rem;display:grid}.retro-card-meta dt{color:#9ca3af;letter-spacing:.08em;text-transform:uppercase;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.68rem}.retro-card-meta dd{color:#f6f1e8;font-size:.84rem}.retro-link-button,.retro-game-action{color:#38d8c8;background:#38d8c829;border:1px solid #38d8c861;gap:.45rem;min-height:46px;padding:.75rem 1rem}.retro-link-button:hover,.retro-link-button:focus-visible,.retro-game-action:hover,.retro-game-action:focus-visible{color:#f6f1e8;background:#38d8c83d}.retro-game-action{width:100%}.retro-request-card{grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:1.5rem;padding:1.25rem;display:grid}.retro-request-card h3{color:#f6f1e8;margin-bottom:.35rem;font-size:1.65rem;font-weight:900}.retro-request-card p{color:#d1d5db}.contact-command-section{background:linear-gradient(90deg,#f2b84b0d 1px,#0000 1px) 0 0/64px 64px,linear-gradient(#38d8c80f 1px,#0000 1px) 0 0/64px 64px,#05070a}.contact-hero-email{background:#05070ab8;border:1px solid #f2b84b61;border-radius:.5rem;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:1rem;max-width:58rem;margin:-1.25rem auto 2rem;padding:.9rem 1rem;display:grid}.contact-hero-email span,.contact-hero-email small{color:#9ca3af;letter-spacing:.1em;text-transform:uppercase;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;font-weight:900}.contact-hero-email a{color:#f6f1e8;overflow-wrap:anywhere;font-weight:900}.contact-hero-email a:hover,.contact-hero-email a:focus-visible{color:#f2b84b}.contact-command-grid{grid-template-columns:minmax(0,.95fr) minmax(0,1.05fr);align-items:start;gap:1rem;display:grid}.contact-direct-panel,.contact-draft-card,.contact-support-panel,.cv-gate{border-radius:.5rem;padding:clamp(1.25rem,2.6vw,2rem)}.contact-direct-panel{min-height:100%}.contact-direct-panel h3,.contact-draft-card h3,.contact-support-panel h3,.cv-gate h3{color:#f6f1e8;margin-bottom:.75rem;font-size:clamp(1.45rem,2vw,2rem);font-weight:900}.contact-direct-copy,.contact-card-header p,.contact-support-panel p,.cv-gate p{color:#d1d5db;line-height:1.65}.contact-email-strip{background:#f2b84b1a;border:1px solid #f2b84b6b;border-radius:.5rem;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:.85rem;margin:1.5rem 0 .4rem;padding:.85rem;display:grid}.contact-email-strip span:first-child{color:#f2b84b;letter-spacing:.1em;text-transform:uppercase;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;font-weight:900}.contact-email-strip strong{color:#f6f1e8;overflow-wrap:anywhere;min-width:0}.contact-email-strip button,.contact-primary-button{min-height:46px;padding:.72rem 1rem}.contact-email-strip button{color:#f6f1e8;background:#05070a94;border:1px solid #f6f1e82e;gap:.4rem}.contact-copy-status{color:#9bd66b;min-height:1.35rem;font-size:.9rem}.contact-action-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem;margin-top:1.2rem;display:grid}.contact-action-card{color:#f6f1e8;background:#ffffff0e;border:1px solid #ffffff24;border-radius:.5rem;gap:.25rem;min-height:7.4rem;padding:1rem;transition:transform .18s,border-color .18s,background-color .18s;display:grid}.contact-action-card:hover,.contact-action-card:focus-visible{background:#38d8c81a;border-color:#38d8c87a;transform:translateY(-2px)}.contact-action-card span{color:#38d8c8;font-weight:900}.contact-action-card small{color:#9ca3af}.contact-draft-card{gap:1rem;display:grid}.contact-card-header{margin-bottom:.25rem}.contact-field-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;display:grid}.contact-draft-card label,.cv-gate label{color:#d1d5db;margin-bottom:.45rem;font-size:.9rem;font-weight:800;display:block}.contact-draft-card input,.contact-draft-card select,.contact-draft-card textarea{color:#f6f1e8;background:#05070aad;border:1px solid #ffffff29;border-radius:.5rem;outline:0;width:100%;padding:.85rem .95rem}.contact-draft-card textarea{resize:vertical}.contact-draft-card input:focus,.contact-draft-card select:focus,.contact-draft-card textarea:focus{border-color:#38d8c89e;box-shadow:0 0 0 3px #38d8c821}.contact-primary-button{color:#05070a;background:#f2b84b;gap:.65rem}.contact-zero-backend-note{color:#9ca3af;font-size:.9rem}.contact-support-panel{border-color:#9bd66b3d}.contact-availability{color:#9bd66b;align-items:center;gap:.6rem;margin-bottom:1rem;display:flex}.contact-availability span{background:#9bd66b;border-radius:9999px;width:.8rem;height:.8rem;box-shadow:0 0 20px #9bd66bb8}.contact-support-panel ul{color:#d1d5db;gap:.45rem;margin-top:1rem;display:grid}.contact-support-panel li{padding-left:1.25rem;position:relative}.contact-support-panel li:before{content:">";color:#9bd66b;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-weight:900;position:absolute;left:0}.cv-gate{border-color:#f2b84b3d}.cv-gate-row{grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.75rem;display:grid}.cv-gate-input{color:#f6f1e8;background:#05070a9e;border:1px solid #ffffff29;border-radius:.85rem;outline:0;width:100%;min-height:46px;padding:.75rem .95rem}.cv-gate-input:focus{border-color:#38d8c89e;box-shadow:0 0 0 3px #38d8c821}.cv-unlock-button,.cv-public-link{color:#05070a;background:#f2b84b;min-height:46px;padding:.75rem 1rem}.cv-secondary-link{color:#38d8c8;background:#38d8c817;border:1px solid #38d8c852;min-height:46px;padding:.75rem 1rem}.cv-unlocked-panel{flex-wrap:wrap;gap:.75rem;display:flex}.cv-unlocked-panel[hidden]{display:none}.portfolio-fab-stack{z-index:55;flex-direction:column;align-items:flex-end;gap:.5rem;display:flex;position:fixed;bottom:1rem;right:1rem}.portfolio-fab{color:#fff;touch-action:manipulation;border:1px solid #ffffff29;border-radius:9999px;justify-content:center;align-items:center;gap:.5rem;min-height:44px;padding:.65rem .9rem;font-weight:800;transition:transform .2s,background-color .2s,border-color .2s;display:inline-flex;box-shadow:0 14px 36px #02050857}.portfolio-fab:hover,.portfolio-fab:focus-visible{transform:translateY(-2px)}.portfolio-fab-primary{color:#05070a;background:#f2b84beb}.portfolio-fab-secondary{background:#080b11c7}.clippy,.clippy-balloon{pointer-events:none}.command-palette{z-index:1100;place-items:end center;padding:1rem;display:grid;position:fixed;inset:0}.command-palette.hidden{display:none}.command-palette-backdrop{background:#000000a3;position:absolute;inset:0}.command-shell{z-index:1;background:#080b11f5;border:1px solid #2dd4bf47;border-radius:.5rem;grid-template-rows:auto minmax(220px,1fr) auto auto;width:min(900px,100%);max-height:min(760px,100svh - 2rem);display:grid;position:relative;overflow:hidden;box-shadow:0 24px 80px #0000007a}.command-shell-header{border-bottom:1px solid #ffffff1a;justify-content:space-between;align-items:center;gap:1rem;padding:1rem 1.1rem;display:flex}.command-kicker{color:#2dd4bf;letter-spacing:.16em;text-transform:uppercase;margin:0 0 .25rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.74rem}.command-shell h2{color:#fff;margin:0;font-size:clamp(1.2rem,2vw,1.75rem)}.command-close{color:#fff;background:#ffffff14;border-radius:9999px;justify-content:center;align-items:center;font-size:1.7rem;line-height:1;display:inline-flex}.command-output{color:#d1d5db;padding:1rem 1.1rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.9rem;line-height:1.55;overflow-y:auto}.command-line{margin-bottom:1rem}.command-input-line{color:#f59e0b}.command-response{white-space:pre-wrap}.command-response a{color:#2dd4bf;text-underline-offset:3px;text-decoration:underline}.command-response strong{color:#fff}.command-form{background:#ffffff09;border-top:1px solid #ffffff1a;grid-template-columns:auto 1fr auto;align-items:center;gap:.7rem;padding:.85rem 1.1rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;display:grid}.command-form span{color:#84cc16;font-weight:800}.command-form input{color:#fff;background:0 0;border:0;outline:0;min-width:0}.command-form button,.command-suggestions button{color:#2dd4bf;background:#2dd4bf24;border-radius:9999px;padding:.55rem .85rem;font-weight:800}.command-suggestions{flex-wrap:wrap;gap:.5rem;padding:0 1.1rem 1rem;display:flex}.project-visual{isolation:isolate;place-items:center;min-height:12rem;display:grid;position:relative;overflow:hidden}.project-visual:before{content:"";opacity:.35;background-color:#0000;background-image:linear-gradient(#ffffff1f 1px,#0000 1px),linear-gradient(90deg,#ffffff1a 1px,#0000 1px);background-position:0 0,0 0;background-repeat:repeat,repeat;background-size:36px 36px;background-attachment:scroll,scroll;background-origin:padding-box,padding-box;background-clip:border-box,border-box;position:absolute;inset:0}.project-visual-road{background:linear-gradient(135deg,#f59e0bc7,#111827f2)}.project-visual-orbit{background:linear-gradient(135deg,#2dd4bfad,#2563ebb8)}.project-visual-tool{background:linear-gradient(135deg,#84cc16a3,#111827f0)}.project-visual-agent{background:linear-gradient(135deg,#f43f5ea8,#2dd4bf6b)}.project-visual-icon{z-index:1;font-size:4.5rem;transition:transform .3s;position:relative}.project-card:hover .project-visual-icon{transform:scale(1.08)rotate(-2deg)}.project-visual-label{z-index:1;color:#fff;letter-spacing:.12em;text-transform:uppercase;background:#080b11b8;border-radius:9999px;padding:.35rem .7rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;position:absolute;top:1rem;left:1rem}.repo-badge,.repo-chip{white-space:nowrap;border-radius:9999px;align-items:center;display:inline-flex}.repo-badge{color:#f59e0b;background:#f59e0b29;padding:.35rem .65rem;font-size:.72rem;font-weight:900}.repo-chip{color:#d1d5db;background:#ffffff12;padding:.28rem .6rem;font-size:.76rem}.reviews-section{border-block:1px solid #ffffff0f}.review-grid{grid-template-columns:repeat(6,minmax(0,1fr));gap:2rem;display:grid}.review-card{text-align:center;background:#ffffff0e;border:1px solid #ffffff1c;border-radius:.5rem;flex-direction:column;grid-column:span 2;justify-content:center;min-height:21.35rem;padding:2rem;transition:transform .18s,border-color .18s,background-color .18s;display:flex}.review-card:hover{background:#ffffff13;border-color:#f59e0b6b;transform:translateY(-4px)}.review-card-accent{background:linear-gradient(#f59e0b29,#ffffff0e)}.review-card-wide{grid-column:span 3;min-height:18.35rem}.review-mark{margin-bottom:1rem;font-size:2.2rem}.review-card h3{color:#f6f1e8;margin-bottom:.85rem;font-size:1.25rem}.review-card p{color:#d1d5db;line-height:1.65}.review-author{color:#9ca3af;margin-top:1rem;font-size:.9rem}.review-note{color:#6b7280;margin-top:.25rem;font-size:.78rem;font-style:italic}.review-roast-teaser{background:#080b119e;border:1px solid #2dd4bf38;border-radius:.5rem;grid-template-columns:1fr auto;align-items:center;gap:1.5rem;padding:1.5rem;display:grid}.roast-button{color:#fb7185;background:#f43f5e29;border:1px solid #f43f5e70;min-height:46px;padding:.75rem 1rem}.goose-desk-scene{isolation:isolate;width:148px;height:148px}.goose-frame-fallback,.goose-desk-goose{object-fit:contain;filter:drop-shadow(0 18px 28px #00000057);width:100%;height:100%}.goose-desk-goose{z-index:2;transform-origin:50% 88%;animation:3.8s ease-in-out infinite goose-waddle;position:relative}.goose-desk-bubble{z-index:3;color:#f2b84b;background:#05070ac7;border:1px solid #f6f1e838;border-radius:9999px;padding:.25rem .55rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;font-weight:900;position:absolute;top:.2rem;right:-.45rem;transform:rotate(7deg)}.goose-desk-shadow{z-index:1;filter:blur(4px);background:#00000057;border-radius:9999px;height:.72rem;position:absolute;bottom:.18rem;left:1.1rem;right:1.1rem}.goose-terror-trigger{color:#f2b84b;background:#f2b84b29;border:1px solid #f2b84b7a;border-radius:9999px;justify-content:center;align-items:center;min-height:46px;padding:.65rem 1rem;font-weight:900;display:inline-flex}.goose-terror-trigger:hover,.goose-terror-trigger:focus-visible,.goose-terror-trigger[aria-pressed=true]{color:#f6f1e8;background:#f2b84b47}.goose-terror-layer{z-index:1200;pointer-events:none;position:fixed;inset:0;overflow:hidden}.goose-terror-toolbar{top:calc(4.8rem + env(safe-area-inset-top));color:#f6f1e8;pointer-events:auto;background:#05070adb;border:1px solid #f2b84b85;border-radius:9999px;align-items:center;gap:.75rem;padding:.45rem .55rem .45rem .9rem;display:inline-flex;position:fixed;right:1rem;box-shadow:0 16px 46px #0000005c}.goose-terror-toolbar span{color:#f2b84b;letter-spacing:.1em;text-transform:uppercase;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.72rem;font-weight:900}.goose-terror-toolbar button{color:#05070a;background:#f2b84b;border-radius:9999px;min-height:40px;padding:.55rem .8rem;font-weight:900}.goose-terror-goose{filter:drop-shadow(0 20px 26px #0000006b);pointer-events:none;width:clamp(82px,9vw,132px);height:auto;transition:left .85s ease-in-out,top .85s ease-in-out,transform .2s;animation:.72s steps(2,end) infinite goose-terror-bob;position:fixed}.goose-terror-goose.goose-terror-flipped{transform:scaleX(-1)}.goose-terror-note{color:#10151a;width:min(210px,100vw - 2rem);min-height:7rem;transform:rotate(var(--note-rotate,-2deg));pointer-events:none;background:#f2e7a8;border:1px solid #10151a2e;border-radius:.35rem;padding:1.05rem .85rem .85rem;position:fixed;box-shadow:0 18px 42px #00000057}.goose-terror-note:nth-of-type(2n){--note-rotate:2deg}.goose-terror-note strong{letter-spacing:.08em;text-transform:uppercase;margin-bottom:.45rem;font-family:JetBrains Mono,Monaco,Consolas,monospace;font-size:.68rem;display:block}.goose-terror-note span{font-weight:900;line-height:1.35;display:block}.goose-terror-note button{color:#10151a;pointer-events:auto;font-size:1.25rem;font-weight:900;position:absolute;top:.25rem;right:.35rem}.goose-terror-footprint{background:#f2b84b75;border-radius:9999px;width:.72rem;height:.42rem;animation:5.2s forwards goose-footprint-fade;position:fixed;transform:rotate(-18deg)}@keyframes goose-terror-bob{0%,to{translate:0}50%{translate:0 -6px}}@keyframes goose-footprint-fade{0%{opacity:.75}to{opacity:0;transform:translateY(10px)rotate(-18deg)}}@keyframes goose-waddle{0%,to{transform:translate(0)translateY(0)rotate(-2deg)}25%{transform:translate(-3px)translateY(-2px)rotate(2deg)}50%{transform:translate(2px)translateY(0)rotate(-1deg)}75%{transform:translate(4px)translateY(-3px)rotate(2deg)}}@media (prefers-reduced-motion:reduce){.goose-desk-goose,.goose-terror-goose,.goose-terror-footprint{animation:none}.goose-terror-goose{transition:none}}@media (max-width:640px){.anchor-section{padding-bottom:8rem}.contact-command-section.anchor-section{padding-top:3rem}.contact-command-hero{margin-bottom:2rem}.contact-hero-email{text-align:center;grid-template-columns:1fr;gap:.35rem;margin-top:-.75rem}.contact-direct-panel,.contact-draft-card,.contact-support-panel,.cv-gate{padding:1.25rem}.portfolio-fab-stack{left:.75rem;right:.75rem;bottom:calc(.75rem + env(safe-area-inset-bottom));grid-template-columns:1fr 1fr 1.2fr;align-items:stretch;display:grid}#hero [data-command-open],.hero-scroll-cue{display:none}.portfolio-fab{width:100%;padding-inline:.55rem;font-size:.82rem}.command-palette{place-items:end center;padding:0}.command-shell{border-radius:18px 18px 0 0;max-height:92svh}.command-output{font-size:.82rem}.goose-terror-toolbar{top:calc(4.5rem + env(safe-area-inset-top));justify-content:space-between;left:.75rem;right:.75rem}.goose-terror-toolbar span{font-size:.64rem}.goose-terror-note{width:min(190px,100vw - 2rem)}}@media (max-width:900px){.retro-archive-header,.retro-request-card{grid-template-columns:1fr}.retro-policy-grid,.retro-catalog-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.contact-command-grid{grid-template-columns:1fr}.retro-section-title{flex-direction:column;align-items:start;gap:.65rem}.review-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.review-card,.review-card-wide{grid-column:span 1}.review-roast-teaser{grid-template-columns:1fr}}@media (max-width:640px){.hire-role-tabs,.retro-policy-grid,.retro-catalog-grid,.review-grid,.contact-action-grid,.contact-field-grid{grid-template-columns:1fr}.retro-game-card{min-height:auto}.retro-card-meta div{grid-template-columns:1fr;gap:.2rem}.retro-link-button,.retro-game-action{width:100%}.hire-console-header,.hire-console-actions{flex-direction:column;align-items:stretch}.cv-gate-row{grid-template-columns:1fr}.cv-unlocked-panel{flex-direction:column}.cv-unlock-button,.cv-public-link,.cv-secondary-link{width:100%}}.scroll-animation{opacity:0;will-change:transform,opacity;transition:none;transform:translateY(20px)scale(.95)}.scroll-animation.visible{opacity:1;transition:transform .6s ease-out,opacity .6s ease-out;transform:translateY(0)scale(1)}@keyframes zoomInOut{0%{opacity:0;transform:scale(.8)}50%{opacity:1;transform:scale(1.1)}to{opacity:1;transform:scale(1)}}#navbar.glass-effect{-webkit-backdrop-filter:blur(14px)saturate(1.15);backdrop-filter:blur(14px)saturate(1.15);border-width:0 0 1px;box-shadow:0 10px 30px #02050859}[tabindex="-1"]:focus{outline:none}@media (min-width:641px){.portfolio-fab{gap:0;min-width:46px;height:46px;padding:0 .85rem}.portfolio-fab span:last-child{white-space:nowrap;opacity:0;max-width:0;transition:opacity .18s;overflow:hidden}.portfolio-fab:hover span:last-child,.portfolio-fab:focus-visible span:last-child{opacity:1;max-width:8rem;margin-left:.5rem}}@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}.skill-spec-grid{grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem;display:grid}.skill-spec{--skill-accent:#58c7bc;border:1px solid #f6f1e81f;border-top:2px solid var(--skill-accent);background:linear-gradient(#f6f1e80d,#f6f1e804),#0b1014;border-radius:.5rem;flex-direction:column;padding:1.25rem 1.25rem 1rem;display:flex}.skill-spec-cyan{--skill-accent:#58c7bc}.skill-spec-brass{--skill-accent:#d8aa4f}.skill-spec-green{--skill-accent:#9bd66b}.skill-spec-coral{--skill-accent:#ff766d}.skill-spec header{justify-content:space-between;align-items:center;margin-bottom:.9rem;display:flex}.skill-spec-icon{width:2.5rem;height:2.5rem;color:var(--skill-accent);border:1px solid #f6f1e824;border-radius:.4rem;place-items:center;display:inline-grid}.skill-spec-icon svg{width:1.6rem;height:1.6rem}.skill-spec-index{letter-spacing:.16em;color:#8a9894;font-family:Cascadia Mono,JetBrains Mono,Consolas,monospace;font-size:.75rem}.skill-spec h3{color:#f6f1e8;margin-bottom:.6rem;font-size:1.2rem;font-weight:800}.skill-spec ul{flex:1;margin-bottom:.9rem}.skill-spec li{color:#d4dbd6;border-bottom:1px dashed #f6f1e817;padding:.4rem 0;font-size:.95rem}.skill-spec li:before{content:"▸ ";color:var(--skill-accent)}.skill-spec-proof{color:#8a9894;font-size:.82rem}.skill-spec-proof a{color:var(--skill-accent);text-underline-offset:3px;-webkit-text-decoration:underline #f6f1e840;text-decoration:underline #f6f1e840}.skill-spec-proof a:hover{text-decoration-color:currentColor}@media (max-width:640px){.skill-spec ul{flex-wrap:wrap;gap:.4rem;display:flex}.skill-spec li{border:1px solid #f6f1e81f;border-radius:9999px;padding:.25rem .65rem;font-size:.82rem}.skill-spec li:before{content:none}}.retro-archive-note p+p{margin-top:.6rem}.retro-ledger{color:#c7c9c6;margin-top:.8rem;font-family:Cascadia Mono,JetBrains Mono,Consolas,monospace;font-size:.82rem;line-height:1.7}.retro-ledger strong{color:#f2b84b}.retro-ledger span{white-space:nowrap}.retro-swipe-hint{display:none}@media (max-width:640px){.retro-swipe-hint{color:#8a9894;text-align:right;margin:-1rem 0 .75rem;font-family:Cascadia Mono,JetBrains Mono,Consolas,monospace;font-size:.78rem;display:block}.retro-game-grid.retro-catalog-grid{overscroll-behavior-x:contain;scroll-snap-type:x mandatory;scrollbar-width:thin;gap:.75rem;margin-inline:-1rem;padding:.25rem 1rem 1rem;display:flex;overflow-x:auto}.retro-catalog-grid .retro-game-card{scroll-snap-align:center;flex:0 0 84%}.retro-catalog-grid .retro-game-card:hover,.retro-catalog-grid .retro-game-card:focus-within{transform:none}}.secret-key-map{text-align:left;gap:.55rem;font-size:.875rem;display:grid}.secret-key-map div{border-bottom:1px dashed #f6f1e817;grid-template-columns:minmax(0,auto) 1fr;align-items:baseline;gap:.75rem;padding-bottom:.55rem;display:grid}.secret-key-map dd{color:#b8c0bc}.secret-key-map kbd{color:#f6f1e8;text-align:center;white-space:nowrap;background:#05070a99;border:1px solid #f6f1e833;border-bottom-width:2px;border-radius:.3rem;min-width:1.6rem;padding:.1rem .45rem;font-family:Cascadia Mono,JetBrains Mono,Consolas,monospace;font-size:.75rem;display:inline-block}.project-visual-schematic{--tone:88,199,188;background:radial-gradient(120% 140% at 50% 0%,rgba(var(--tone),.12),transparent 62%),#0b1014}.project-visual-schematic[data-tone=brass]{--tone:216,170,79}.project-visual-schematic[data-tone=green]{--tone:155,214,107}.project-visual-schematic:before{opacity:.16}.project-visual-art{z-index:1;object-fit:cover;width:100%;height:100%;transition:transform .3s;position:absolute;inset:0}.project-card:hover .project-visual-art{transform:scale(1.03)}.project-visual-schematic .project-visual-label{z-index:2}@media (min-width:1024px){.project-card-wide{grid-column:1/-1;grid-template-columns:1fr 1fr;align-items:stretch;display:grid}}.project-visual-photo{background:#05070a}.brain-lab-stage{background:#05070a;min-height:34rem;position:relative;overflow:hidden}.brain-lab-stage img,.brain-lab-stage canvas{width:100%;height:100%;position:absolute;inset:0}.brain-lab-stage img{object-fit:contain;transition:opacity .4s}.brain-lab-stage.is-live img{opacity:0}.brain-lab-stage canvas{cursor:grab;touch-action:pan-y}.brain-lab-stage canvas:active{cursor:grabbing}.brain-lab-note{color:#9eafab;font-size:.82rem;line-height:1.5}.path-related-link{color:#d8aa4f;text-underline-offset:3px;font-size:.9rem;text-decoration:underline}@media (max-width:640px){.brain-lab-stage{min-height:26rem}}.doc-page{background:#05070a}.doc-nav{z-index:3;justify-content:space-between;align-items:center;padding:.75rem clamp(1rem,4vw,3rem);font-size:.875rem;display:flex;position:absolute;inset:0 0 auto}.doc-nav div{gap:1.25rem;display:flex}.doc-nav a{color:#c7cfcb;align-items:center;min-height:44px;display:inline-flex}.doc-nav a:hover{color:#f6f1e8}.doc-nav .doc-nav-home{color:#d8aa4f;font-size:1.4rem;font-weight:800}.doc-hero{background:#05070a;align-items:flex-end;min-height:min(100svh,56rem);display:flex;position:relative;overflow:hidden}.doc-hero-poster,#brain-canvas{width:100%;height:100%;position:absolute;inset:0}.doc-hero-poster{object-fit:contain;transition:opacity .6s}#brain-canvas{opacity:0;cursor:grab;touch-action:pan-y;transition:opacity .6s}#brain-canvas:active{cursor:grabbing}.doc-hero.is-live #brain-canvas{opacity:1}.doc-hero.is-live .doc-hero-poster{opacity:0}.doc-hero:after{content:"";z-index:1;pointer-events:none;background:linear-gradient(#0000,#05070af2 70%);position:absolute;inset:40% 0 0}.doc-hero-copy{z-index:2;pointer-events:none;max-width:40rem;padding:0 clamp(1rem,4vw,3rem) clamp(3.5rem,8vh,5rem);position:relative}.doc-eyebrow{color:#9eafab;margin-bottom:.9rem;font-size:.9rem}.doc-hero-copy h1{color:#f6f1e8;letter-spacing:-.02em;margin-bottom:.9rem;font-size:clamp(2.25rem,5.5vw,4rem);font-weight:600;line-height:1.05}.doc-hero-copy p:last-child{color:#d4dbd6;font-size:1.15rem;line-height:1.55}.doc-hero-note{z-index:2;color:#8a9894;font-size:.78rem;position:absolute;bottom:1.25rem;right:clamp(1rem,4vw,3rem)}.doc-body{max-width:62rem;margin:0 auto;padding:3rem clamp(1rem,4vw,2rem) 4rem}.doc-body>section,.doc-details{margin-top:4.5rem}.doc-figures{border-bottom:1px solid #f6f1e81f;grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));gap:1.5rem;padding-bottom:2rem;display:grid}.doc-figures div{flex-direction:column;display:flex}.doc-figures dd{color:#f6f1e8;letter-spacing:-.01em;font-size:2rem;font-weight:600}.doc-figures dt{color:#9eafab;order:1;font-size:.875rem}.doc-body h2{color:#f6f1e8;margin-bottom:1rem;font-size:1.75rem;font-weight:600}.doc-body h3{color:#f6f1e8;margin:2rem 0 .75rem;font-size:1.25rem;font-weight:600}.doc-body p,.doc-body li{color:#cdd5d1;line-height:1.7}.doc-body p{margin-bottom:.9rem}.doc-body strong{color:#f6f1e8;font-weight:600}.doc-body a{color:#8fd4cc;text-underline-offset:3px;-webkit-text-decoration:underline #8fd4cc59;text-decoration:underline #8fd4cc59}.doc-split{grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);align-items:center;gap:3rem;display:grid}.doc-body figure img{background:#05070a;border-radius:.25rem;width:100%;height:auto}.doc-body figcaption{color:#8a9894;margin-top:.6rem;font-size:.82rem;line-height:1.5}.doc-steps{gap:.5rem;max-width:44rem;margin-bottom:2rem;padding-left:1.3rem;list-style:decimal;display:grid}.doc-pair{grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;display:grid}.doc-results div{border-bottom:1px solid #f6f1e81a;justify-content:space-between;gap:1rem;padding:.75rem 0;display:flex}.doc-results dt{color:#cdd5d1}.doc-results dd{color:#f6f1e8;text-align:right;font-weight:600}.doc-body .doc-note{color:#9eafab;margin-top:.9rem;font-size:.92rem}.doc-details{border-top:1px solid #f6f1e81f;border-bottom:1px solid #f6f1e81f}.doc-details summary{color:#d4dbd6;cursor:pointer;align-items:center;min-height:3.5rem;font-weight:600;display:flex}.doc-details summary:before{content:"+";color:#9eafab;margin-right:.6rem}.doc-details[open] summary:before{content:"−"}.doc-details>div{padding-bottom:2rem}.doc-details ul,.doc-details ol{gap:.6rem;margin-bottom:1rem;padding-left:1.3rem;list-style:outside;display:grid}.doc-table-wrap{margin-bottom:1rem;overflow-x:auto}.doc-table-wrap table{border-collapse:collapse;width:100%;font-size:.95rem}.doc-table-wrap th,.doc-table-wrap td{color:#cdd5d1;text-align:left;border-bottom:1px solid #f6f1e81a;padding:.55rem .75rem .55rem 0}.doc-table-wrap th{color:#f6f1e8;font-weight:600}.doc-body .doc-sources{font-size:.9rem}.doc-sources h2{font-size:1.1rem}.doc-sources ul{gap:.4rem;margin-bottom:1rem;display:grid}@media (max-width:760px){.doc-split,.doc-pair{grid-template-columns:1fr}.doc-split{gap:1.5rem}.doc-hero-note{display:none}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@keyframes fadeIn{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes float{0%,to{transform:translateY(0)}50%{transform:translateY(-20px)}} \ No newline at end of file diff --git a/assets/js/fly-brain.js b/assets/js/fly-brain.js index 26660c05d3..002c4a3454 100644 --- a/assets/js/fly-brain.js +++ b/assets/js/fly-brain.js @@ -96,18 +96,34 @@ export async function mountBrain({ stage, canvas, framing = 'hero' }) { state.yaw += steps[event.key]; draw(); }); + // The loop runs only while the stage is on screen and the GPU context is alive; off screen it costs nothing. + let contextLost = false; + const syncLoop = () => { + const spin = !reduced && state.visible && !contextLost; + renderer.setAnimationLoop(spin ? () => { + if (!state.dragging) state.yaw += 0.0022; + draw(); + } : null); + }; new IntersectionObserver((entries) => { state.visible = entries[0].isIntersecting; + syncLoop(); }).observe(stage); + canvas.addEventListener('webglcontextlost', (event) => { + event.preventDefault(); + contextLost = true; + syncLoop(); + stage.classList.remove('is-live'); + }); + canvas.addEventListener('webglcontextrestored', () => { + contextLost = false; + stage.classList.add('is-live'); + draw(); + syncLoop(); + }); stage.classList.add('is-live'); - if (!reduced) { - renderer.setAnimationLoop(() => { - if (!state.visible) return; - if (!state.dragging) state.yaw += 0.0022; - draw(); - }); - } + syncLoop(); return { // Additive blending makes black points invisible, so a region can switch off without rebuilding anything. @@ -125,7 +141,14 @@ export async function mountBrain({ stage, canvas, framing = 'hero' }) { const heroStage = document.getElementById('brain-stage'); const heroCanvas = document.getElementById('brain-canvas'); if (heroStage && heroCanvas && 'WebGL2RenderingContext' in window) { - const begin = () => mountBrain({ stage: heroStage, canvas: heroCanvas }).catch(() => {}); + // On fly101.html the poster image is the fallback, so failure only needs to be visible to tests and agents. + const hero = window.PortfolioState?.defineLoader('brain-hero'); + const begin = () => { + hero?.send('load'); + mountBrain({ stage: heroStage, canvas: heroCanvas }) + .then(() => hero?.send('resolve')) + .catch(() => hero?.send('reject')); + }; if (document.readyState === 'complete') begin(); else window.addEventListener('load', begin, { once: true }); } diff --git a/assets/js/index-page.js b/assets/js/index-page.js index c769380b4a..ffe6924784 100644 --- a/assets/js/index-page.js +++ b/assets/js/index-page.js @@ -188,18 +188,60 @@ let sourceOpenCount = 0; let konamiSequence = []; let repoSnapshot = FALLBACK_REPO_SNAPSHOT; - let commandHistory = []; let commandPaletteWelcomed = false; let commandPaletteOpener = null; let modalCount = 0; + let konamiTimer = null; + + // Explicit machines (assets/js/state-machine.js). Each mirrors to -state>; docs/site/STATE.md lists them. + const { define, defineLoader, defineChoice, overlays } = window.PortfolioState; + const paletteMachine = define('palette', { + initial: 'closed', + states: { closed: { open: 'open' }, open: { close: 'closed' } } + }); + const dialogMachine = define('dialog', { + initial: 'closed', + states: { closed: { open: 'open' }, open: { close: 'closed' } } + }); + const gooseMachine = define('goose', { + initial: 'idle', + states: { idle: { release: 'loose' }, loose: { stop: 'idle' } } + }); + const clippyLoader = defineLoader('clippy'); + // Which helper is on screen: the text bubble, the animated agent, or nothing. + const helperMachine = define('helper', { + initial: 'hidden', + states: { + hidden: { showText: 'text', showAgent: 'agent' }, + text: { hide: 'hidden', showText: 'text', showAgent: 'agent' }, + agent: { hide: 'hidden', showText: 'text', showAgent: 'agent' } + } + }); + const contactMachine = define('contact', { + initial: 'editing', + states: { editing: { draft: 'drafted' }, drafted: { edit: 'editing' } } + }); + const emailCopyMachine = define('email-copy', { + initial: 'idle', + states: { + idle: { copy: 'copying' }, + copying: { succeed: 'copied', fail: 'failed' }, + copied: { copy: 'copying', reset: 'idle' }, + failed: { copy: 'copying', reset: 'idle' } + } + }); + const hireMachine = defineChoice('hire', ['robotics', 'autonomy', 'simulation', 'ai']); + // Bumped by every show or hide, so a deferred show that lost the race can tell it is stale. + let helperRequest = 0; + const gooseTerrorState = { - active: false, + get active() { + return gooseMachine.is('loose'); + }, layer: null, goose: null, moveTimer: null, noteTimer: null, - footTimer: null, - keyHandler: null, resizeHandler: null, x: 24, y: 120 @@ -211,7 +253,7 @@ } let realClippyAgent = null; let realClippyLoaded = false; - let realClippyLoadStarted = false; + let realClippyLoad = null; let realClippyHideTimer = null; let realClippySpeechTimer = null; let realClippyGoodbyeTimer = null; @@ -239,11 +281,16 @@ } } + const flashTimers = new WeakMap(); function flashInvalidFields(fields) { - fields.forEach((field) => field.classList.add('ring-2', 'ring-red-500')); - window.setTimeout(() => { - fields.forEach((field) => field.classList.remove('ring-2', 'ring-red-500')); - }, 1500); + fields.forEach((field) => { + window.clearTimeout(flashTimers.get(field)); + field.classList.add('ring-2', 'ring-red-500'); + flashTimers.set(field, window.setTimeout(() => { + field.classList.remove('ring-2', 'ring-red-500'); + flashTimers.delete(field); + }, 1500)); + }); } function buildEmailPreviewText(formData) { @@ -332,32 +379,39 @@ }); } + let emailCopyTimer = null; async function copyEmailAddress(button) { + if (!emailCopyMachine.send('copy')) { + return false; + } + button.dataset.baseLabel ??= button.innerHTML; + window.clearTimeout(emailCopyTimer); + const copied = await copyTextToClipboard(EMAIL_ADDRESS); + emailCopyMachine.send(copied ? 'succeed' : 'fail'); const status = document.getElementById('copy-email-status'); - const originalHtml = button?.innerHTML; - if (status) { status.textContent = copied ? 'Email copied. Now go make something happen.' : `Copy failed. Email: ${EMAIL_ADDRESS}`; } - if (button) { - button.innerHTML = copied ? 'Copied Email' : `${EMAIL_ADDRESS}`; - window.setTimeout(() => { - button.innerHTML = originalHtml || 'Copy Email Address'; - }, COPY_RESET_DELAY_MS); - } + button.innerHTML = copied ? 'Copied Email' : `${EMAIL_ADDRESS}`; + emailCopyTimer = window.setTimeout(() => { + emailCopyMachine.send('reset'); + button.innerHTML = button.dataset.baseLabel; + }, COPY_RESET_DELAY_MS); return copied; } + let parkedContactForm = null; function renderContactSuccess(mailtoLink, previewText) { const formContainer = document.getElementById('simple-contact-form'); - if (!formContainer) { + if (!formContainer || !contactMachine.send('draft')) { return; } - formContainer.innerHTML = ''; + parkedContactForm = document.createDocumentFragment(); + parkedContactForm.append(...formContainer.childNodes); const successContainer = document.createElement('div'); successContainer.className = 'text-center py-8'; @@ -375,7 +429,7 @@
- Build another draft +
`; @@ -394,6 +448,15 @@ formContainer.appendChild(successContainer); updateContactStatus('Email client ready. Use the copy button if your mail app did not open.'); bindCopyButton(); + successContainer.querySelector('#contact-edit-draft')?.addEventListener('click', () => { + if (!contactMachine.send('edit')) { + return; + } + successContainer.remove(); + formContainer.append(parkedContactForm); + parkedContactForm = null; + document.getElementById('contact-name')?.focus(); + }); } function openEmailClient() { @@ -655,7 +718,10 @@ note.style.visibility=''; note.style.left = `${point.x}px`; note.style.top = `${point.y}px`; - note.querySelector('button')?.addEventListener('click', () => note.remove()); + note.querySelector('button')?.addEventListener('click', () => { + note.remove(); + gooseTerrorState.goose?.focus({ preventScroll: true }); + }); gooseTerrorState.layer.appendChild(note); gooseTerrorState.carried=note; @@ -679,7 +745,6 @@ layer.id = 'goose-terror-layer'; layer.className = 'goose-terror-layer'; layer.dataset.gooseTerror = 'active'; - layer.setAttribute('aria-live', 'polite'); layer.innerHTML = `
Desk Goose · tap for mischief @@ -696,7 +761,8 @@ layer.appendChild(goose); document.body.appendChild(layer); - gooseTerrorState.active = true; + gooseMachine.send('release'); + gooseTerrorState.opener = document.activeElement; gooseTerrorState.layer = layer; gooseTerrorState.goose = goose; gooseTerrorState.reduced=window.matchMedia('(prefers-reduced-motion: reduce)').matches; @@ -714,11 +780,7 @@ spawnGooseNote(); if(!gooseTerrorState.reduced) gooseTerrorState.target=getGoosePoint(82,76); }); - gooseTerrorState.keyHandler = (event) => { - if (event.key === 'Escape') { - stopGooseTerror('escape'); - } - }; + overlays.open('goose', { close: () => stopGooseTerror('escape') }); gooseTerrorState.resizeHandler = () => stopGooseTerror('navigation'); gooseTerrorState.pointerHandler = event => { if(event.pointerType!=='mouse' || gooseTerrorState.reduced || event.target.closest('a,button,input,textarea,select,summary')) return; @@ -732,7 +794,6 @@ if(event.type!=='click' || event.target.closest('a[href]')) stopGooseTerror('navigation'); }; - document.addEventListener('keydown', gooseTerrorState.keyHandler); window.addEventListener('resize', gooseTerrorState.resizeHandler); document.addEventListener('pointermove',gooseTerrorState.pointerHandler); document.addEventListener('click',gooseTerrorState.navigationHandler); @@ -758,15 +819,12 @@ return false; } - [gooseTerrorState.moveTimer, gooseTerrorState.footTimer, gooseTerrorState.noteTimer].forEach((timer) => { + [gooseTerrorState.moveTimer, gooseTerrorState.noteTimer].forEach((timer) => { if (timer) { window.clearInterval(timer); } }); - - if (gooseTerrorState.keyHandler) { - document.removeEventListener('keydown', gooseTerrorState.keyHandler); - } + overlays.remove('goose'); if (gooseTerrorState.resizeHandler) { window.removeEventListener('resize', gooseTerrorState.resizeHandler); @@ -781,19 +839,23 @@ gooseTerrorState.pointerHandler=null;gooseTerrorState.navigationHandler=null; gooseTerrorState.target=null;gooseTerrorState.carried=null; + const focusWasInside = gooseTerrorState.layer?.contains(document.activeElement); gooseTerrorState.layer?.remove(); - gooseTerrorState.active = false; + gooseMachine.send('stop'); gooseTerrorState.layer = null; gooseTerrorState.goose = null; gooseTerrorState.moveTimer = null; - gooseTerrorState.footTimer = null; gooseTerrorState.noteTimer = null; - gooseTerrorState.keyHandler = null; gooseTerrorState.resizeHandler = null; updateGooseTerrorButton(); + if (focusWasInside) { + const opener = gooseTerrorState.opener?.isConnected ? gooseTerrorState.opener : document.getElementById('goose-terror-button'); + opener?.focus({ preventScroll: true }); + } + gooseTerrorState.opener = null; if (reason !== 'silent') { - hideClippy(true); + hideClippy(); const wisdom = document.getElementById('goose-wisdom'); if (wisdom) wisdom.textContent = 'Goose banished. The page is safe. Suspiciously safe.'; } @@ -815,6 +877,9 @@ } clippyElement.classList.remove('show'); } + if (helperMachine.is('text')) { + helperMachine.send('hide'); + } } function showFallbackClippy(message, durationMs = 12000) { @@ -829,6 +894,7 @@ } clippyElement.classList.add('show'); + helperMachine.send('showText'); if (clippyElement.dataset.hideTimer) { window.clearTimeout(Number(clippyElement.dataset.hideTimer)); @@ -839,8 +905,10 @@ return; } + // Expiry is not a new request, so it hides only the bubble and leaves a pending Guide request alive. const timerId = window.setTimeout(() => { - hideClippy(); + delete clippyElement.dataset.hideTimer; + hideFallbackClippy(); }, durationMs); clippyElement.dataset.hideTimer = String(timerId); } @@ -856,12 +924,13 @@ } function initRealClippy() { - if (realClippyLoadStarted) { - return realClippyLoadStarted; + if (clippyLoader.is('ready', 'loading')) { + return realClippyLoad; } + clippyLoader.send('load'); updateRealClippyGlobals(); - realClippyLoadStarted = CLIPPY_SCRIPTS + realClippyLoad = CLIPPY_SCRIPTS .reduce((chain, src) => chain.then(() => loadScript(src)), Promise.resolve()) .then(() => new Promise((resolve) => { if (!window.clippy || typeof window.clippy.load !== 'function' || !window.jQuery) { @@ -885,6 +954,7 @@ close.addEventListener('mousedown', event => event.stopPropagation()); close.addEventListener('click', event => {event.stopPropagation();hideClippy(true);}); document.querySelector('.clippy')?.appendChild(close); + clippyLoader.send('resolve'); window.dispatchEvent(new CustomEvent('portfolio:clippy-ready')); resolve(true); }, @@ -895,12 +965,17 @@ } ); })) - .catch(() => { - realClippyLoaded = false; - updateRealClippyGlobals(); - return false; + .catch(() => false) + .then((loaded) => { + // Failure is a real state now, and the next explicit request retries instead of reusing a dead promise. + if (!loaded) { + realClippyLoaded = false; + updateRealClippyGlobals(); + clippyLoader.send('reject'); + } + return loaded; }); - return realClippyLoadStarted; + return realClippyLoad; } function scheduleRealClippy() { @@ -1020,6 +1095,7 @@ : Math.max(76, window.innerHeight - 240); realClippyAgent.moveTo(left, top); realClippyAgent.show(true); + helperMachine.send('showAgent'); const selectedAnimation = selectClippyAnimation(animation); if (selectedAnimation) { @@ -1046,10 +1122,12 @@ realClippyGoodbyeTimer = window.setTimeout(() => { realClippyAgent?.hide(true); resetRealClippyQueue(realClippyAgent); + helperMachine.send('hide'); }, 1200); } else { realClippyAgent.hide(true); resetRealClippyQueue(realClippyAgent); + helperMachine.send('hide'); } }, durationMs); } @@ -1062,6 +1140,7 @@ function showClippy(message, durationMs = 12000) { hideClippy(); + helperRequest += 1; rememberHelperAppearance(); if (useTextHelper()) { showFallbackClippy(message, durationMs); return; } if (summonRealClippy(message, 'GetAttention', durationMs)) { @@ -1086,6 +1165,7 @@ function pinClippyGuide(message) { hideClippy(); + helperRequest += 1; rememberHelperAppearance(); if (useTextHelper()) { showFallbackClippy(message, 0); @@ -1105,6 +1185,7 @@ try { sessionStorage.setItem('portfolioHelperDismissed', 'true'); } catch {} } clearClippyTimers(); + helperRequest += 1; if (realClippyAgent && realClippyLoaded) { realClippyAgent.hide(true); @@ -1113,6 +1194,9 @@ } hideFallbackClippy(); + if (helperMachine.is('agent')) { + helperMachine.send('hide'); + } } function scheduleClippyIntro() { @@ -1123,8 +1207,7 @@ return; } - const clippyElement = document.getElementById('custom-clippy'); - if (clippyElement?.classList.contains('show') || document.querySelector('.clippy-balloon:not([style*="display: none"])')) { + if (!helperMachine.is('hidden')) { return; } @@ -1167,10 +1250,6 @@ } } - if (event.key === 'Escape') { - closeCommandPalette(); - } - if (event.altKey && event.shiftKey && event.code === 'KeyC') { event.preventDefault(); showClippyGuide('Old-school Clippy activated. No third-party script, no clipboard drama, just one paperclip doing its job.'); @@ -1194,8 +1273,9 @@ // Animate main, not body: a filter on body would re-anchor every fixed element for five seconds. const stage = document.getElementById('main-content'); if (stage && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + window.clearTimeout(konamiTimer); stage.style.animation = 'rainbow 2s infinite'; - window.setTimeout(() => { + konamiTimer = window.setTimeout(() => { stage.style.animation = ''; }, 5000); } @@ -1362,11 +1442,15 @@ ${role.bullets.map((bullet) => `- ${escapeHtml(bullet)}`).join('\n')}`; ); } + hireMachine.send(roleKey); document.querySelectorAll('[data-hire-role]').forEach((button) => { - const isActive = button.getAttribute('data-hire-role') === roleKey; + const isActive = button.getAttribute('data-hire-role') === hireMachine.state; button.classList.toggle('is-active', isActive); button.setAttribute('aria-selected', isActive ? 'true' : 'false'); button.tabIndex = isActive ? 0 : -1; + if (isActive) { + document.getElementById('hire-role-panel')?.setAttribute('aria-labelledby', button.id); + } }); document.querySelectorAll('[data-command-run^="hire "]').forEach((button) => { @@ -1414,9 +1498,11 @@ ${role.bullets.map((bullet) => `- ${escapeHtml(bullet)}`).join('\n')}`; } // First explicit request fetches the agent, but nobody waits more than a beat for a paperclip. + // If anything else shows or hides the helper meanwhile, this request is stale and must not reappear. + const request = ++helperRequest; let shown = false; const show = () => { - if (!shown) { + if (!shown && request === helperRequest) { shown = true; pinClippyGuide(guideMessage); } @@ -1655,9 +1741,7 @@ The flex is not perfection. The flex is range, follow-through, and enough taste } function runCommand(command) { - const response = getCommandResponse(command); - commandHistory.push(command); - appendCommandOutput(command, response); + appendCommandOutput(command, getCommandResponse(command)); } async function loadRepoSnapshot() { @@ -1683,8 +1767,9 @@ The flex is not perfection. The flex is range, follow-through, and enough taste return; } - if (palette.classList.contains('hidden')) { + if (paletteMachine.send('open')) { commandPaletteOpener = document.activeElement; + overlays.open('palette', { close: closeCommandPalette, container: palette.querySelector('.command-shell') }); } palette.classList.remove('hidden'); @@ -1706,10 +1791,11 @@ The flex is not perfection. The flex is range, follow-through, and enough taste function closeCommandPalette() { const palette = document.getElementById('command-palette'); - if (!palette || palette.classList.contains('hidden')) { + if (!palette || !paletteMachine.send('close')) { return; } + overlays.remove('palette'); palette.classList.add('hidden'); document.querySelectorAll('[data-command-open]').forEach((button) => { button.setAttribute('aria-expanded', 'false'); @@ -1721,34 +1807,9 @@ The flex is not perfection. The flex is range, follow-through, and enough taste commandPaletteOpener = null; } - function trapFocus(container, event) { - if (event.key !== 'Tab') { - return; - } - - const focusable = [...container.querySelectorAll('a[href], button, input')] - .filter((element) => !element.disabled && element.getClientRects().length); - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (!first) { - return; - } - - if (!container.contains(document.activeElement) || (event.shiftKey && document.activeElement === first)) { - event.preventDefault(); - (event.shiftKey ? last : first).focus(); - } else if (!event.shiftKey && document.activeElement === last) { - event.preventDefault(); - first.focus(); - } - } - function initCommandPalette() { const form = document.getElementById('command-form'); const input = document.getElementById('command-input'); - const palette = document.getElementById('command-palette'); - - palette?.addEventListener('keydown', (event) => trapFocus(palette, event)); document.querySelectorAll('[data-command-open]').forEach((button) => { button.addEventListener('click', () => openCommandPalette()); @@ -1866,17 +1927,14 @@ The flex is not perfection. The flex is range, follow-through, and enough taste
`; - const onKeydown = (event) => { - if (event.key === 'Escape') { - modal.closeModal(); - } else { - trapFocus(modal, event); - } - }; - + const overlayName = `dialog-${modalCount}`; modal.closeModal = () => { + if (!modal.isConnected) { + return; + } modal.remove(); - document.removeEventListener('keydown', onKeydown); + overlays.remove(overlayName); + dialogMachine.send('close'); if (opener?.isConnected && typeof opener.focus === 'function') { opener.focus({ preventScroll: true }); } @@ -1887,13 +1945,17 @@ The flex is not perfection. The flex is range, follow-through, and enough taste modal.closeModal(); } }); - document.addEventListener('keydown', onKeydown); + modal.overlayName = overlayName; return modal; } function showModal(modal) { + document.querySelectorAll('[role="dialog"][data-portfolio-dialog]').forEach((open) => open.closeModal?.()); + modal.dataset.portfolioDialog = 'true'; document.body.appendChild(modal); + dialogMachine.send('open'); + overlays.open(modal.overlayName, { close: modal.closeModal, container: modal }); modal.querySelector('[data-modal-actions] a, [data-modal-actions] button')?.focus({ preventScroll: true }); } @@ -2021,8 +2083,17 @@ The flex is not perfection. The flex is range, follow-through, and enough taste document.head.appendChild(style); } + function syncHelperWithReadingMode() { + document.addEventListener('portfolio:lab-visibility', () => { + if (helperMachine.is('agent') && useTextHelper()) { + hideClippy(); + } + }); + } + function initPage() { scheduleRealClippy(); + syncHelperWithReadingMode(); loadRepoSnapshot(); initProjectCardObserver(); initFunZone(); diff --git a/assets/js/lab-experiments.js b/assets/js/lab-experiments.js index 87f8d8e019..15a70772ca 100644 --- a/assets/js/lab-experiments.js +++ b/assets/js/lab-experiments.js @@ -15,12 +15,24 @@ const orbitOptions={thrust:1,target:0,guided:true}; const perception={coherence:1,depth:0}; const setStatus = text => status.textContent=text; const read = id => document.getElementById(id); +// Machines (docs/site/STATE.md): experiment-scene for WebGL, experiment-run for playback. +const sceneMachine = window.PortfolioState.define('experiment-scene', { + initial: 'uninitialized', + states: { uninitialized: { ready: 'ready', fail: 'unavailable' }, ready: {}, unavailable: {} } +}); +const runMachine = window.PortfolioState.define('experiment-run', { + initial: 'paused', + states: { paused: { play: 'playing' }, playing: { pause: 'paused' } } +}); function pause(message) { playing=false; + runMachine.send('pause'); renderer?.setAnimationLoop(null); play.textContent='Play'; play.setAttribute('aria-pressed','false'); if(message) setStatus(message); + // The HUD clock reads "Rotating" or "Paused", so it has to follow every pause, not just the next frame. + if(initialized) updateReadout(); } function disposeScene() { scene?.traverse(obj => { @@ -162,17 +174,18 @@ function configure(nextMode) { updateReadout();resize(); } function initialize() { - if(initialized)return; + // After one WebGL failure, scrolling past again must not rebuild the fallback and rewrite the status. + if(initialized||sceneMachine.is('unavailable'))return; try { const context=canvas.getContext('webgl2',{antialias:true}); if(!context)throw new Error('WebGL2 unavailable'); renderer=new THREE.WebGLRenderer({canvas,context,antialias:true,powerPreference:'low-power'}); renderer.setPixelRatio(Math.min(devicePixelRatio,1.5)); renderer.outputColorSpace=THREE.SRGBColorSpace; - initialized=true;canvas.hidden=false;read('experiment-fallback').hidden=true;root.dataset.webgl='available'; + initialized=true;sceneMachine.send('ready');canvas.hidden=false;read('experiment-fallback').hidden=true;root.dataset.webgl='available'; configure(mode); } catch { - controls.forEach(el=>el.disabled=true);root.dataset.webgl='unavailable'; + sceneMachine.send('fail');controls.forEach(el=>el.disabled=true);root.dataset.webgl='unavailable'; canvas.hidden=true;read('experiment-fallback').hidden=false; setStatus('WebGL unavailable. The explanation remains readable; simulation controls are disabled.'); } @@ -198,7 +211,7 @@ function togglePlay() { if(!initialized||root.hidden)return; if(playing){pause('Paused. Adjust a control or choose Play.');return;} if(orbit.done&&mode==='space'){setStatus('Reset the completed run before playing.');return;} - playing=true;lastTime=0;play.textContent='Pause';play.setAttribute('aria-pressed','true'); + playing=true;runMachine.send('play');lastTime=0;play.textContent='Pause';play.setAttribute('aria-pressed','true'); setStatus(mode==='space'?'Running at 40× simulated time.':'Rotating slowly. Pause whenever you like.'); renderer.setAnimationLoop(tick); } @@ -239,8 +252,8 @@ document.addEventListener('portfolio:lab-change',event=>{ new IntersectionObserver(entries=>{ visible=entries[0].isIntersecting&&!root.hidden; root.dataset.nearViewport=String(visible);document.dispatchEvent(new Event('portfolio:lab-visibility')); - if(visible)initialize();else pause('Paused offscreen. Choose Play to continue.'); + if(visible)initialize();else if(playing)pause('Paused offscreen. Choose Play to continue.'); },{rootMargin:'120px 0px'}).observe(stage); new ResizeObserver(resize).observe(stage); -document.addEventListener('visibilitychange',()=>{if(document.hidden)pause('Paused while the tab was hidden.');}); +document.addEventListener('visibilitychange',()=>{if(document.hidden&&playing)pause('Paused while the tab was hidden.');}); if(['space','perception'].includes(document.body.dataset.activeLab))mode=document.body.dataset.activeLab; diff --git a/assets/js/path-playground.js b/assets/js/path-playground.js index bded287409..db59ca8d6b 100644 --- a/assets/js/path-playground.js +++ b/assets/js/path-playground.js @@ -57,10 +57,30 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, } }); + // Machines (docs/site/STATE.md). drive-scene: can WebGL draw it? drive-run: is the car moving? + const sceneMachine = window.PortfolioState.define('drive-scene', { + initial: 'uninitialized', + states: { + uninitialized: { ready: 'ready', fail: 'unavailable' }, + ready: { fail: 'unavailable' }, + unavailable: {} + } + }); + const runMachine = window.PortfolioState.define('drive-run', { + initial: 'stopped', + states: { + stopped: { play: 'running' }, + running: { pause: 'stopped', finish: 'complete' }, + complete: { play: 'running', reset: 'stopped' } + } + }); + const state = { scenario: 'slalom', initialized: false, - playing: false, + get playing() { + return runMachine.is('running'); + }, progress: 0, lastTime: 0, azimuth: -0.68, @@ -514,7 +534,7 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, state.progress = simulation.nearest / (routeSamples.length-1); if (simulation.time-lastSample >= 0.1) sampleSimulation(); if (simulation.done) { - state.playing = false; + runMachine.send('finish'); stopLoop(); setStatus(simulation.reason+'. Reset the route to compare gains.'); } @@ -536,7 +556,8 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, renderer?.dispose(); renderer = null; state.initialized = false; - state.playing = false; + sceneMachine.send('fail'); + runMachine.send('pause'); setControlsEnabled(false); stage?.classList.remove('is-ready'); stage?.setAttribute('data-webgl', 'unavailable'); @@ -570,6 +591,10 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, if (state.initialized) { return true; } + // WebGL failed once already; retrying on every scroll only rewrites the same fallback message. + if (sceneMachine.is('unavailable')) { + return false; + } try { const context = canvas?.getContext('webgl2', { antialias: true, alpha: true }); @@ -609,6 +634,7 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, state.distance = getFramingDistance(); updateCamera(); state.initialized = true; + sceneMachine.send('ready'); root.dataset.pathReady = 'true'; stage?.setAttribute('data-webgl', 'available'); stage?.classList.add('is-ready'); @@ -634,7 +660,7 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, } state.scenario = key; - state.playing = false; + stopRun(); stopLoop(); updateScenarioButtons(); @@ -660,7 +686,11 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, updateVehicle(); } - state.playing = !state.playing; + if (runMachine.is('running')) { + runMachine.send('pause'); + } else { + runMachine.send('play'); + } if (state.playing) { setStatus(`${currentScenario().label} running. Gains update live.`); startLoop(); @@ -672,8 +702,26 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, updateReadout(); } + // Any state to stopped: the transition a scenario change or reset needs. + function stopRun() { + if (!runMachine.send('pause')) { + runMachine.send('reset'); + } + } + + // Pauses that the visitor did not ask for (lab switch, hidden tab, scrolled away) must say so, + // otherwise the status line keeps claiming the route is running. + function pauseFor(reason) { + if (!runMachine.send('pause')) { + return; + } + stopLoop(); + setStatus(`${currentScenario().label} paused ${reason}. Press play to resume.`); + updateReadout(); + } + function resetRoute() { - state.playing = false; + stopRun(); state.progress = 0; stopLoop(); resetSimulation(); @@ -805,13 +853,13 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, }); document.addEventListener('portfolio:lab-change', event => { if (event.detail !== 'drive') { - state.playing = false; stopLoop(); updateReadout(); + pauseFor('while another lab is open'); root.dataset.nearViewport = 'false'; document.dispatchEvent(new Event('portfolio:lab-visibility')); } }); document.addEventListener('visibilitychange', () => { - if (document.hidden) { state.playing = false; stopLoop(); updateReadout(); } + if (document.hidden) pauseFor('while the tab was hidden'); }); scenarioButtons.forEach((button) => { button.addEventListener('click', () => setScenario(button.dataset.pathScenario)); @@ -843,11 +891,8 @@ import { createDrive, stepDrive, DRIVE_DEFAULTS, canSaveDriveRun, saveDriveRun, document.dispatchEvent(new Event('portfolio:lab-visibility')); if (entry.isIntersecting) { initScene(); - } else if (state.playing) { - state.playing = false; - stopLoop(); - setStatus(`${currentScenario().label} paused offscreen. Press play to resume.`); - updateReadout(); + } else { + pauseFor('offscreen'); } }, { rootMargin: '240px 0px' }); intersectionObserver.observe(stage); diff --git a/assets/js/portfolio-labs.js b/assets/js/portfolio-labs.js index 8bd35551e1..cdd90cb96a 100644 --- a/assets/js/portfolio-labs.js +++ b/assets/js/portfolio-labs.js @@ -1,4 +1,9 @@ // Navigation and visibility only; each demo owns its model and renderer. +// Machines (see docs/site/STATE.md): lab, reconstruction, lab-drive, lab-experiments, brain-lab. +const { defineChoice, define, defineLoader } = window.PortfolioState; +const LAB_MODES = ['drive', 'space', 'perception', 'reconstruction', 'brain']; +const LAB_HASHES = { drive: 'path-playground', space: 'space-lab', perception: 'perception-lab', reconstruction: 'reconstruction-lab', brain: 'brain-lab' }; + const driving = document.getElementById('path-playground'); const experiments = document.getElementById('experiment-playground'); const reconstruction = document.getElementById('reconstruction-lab'); @@ -9,16 +14,30 @@ const closeViewer = document.getElementById('reconstruction-close'); const viewerStatus = document.getElementById('reconstruction-status'); const labReading = new Set(); +const lab = defineChoice('lab', LAB_MODES); +// A cross-origin iframe cannot report that its scene rendered, so "open" means requested and holding WebGL. +const viewer = define('reconstruction', { + initial: 'closed', + states: { closed: { open: 'open' }, open: { close: 'closed' } } +}); +const driveScene = defineLoader('lab-drive'); +const experimentScene = defineLoader('lab-experiments'); +const brainScene = defineLoader('brain-lab'); +// Once a visitor picks a lab, the page's late load event must not snap them back to the URL's lab. +let labChosenByVisitor = false; + function unloadReconstruction(message = 'Viewer closed. Load it again whenever you like.') { - if (!frameSlot?.firstChild) return; + if (!viewer.send('close')) return; + const focusWasOnClose = closeViewer.contains(document.activeElement); frameSlot.replaceChildren(); // Removing the browsing context stops its WebGL, media and requests. frameSlot.hidden = true; loadViewer.hidden = false; closeViewer.hidden = true; viewerStatus.textContent = message; + if (focusWasOnClose && !reconstruction.hidden) loadViewer.focus({preventScroll:true}); } loadViewer?.addEventListener('click', () => { - if (frameSlot.firstChild) return; + if (!viewer.send('open')) return; const frame = document.createElement('iframe'); frame.title = 'FPV reconstruction: point cloud, camera path and method comparison'; frame.referrerPolicy = 'no-referrer'; @@ -29,33 +48,41 @@ loadViewer?.addEventListener('click', () => { loadViewer.hidden = true; closeViewer.hidden = false; closeViewer.focus({preventScroll:true}); - // A cross-origin iframe load event cannot prove that its scene rendered. viewerStatus.textContent = 'Viewer requested below. If it stays blank or reports an error, open the full viewer. Close releases the scene.'; }); closeViewer?.addEventListener('click', () => { unloadReconstruction(); loadViewer.focus({preventScroll:true}); }); +// Release the scene's WebGL once the panel is fully off screen. The reading observer below uses a shrunken +// root, so it fires while the panel is still partly visible and cannot make this call. +if (reconstruction) { + new IntersectionObserver(entries => { + if (!entries[0].isIntersecting) unloadReconstruction('Viewer closed offscreen. Load it again to continue.'); + }).observe(reconstruction); +} document.addEventListener('portfolio:lab-visibility', () => { document.body.classList.toggle('path-scene-near', labReading.size > 0 || [driving, experiments].some(panel => panel && !panel.hidden && panel.dataset.nearViewport === 'true')); }); + // The connectome data and three.js load only when someone opens this lab. -let brainRequested = false; function openBrainLab() { - if (brainRequested) return; - brainRequested = true; + if (!brainScene.send('load')) return; const status = document.getElementById('brain-lab-status'); if (!('WebGL2RenderingContext' in window)) { + brainScene.send('reject'); status.textContent = 'This view needs WebGL. The picture shows the same neurons.'; return; } - import('./fly-brain.js?v=20261106b') + status.textContent = 'Loading neuron positions…'; + import('./fly-brain.js?v=20261108') .then(({ mountBrain }) => mountBrain({ stage: document.getElementById('brain-lab-stage'), canvas: document.getElementById('brain-lab-canvas'), framing: 'panel' })) .then((brain) => { + brainScene.send('resolve'); status.textContent = 'Drag to rotate. Switch a region off to look inside.'; document.querySelectorAll('[data-brain-region]').forEach((button) => { button.disabled = false; @@ -68,12 +95,14 @@ function openBrainLab() { }); }) .catch(() => { - brainRequested = false; + brainScene.send('reject'); status.textContent = 'The neuron data did not load. Reopen this lab to try again.'; }); } + function selectLab(mode, updateHash = false) { - if (!['drive','space','perception','reconstruction','brain'].includes(mode)) return; + if (!LAB_MODES.includes(mode)) return; + lab.send(mode); if (mode !== 'reconstruction') unloadReconstruction(); if (driving) driving.hidden = mode !== 'drive'; if (experiments) experiments.hidden = !['space','perception'].includes(mode); @@ -86,16 +115,22 @@ function selectLab(mode, updateHash = false) { document.querySelectorAll('[data-experiment-copy]').forEach(el => el.hidden = el.dataset.experimentCopy !== mode); document.body.dataset.activeLab = mode; document.dispatchEvent(new CustomEvent('portfolio:lab-change', { detail: mode })); - if (updateHash) history.replaceState(null, '', '#' + ({drive:'path-playground',space:'space-lab',perception:'perception-lab',reconstruction:'reconstruction-lab',brain:'brain-lab'}[mode])); + if (updateHash) history.replaceState(null, '', '#' + LAB_HASHES[mode]); } -document.querySelectorAll('[data-lab-mode]').forEach(button => button.addEventListener('click', () => selectLab(button.dataset.labMode, true))); -document.querySelectorAll('[data-lab-target]').forEach(link => link.addEventListener('click', () => selectLab(link.dataset.labTarget, true), {capture:true})); +const chooseLab = (mode) => { + labChosenByVisitor = true; + selectLab(mode, true); +}; +document.querySelectorAll('[data-lab-mode]').forEach(button => button.addEventListener('click', () => chooseLab(button.dataset.labMode))); +document.querySelectorAll('[data-lab-target]').forEach(link => link.addEventListener('click', () => chooseLab(link.dataset.labTarget), {capture:true})); function readHash() { - const mode = {'#path-playground':'drive','#space-lab':'space','#perception-lab':'perception','#reconstruction-lab':'reconstruction','#brain-lab':'brain'}[location.hash]; + const mode = Object.keys(LAB_HASHES).find(key => '#' + LAB_HASHES[key] === location.hash); if (mode) { selectLab(mode); requestAnimationFrame(() => document.getElementById('lab-picker')?.scrollIntoView({block:'start',behavior:'instant'})); } } window.addEventListener('hashchange', readHash); -window.addEventListener('load', readHash); +window.addEventListener('load', () => { + if (!labChosenByVisitor) readHash(); +}); selectLab('drive'); readHash(); @@ -107,10 +142,6 @@ const readingObserver = new IntersectionObserver(entries => { else if (entry.target.matches('#lab-picker, .path-playground')) { if (entry.isIntersecting) labReading.add(entry.target); else labReading.delete(entry.target); - if (entry.target === reconstruction && !entry.isIntersecting) { - const bounds = reconstruction.getBoundingClientRect(); - if (bounds.bottom <= 0 || bounds.top >= innerHeight) unloadReconstruction('Viewer closed offscreen. Load it again to continue.'); - } } else if (entry.isIntersecting) quietSections.add(entry.target); else quietSections.delete(entry.target); @@ -121,13 +152,20 @@ const readingObserver = new IntersectionObserver(entries => { document.querySelectorAll('#hero, #about, #contact, #lab-picker, .path-playground').forEach(el => readingObserver.observe(el)); // three.js is ~750 KB, so the scenes load when a lab gets close or the page goes idle, never before first paint. -let labScenesRequested = false; +// Each scene has its own loader, so one failure no longer reports the other as broken, and a failed one retries. +function loadScene(loader, path, statusId) { + if (!loader.send('load')) return; + import(path) + .then(() => loader.send('resolve')) + .catch(() => { + loader.send('reject'); + const status = document.getElementById(statusId); + if (status) status.textContent = 'This lab failed to load. Scroll back here or refresh to try again.'; + }); +} function loadLabScenes() { - if (labScenesRequested) return; - labScenesRequested = true; - labLoadObserver.disconnect(); - Promise.all([import('./path-playground.js?v=20261102'), import('./lab-experiments.js?v=20261102')]) - .catch(() => document.querySelectorAll('#path-status, #experiment-status').forEach(el => { el.textContent = 'Lab failed to load. Refresh to try again.'; })); + loadScene(driveScene, './path-playground.js?v=20261108', 'path-status'); + loadScene(experimentScene, './lab-experiments.js?v=20261108', 'experiment-status'); } const labLoadObserver = new IntersectionObserver(entries => { if (entries.some(entry => entry.isIntersecting)) loadLabScenes(); diff --git a/assets/js/site-shell.js b/assets/js/site-shell.js index 3dc51830ca..1705222274 100644 --- a/assets/js/site-shell.js +++ b/assets/js/site-shell.js @@ -102,8 +102,17 @@ const mobileMenuButtonParent = mobileMenuButton.parentElement; const mobileMenuButtonNextSibling = mobileMenuButton.nextSibling; - const hamburgerIcon = mobileMenuButton.querySelector('i'); let closeTimer = null; + // opening and closing are the 300 ms slide; the panel only takes input in open. + const menu = window.PortfolioState.define('menu', { + initial: 'closed', + states: { + closed: { open: 'opening' }, + opening: { opened: 'open', close: 'closing' }, + open: { close: 'closing' }, + closing: { closed: 'closed', open: 'opening' } + } + }); cloneDesktopLinks(mobileNavList, desktopNav); initSmoothAnchors(); @@ -124,37 +133,51 @@ } }; - const closeMobileMenu = () => { + // returnFocus is false when a nav link closes the drawer: the anchor handler has already moved focus to the section. + const closeMobileMenu = (returnFocus = true) => { + if (!menu.send('close')) { + return; + } + window.PortfolioState.overlays.remove('menu'); mobileMenuPanel.classList.add('translate-x-full'); mobileMenu.classList.add('pointer-events-none'); mobileMenuButton.classList.remove('mobile-menu-button-open'); mobileMenuButton.setAttribute('aria-expanded', 'false'); - hamburgerIcon?.classList.replace('fa-times', 'fa-bars'); restoreMobileMenuButton(); - mobileMenuButton.focus({preventScroll:true}); + if (returnFocus) { + mobileMenuButton.focus({ preventScroll: true }); + } window.clearTimeout(closeTimer); closeTimer = window.setTimeout(() => { + closeTimer = null; + if (!menu.send('closed')) { + return; + } mobileMenu.classList.add('hidden'); mobileMenu.classList.remove('pointer-events-none'); document.body.style.overflow = ''; - closeTimer = null; }, 300); }; const openMobileMenu = () => { + if (!menu.send('open')) { + return; + } window.clearTimeout(closeTimer); mobileMenu.classList.remove('pointer-events-none'); mobileMenu.classList.remove('hidden'); mobileMenuButton.classList.add('mobile-menu-button-open'); mobileMenuButton.setAttribute('aria-expanded', 'true'); document.body.appendChild(mobileMenuButton); + window.PortfolioState.overlays.open('menu', { close: () => closeMobileMenu(), container: mobileMenuPanel }); requestAnimationFrame(() => { - if (mobileMenuButton.getAttribute('aria-expanded') !== 'true') return; + if (!menu.send('opened')) { + return; + } mobileMenuPanel.classList.remove('translate-x-full'); - hamburgerIcon?.classList.replace('fa-bars', 'fa-times'); - mobileMenuClose?.focus({preventScroll:true}); + mobileMenuClose?.focus({ preventScroll: true }); }); document.body.style.overflow = 'hidden'; @@ -162,8 +185,7 @@ mobileMenuButton.addEventListener('click', (event) => { event.preventDefault(); - const isOpen = mobileMenuButton.getAttribute('aria-expanded') === 'true'; - if (isOpen) { + if (menu.is('open', 'opening')) { closeMobileMenu(); } else { openMobileMenu(); @@ -175,23 +197,16 @@ closeMobileMenu(); }); - mobileMenuBackdrop?.addEventListener('click', closeMobileMenu); + mobileMenuBackdrop?.addEventListener('click', () => closeMobileMenu()); mobileMenu.querySelectorAll('.mobile-nav-link').forEach((link) => { - link.addEventListener('click', closeMobileMenu); + link.addEventListener('click', () => closeMobileMenu(false)); }); - document.addEventListener('keydown', (event) => { - if (event.key === 'Tab' && mobileMenuButton.getAttribute('aria-expanded') === 'true') { - const focusable = [...mobileMenuPanel.querySelectorAll('a[href], button, summary')].filter(el => !el.disabled && el.getClientRects().length && !el.matches('details:not([open]) a')); - const first = focusable[0], last = focusable[focusable.length - 1]; - if (first && (!mobileMenuPanel.contains(document.activeElement) || (event.shiftKey && document.activeElement === first) || (!event.shiftKey && document.activeElement === last))) { - event.preventDefault(); - (event.shiftKey ? last : first).focus(); - } - } - if (event.key === 'Escape' && mobileMenuButton.getAttribute('aria-expanded') === 'true') { - closeMobileMenu(); + // The drawer only exists below md; widening the window must not leave its overlay or scroll lock behind. + window.matchMedia('(min-width: 768px)').addEventListener('change', (event) => { + if (event.matches) { + closeMobileMenu(false); } }); diff --git a/assets/js/state-machine.js b/assets/js/state-machine.js new file mode 100644 index 0000000000..a4e5caa9a5 --- /dev/null +++ b/assets/js/state-machine.js @@ -0,0 +1,152 @@ +// Explicit state machines for the portfolio's interactive parts. +// Every machine mirrors its state onto as data--state, so CSS, tests and agents read one attribute +// instead of inferring state from classes, timers or `hidden`. docs/site/STATE.md lists every machine. +(function () { + 'use strict'; + + const machines = new Map(); + const root = document.documentElement; + + /** + * define('menu', { initial: 'closed', states: { closed: { open: 'opening' }, ... } }) + * send(event) returns false and changes nothing when the current state has no such event, + * which makes re-entry, double clicks and late timers harmless by construction. + */ + function define(name, { initial, states }) { + if (!(initial in states)) { + throw new Error(`State machine "${name}" has no initial state "${initial}"`); + } + + let current = initial; + const attribute = `data-${name}-state`; + root.setAttribute(attribute, current); + + const machine = { + name, + get state() { + return current; + }, + is(...candidates) { + return candidates.includes(current); + }, + can(event) { + return Boolean(states[current][event]); + }, + send(event, detail) { + const next = states[current][event]; + if (!next) { + return false; + } + const from = current; + current = next; + root.setAttribute(attribute, current); + document.dispatchEvent(new CustomEvent('portfolio:state', { + detail: { machine: name, from, to: next, event, ...detail } + })); + return true; + } + }; + + machines.set(name, machine); + return machine; + } + + // The common lazy-load shape: idle → loading → ready | failed, with retry from failed. + function defineLoader(name) { + return define(name, { + initial: 'idle', + states: { + idle: { load: 'loading' }, + loading: { resolve: 'ready', reject: 'failed' }, + ready: {}, + failed: { load: 'loading' } + } + }); + } + + // Exclusive selections (lab picker, hire tabs): any option can follow any other, and the event is the option's name. + function defineChoice(name, options, initial = options[0]) { + const states = Object.fromEntries(options.map((option) => [ + option, + Object.fromEntries(options.filter((other) => other !== option).map((other) => [other, other])) + ])); + return define(name, { initial, states }); + } + + // Overlays (drawer, palette, dialogs, goose) form a stack. Escape closes only the top one and only the top one + // traps Tab, so layered overlays no longer fight over focus or all close on one key press. + const overlayStack = []; + const overlays = { + open(name, { close, container = null }) { + overlays.remove(name); + overlayStack.push({ name, close, container }); + root.setAttribute('data-overlay-top', name); + }, + remove(name) { + const index = overlayStack.findIndex((entry) => entry.name === name); + if (index !== -1) { + overlayStack.splice(index, 1); + } + const top = overlayStack[overlayStack.length - 1]; + if (top) { + root.setAttribute('data-overlay-top', top.name); + } else { + root.removeAttribute('data-overlay-top'); + } + }, + top() { + return overlayStack[overlayStack.length - 1]?.name || null; + } + }; + + function focusableIn(container) { + return [...container.querySelectorAll('a[href], button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])')] + .filter((element) => !element.disabled && element.getClientRects().length && !element.closest('details:not([open]) > :not(summary)')); + } + + document.addEventListener('keydown', (event) => { + const top = overlayStack[overlayStack.length - 1]; + if (!top) { + return; + } + + if (event.key === 'Escape') { + event.preventDefault(); + event.stopImmediatePropagation(); + top.close('escape'); + return; + } + + if (event.key !== 'Tab' || !top.container) { + return; + } + + const focusable = focusableIn(top.container); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (!first) { + return; + } + if (!top.container.contains(document.activeElement) || (event.shiftKey && document.activeElement === first)) { + event.preventDefault(); + (event.shiftKey ? last : first).focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + }, true); + + window.PortfolioState = { + define, + defineLoader, + defineChoice, + overlays, + get(name) { + return machines.get(name) || null; + }, + // One call for tests and agents: every machine's current state. + snapshot() { + return Object.fromEntries([...machines].map(([name, machine]) => [name, machine.state])); + } + }; +})(); diff --git a/docs/README.md b/docs/README.md index ae6f1f268e..0712878246 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,6 @@ This folder organizes the repository's public documentation. GitHub Pages publishes from `dev` at `/`, so files under `docs/` are publicly accessible. Keep private notes and personal source documents outside this repository. -- `site/` contains current product, design, setup, and vocabulary context. +- `site/` contains current product, design, setup, and vocabulary context. `site/STATE.md` lists every interactive state machine. - `archive/` contains older audit and refactor notes that are useful history but not active instructions. - `superpowers/` contains the planning/spec artifacts from the audit and redesign pass. diff --git a/docs/site/STATE.md b/docs/site/STATE.md new file mode 100644 index 0000000000..7c496bbfe7 --- /dev/null +++ b/docs/site/STATE.md @@ -0,0 +1,70 @@ +# Interactive State + +Every interactive part of the site is an explicit state machine built with +`assets/js/state-machine.js`. A machine has one named state and a fixed list of +allowed transitions. Its state is mirrored onto `` as `data--state`. + +## Reading state + +- **In the browser console, a test or an agent:** `PortfolioState.snapshot()` + returns every machine's current state, for example + `{ menu: 'closed', palette: 'open', lab: 'drive', ... }`. +- **From the DOM:** read `document.documentElement.dataset`, or select on the + attribute: `html[data-palette-state="open"]`. +- **Watching changes:** listen for `portfolio:state` on `document`. + `event.detail` is `{ machine, from, to, event }`. +- **Overlays:** `` names the overlay that + currently owns Escape and Tab. + +Prefer these over inferring state from classes, `hidden`, opacity or text. + +## Rules + +- **Change state only with `machine.send(event)`.** It returns `false` and + changes nothing when the current state has no such event. Use that return + value to guard re-entry, double clicks and late timers. Do not add parallel + booleans. +- **Use the shared shapes.** + - `defineLoader(name)` gives `idle → loading → ready | failed`, and `failed` + can retry. + - `defineChoice(name, options)` gives exclusive selections. +- **Overlays register with `PortfolioState.overlays.open(name, { close, + container })`** and unregister with `overlays.remove(name)` when they close. + Escape closes only the top overlay, and only the top overlay traps Tab. Do not + add your own Escape or focus-trap listeners. +- **When you add a machine,** add a row below and a test that reads its + `data--state`. + +## Machines + +| Machine | Page | States | Owner | +| --- | --- | --- | --- | +| `menu` | index, photo | closed → opening → open → closing → closed (opening and closing are the 300 ms slide) | `site-shell.js` | +| `palette` | index | closed, open | `index-page.js` | +| `dialog` | index | closed, open (retro source and nomination dialogs; only one at a time) | `index-page.js` | +| `goose` | index | idle, loose (goose terror mode) | `index-page.js` | +| `clippy` | index | idle, loading, ready, failed (the animated agent's scripts and sprites) | `index-page.js` | +| `helper` | index | hidden, text, agent (which helper is on screen) | `index-page.js` | +| `contact` | index | editing, drafted ("Build another draft" returns to editing and keeps what was typed) | `index-page.js` | +| `email-copy` | index | idle, copying, copied, failed | `index-page.js` | +| `hire` | index | robotics, autonomy, simulation, ai | `index-page.js` | +| `lab` | index | drive, space, perception, reconstruction, brain | `portfolio-labs.js` | +| `reconstruction` | index | closed, open (the cross-origin viewer iframe is requested and holding WebGL) | `portfolio-labs.js` | +| `lab-drive`, `lab-experiments`, `brain-lab` | index | loader states for each lab's code and data | `portfolio-labs.js` | +| `drive-scene` | index | uninitialized, ready, unavailable (WebGL) | `path-playground.js` | +| `drive-run` | index | stopped, running, complete | `path-playground.js` | +| `experiment-scene` | index | uninitialized, ready, unavailable (WebGL) | `lab-experiments.js` | +| `experiment-run` | index | paused, playing | `lab-experiments.js` | +| `brain-hero` | fly101 | loader states for the rotating brain | `fly-brain.js` | + +## Not machines + +These are derived signals, not states, so they stay as they are: + +- **Reading position on ``:** `quiet-reading`, `path-scene-near` and + `hero-intro-near` are recomputed from intersection observers. +- **The scrolled nav background.** +- **The click counter and the retro source counter.** + +`photo-page.js` (the gallery) is not converted yet. Photo changes need the +owner's approval first. diff --git a/fly101.html b/fly101.html index 893909b5a6..ad8457a768 100644 --- a/fly101.html +++ b/fly101.html @@ -190,7 +190,8 @@

Sources and credits

- + + diff --git a/index.html b/index.html index bb3d18103a..904877e54d 100644 --- a/index.html +++ b/index.html @@ -228,7 +228,7 @@
-
+
Hey there! Looking to hire an awesome engineer? I'm here to help.
@@ -427,18 +427,18 @@

Recruiters shouldn’t need a treasure map. Pick the role you’re hiring for and I’ll line up the projects, the tools and the honest reason I fit.

- - - -

-
+
FIT_PACKET @@ -1434,8 +1434,9 @@

Specializations

- - - + + + + diff --git a/photo.html b/photo.html index fac0996aa0..6d8dd6c41b 100644 --- a/photo.html +++ b/photo.html @@ -512,7 +512,8 @@

Photography

- + + diff --git a/tests/smoke.spec.js b/tests/smoke.spec.js index 09be769bd9..2572f1e0e7 100644 --- a/tests/smoke.spec.js +++ b/tests/smoke.spec.js @@ -128,6 +128,79 @@ test.describe('maintained smoke suite', () => { expect(errors).toEqual([]); }); + test('interactive state is explicit and survives layered overlays, double clicks and drafts', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== DESKTOP_PROJECT, 'State contract runs once on desktop'); + const errors = []; + page.on('pageerror', (error) => errors.push(error.message)); + await page.addInitScript(() => { + window.__portfolioMailtoHandler = () => {}; + Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: async () => {} } }); + }); + await page.goto('/', { waitUntil: 'domcontentloaded' }); + const html = page.locator('html'); + + // docs/site/STATE.md: every machine is listed by PortfolioState.snapshot() and mirrored on . + const snapshot = await page.evaluate(() => window.PortfolioState.snapshot()); + for (const name of ['menu', 'palette', 'dialog', 'goose', 'clippy', 'helper', 'contact', 'email-copy', 'hire', 'lab', 'reconstruction', 'lab-drive', 'lab-experiments', 'brain-lab']) { + expect(snapshot, `machine ${name}`).toHaveProperty(name); + } + expect(snapshot).toMatchObject({ menu: 'closed', palette: 'closed', goose: 'idle', lab: 'drive', hire: 'robotics', contact: 'editing' }); + + // Escape closes only the top overlay: the palette first, then the goose under it. + // Any scroll ends a goose visit by design, so scroll first, then release it. + const gooseButton = page.locator('#goose-terror-button'); + await gooseButton.scrollIntoViewIfNeeded(); + await page.waitForTimeout(300); + await gooseButton.click(); + await expect(html).toHaveAttribute('data-goose-state', 'loose'); + await page.keyboard.press('/'); + await expect(html).toHaveAttribute('data-palette-state', 'open'); + await expect(html).toHaveAttribute('data-overlay-top', 'palette'); + await page.keyboard.press('Escape'); + await expect(html).toHaveAttribute('data-palette-state', 'closed'); + await expect(html).toHaveAttribute('data-goose-state', 'loose'); + await page.keyboard.press('Escape'); + await expect(html).toHaveAttribute('data-goose-state', 'idle'); + await expect(html).not.toHaveAttribute('data-overlay-top', /.+/); + + await page.locator('[data-hire-role="ai"]').click(); + await expect(html).toHaveAttribute('data-hire-state', 'ai'); + await expect(page.locator('#hire-role-panel')).toHaveAttribute('aria-labelledby', 'hire-tab-ai'); + + // A double click used to leave "Copied Email" on the button for good. + const copyEmail = page.locator('#copy-email-address'); + const baseLabel = (await copyEmail.innerText()).trim(); + await copyEmail.click(); + await copyEmail.click(); + await expect(copyEmail).toHaveText(baseLabel, { timeout: 5000 }); + await expect(html).toHaveAttribute('data-email-copy-state', 'idle'); + + // "Build another draft" returns to the form with what was typed. + const fields = await fillContactForm(page, { name: 'State Tester' }); + await expect(html).toHaveAttribute('data-contact-state', 'drafted'); + await page.locator('#contact-edit-draft').click(); + await expect(html).toHaveAttribute('data-contact-state', 'editing'); + await expect(page.locator('#contact-name')).toHaveValue(fields.name); + await expect(page.locator('#contact-name')).toBeFocused(); + + await page.locator('[data-lab-mode="space"]').click(); + await expect(html).toHaveAttribute('data-lab-state', 'space'); + + // Drawer: explicit states, Escape closes it, and a nav link leaves focus on the section it jumped to. + await page.setViewportSize({ width: 390, height: 844 }); + await page.locator('#mobile-menu-button').click(); + await expect(html).toHaveAttribute('data-menu-state', 'open'); + await page.keyboard.press('Escape'); + await expect(html).toHaveAttribute('data-menu-state', 'closed'); + await page.locator('#mobile-menu-button').click(); + await expect(html).toHaveAttribute('data-menu-state', 'open'); + await page.locator('#mobile-menu .mobile-nav-link[href="#about"]').first().click(); + await expect(html).toHaveAttribute('data-menu-state', 'closed'); + await expect(page.locator('#about')).toBeFocused(); + + expect(errors).toEqual([]); + }); + test('connectome lab loads its data only on request and region switches work', async ({ page }, testInfo) => { test.skip(testInfo.project.name !== DESKTOP_PROJECT, 'Lab lifecycle runs once on desktop'); const errors = [];