diff --git a/main.go b/main.go index 6404519..f03ad82 100644 --- a/main.go +++ b/main.go @@ -1,270 +1,157 @@ package main import ( - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "flag" - "fmt" + "embed" + "html/template" + "io/fs" "log" "net/http" - "os" - "strconv" - "sync" - "time" - - "github.com/joho/godotenv" - "github.com/pquerna/otp/totp" ) -type Config struct { - Job string `json:"job"` - Emails []string `json:"emails"` - NewsSent int `json:"newletter-sent"` -} - -func loadConfig() (*Config, error) { - data, err := os.ReadFile("data.json") - if err != nil { - return nil, err - } - var c Config - return &c, json.Unmarshal(data, &c) -} - -func saveConfig(c *Config) error { - data, err := json.MarshalIndent(c, "", " ") - if err != nil { - return err - } - return os.WriteFile("data.json", data, 0644) -} - -func validateEnv() { - // checking if env file is there in the directory: - err := godotenv.Load(".env") - if err != nil { - log.Fatal("Error loading .env file") - } - - // checking secret for rooling 2fa - secret := os.Getenv("secret") - if secret == "" { - fmt.Println("set secret first") - os.Exit(1) - } -} +//go:embed source static +var sourceFS embed.FS func main() { - validateEnv() - - port := flag.String("port", "8081", "port to listen on") - flag.Parse() - - mux := http.NewServeMux() - - fs := http.FileServer(http.Dir("static")) - mux.Handle("/static/", http.StripPrefix("/static/", fs)) - - mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/index.html") }) - mux.HandleFunc("GET /infra", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/infra.html") }) - mux.HandleFunc("GET /finance", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/finance.html") }) - mux.HandleFunc("GET /cinema", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cinema.html") }) - mux.HandleFunc("GET /engine", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/engine.html") }) - mux.HandleFunc("GET /42", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/42.html") }) - mux.HandleFunc("GET /secret", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/secret.html") }) - mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/icon.png") }) - mux.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { - - cookie, err := r.Cookie("session") - if err == nil { - sessionMu.RLock() - expiry, ok := sessionStore[cookie.Value] - sessionMu.RUnlock() - // had session, but invalid - if !ok || time.Now().After(expiry) { - sessionMu.Lock() - delete(sessionStore, cookie.Value) - sessionMu.Unlock() - } else { // valid session - http.Redirect(w, r, "/admin", http.StatusSeeOther) - } - } - - http.ServeFile(w, r, "./static/login.html") - }) - - mux.HandleFunc("GET /admin", requireAuth(func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/admin.html") - })) - - mux.HandleFunc("GET /logout", requireAuth(func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("session") - if err == nil { - // delete session cookie token - sessionMu.Lock() - delete(sessionStore, cookie.Value) - sessionMu.Unlock() - } - - http.Redirect(w, r, "/", http.StatusSeeOther) - })) - - mux.HandleFunc("POST /newsletter/subscribe", func(w http.ResponseWriter, r *http.Request) { - var req struct { - Email string `json:"email"` - } - - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - double := false - - conf, err := loadConfig() - for _, e := range conf.Emails { - if e == req.Email { - double = true - } - } - if !double { - conf.Emails = append(conf.Emails, req.Email) - } - saveConfig(conf) - }) - - mux.HandleFunc("GET /status", func(w http.ResponseWriter, r *http.Request) { - config, err := loadConfig() - if err != nil { - http.Error(w, "failed to load config", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/html") - - //samantha42.xyz - - if config.Job == "" || config.Job == "none" { - fmt.Fprint(w, `available for work`) - } else { - fmt.Fprintf(w, `working at: %s`, config.Job) - } - }) - - mux.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - password := r.Form.Get("password") - auth := r.Form.Get("totp") - - // cheking password - h := sha256.New() - h.Write([]byte(password)) - b := hex.EncodeToString(h.Sum(nil)) - - if string(b) == os.Getenv("password_sha") && totp.Validate(auth, os.Getenv("secret")) { - fmt.Println("logged in") - token := generateSession() - - sessionMu.Lock() - sessionStore[token] = time.Now().Add(12 * time.Hour) - sessionMu.Unlock() - - http.SetCookie(w, &http.Cookie{ - Name: "session", - Value: token, - Path: "/", - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteLaxMode, - Expires: time.Now().Add(48 * time.Hour), - }) - - // htmx reads this header and does the redirect client-side - w.Header().Set("HX-Redirect", "/admin") - w.WriteHeader(http.StatusOK) - } else { - // failure - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, `
// incorrect password or 2FA code
`) - } - }) - - //http.HandleFunc("/portfolio", Portfolio) - mux.HandleFunc("/git", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "https://git.samantha42.xyz", http.StatusFound) - }) - - // --- /admin/ sub-mux --- - adminMux := http.NewServeMux() - adminMux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/admin.html") - }) - - // strip the /admin prefix before handing off - mux.Handle("/admin/", http.StripPrefix("/admin", adminMux)) - - // --- /admin/api/ sub-mux --- - apiMux := http.NewServeMux() - apiMux.HandleFunc("GET /stats/subscribers", requireAuth(func(w http.ResponseWriter, r *http.Request) { - conf, err := loadConfig() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - - } - - fmt.Fprintf(w, "%s", strconv.Itoa(len(conf.Emails))) - })) - - apiMux.HandleFunc("GET /stats/sent", requireAuth(func(w http.ResponseWriter, r *http.Request) { - conf, err := loadConfig() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - - } - fmt.Fprintf(w, "%s", strconv.Itoa(conf.NewsSent)) - })) - - // strip /admin/api before handing off - adminMux.Handle("/api/", http.StripPrefix("/api", apiMux)) - - fmt.Printf("running on http://localhost:%s/\n", *port) - if err := http.ListenAndServe(":"+*port, mux); err != nil { + staticRoot, err := fs.Sub(sourceFS, "source") + if err != nil { log.Fatal(err) } -} -var sessionStore = map[string]time.Time{} -var sessionMu sync.RWMutex - -func generateSession() string { - b := make([]byte, 32) - rand.Read(b) - return hex.EncodeToString(b) -} - -func requireAuth(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("session") + // print exactly what got embedded + fs.WalkDir(staticRoot, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { - http.Redirect(w, r, "/login", http.StatusSeeOther) + log.Println("walk error:", err) + return nil + } + log.Println("embedded:", path) + return nil + }) + + mux := http.NewServeMux() + mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFS(staticRoot, "templates/main.html", "pages/index.html") + if err != nil { + log.Println("parse error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) return } - sessionMu.RLock() - expiry, ok := sessionStore[cookie.Value] - sessionMu.RUnlock() + data := struct { + Title string + Sub bool + }{ + Title: "home", + Sub: false, + } - if !ok || time.Now().After(expiry) { - sessionMu.Lock() - delete(sessionStore, cookie.Value) - sessionMu.Unlock() - http.Redirect(w, r, "/login", http.StatusSeeOther) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // execute the base file by name — main.html is the entrypoint + if err := tmpl.ExecuteTemplate(w, "main.html", data); err != nil { + log.Println("execute error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + mux.HandleFunc("GET /research", func(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFS(staticRoot, "templates/main.html", "pages/research.html") + if err != nil { + log.Println("parse error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) return } - next(w, r) - } + data := struct { + Title string + Sub bool + }{ + Title: "research", + Sub: true, + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // execute the base file by name — main.html is the entrypoint + if err := tmpl.ExecuteTemplate(w, "main.html", data); err != nil { + log.Println("execute error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + mux.HandleFunc("GET /bookkeeping", func(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFS(staticRoot, "templates/main.html", "pages/bookkeeping.html") + if err != nil { + log.Println("parse error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := struct { + Title string + Sub bool + }{ + Title: "bookkeeping", + Sub: true, + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // execute the base file by name — main.html is the entrypoint + if err := tmpl.ExecuteTemplate(w, "main.html", data); err != nil { + log.Println("execute error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + mux.HandleFunc("GET /automation", func(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFS(staticRoot, "templates/main.html", "pages/automation.html") + if err != nil { + log.Println("parse error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := struct { + Title string + Sub bool + }{ + Title: "automation", + Sub: true, + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // execute the base file by name — main.html is the entrypoint + if err := tmpl.ExecuteTemplate(w, "main.html", data); err != nil { + log.Println("execute error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + mux.HandleFunc("GET /interests", func(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFS(staticRoot, "templates/main.html", "pages/interests.html") + if err != nil { + log.Println("parse error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := struct { + Title string + Sub bool + }{ + Title: "interests", + Sub: true, + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // execute the base file by name — main.html is the entrypoint + if err := tmpl.ExecuteTemplate(w, "main.html", data); err != nil { + log.Println("execute error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + assetsRoot, err := fs.Sub(sourceFS, "static") + + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServerFS(assetsRoot))) + + log.Println("running on http://localhost:8081/") + log.Fatal(http.ListenAndServe(":8081", mux)) } diff --git a/site b/site index b5061d0..12c902b 100755 Binary files a/site and b/site differ diff --git a/static/42.html b/source/42.html similarity index 100% rename from static/42.html rename to source/42.html diff --git a/static/admin.html b/source/admin.html similarity index 100% rename from static/admin.html rename to source/admin.html diff --git a/static/cinema.html b/source/cinema.html similarity index 100% rename from static/cinema.html rename to source/cinema.html diff --git a/static/cyber.html b/source/cyber.html similarity index 100% rename from static/cyber.html rename to source/cyber.html diff --git a/static/engine.html b/source/engine.html similarity index 100% rename from static/engine.html rename to source/engine.html diff --git a/static/finance.html b/source/finance.html similarity index 100% rename from static/finance.html rename to source/finance.html diff --git a/static/icon.png b/source/icon.png similarity index 100% rename from static/icon.png rename to source/icon.png diff --git a/source/index.html b/source/index.html new file mode 100644 index 0000000..b70d288 --- /dev/null +++ b/source/index.html @@ -0,0 +1,208 @@ + + + + + + + Samantha Vero Friis + + + + + + + + + + +
+ +
+
+

About Me · 2026

+

Samantha
Vero Friis +

+

Bachelor's student in Business Administration seeking an entry-level analyst or + finance-related role while studying. Strong numerical skills, advanced Excel, and experience in company, risk, + and financial analysis. Analytical, detail-oriented, and efficiency-focused.

+
+ +
+ samantha42.xyz + me@samantha42.xyz +
+
+
+
+

experience

+
+
+ + + + +
+
+
+
+

education

+
+
+
+
2025 — present
+
+
Bachelor of Economics & Business Administration (HA)
+
Aalborg University · Expected 2028
+
+
+
+
2024 — 2025 +
+ discontinued +
+
+
Mathematics-Technology (BSc)
+
Aalborg University · 1 semester
+ +
+
+ +
+
2020 — 2023
+
+
HTX – Technical Upper Secondary Education
+
Viden Gymnasier
+
Mathematics A, Biology B · Study project: Mathematics & Technology
+
+
+
+
+ + +
+
+

interests

+
+
+ +
+
+ + +
+
+ + + + + + + + \ No newline at end of file diff --git a/static/infra.html b/source/infra.html similarity index 100% rename from static/infra.html rename to source/infra.html diff --git a/static/login.html b/source/login.html similarity index 100% rename from static/login.html rename to source/login.html diff --git a/source/pages/automation.html b/source/pages/automation.html new file mode 100644 index 0000000..853ffd5 --- /dev/null +++ b/source/pages/automation.html @@ -0,0 +1,56 @@ +{{define "nav"}} + +{{end}} + +{{define "body"}} +
+ +
+

+ The task here is simple: to reduce work, it's a simple investment. The choice of picking the correct + bottleneck is as important as the tool itself, as this gives it use. If it's never used, it never creates + value. The same goes for uptime, it's important that stability is almost perfect, as downtime undermines + the utility of having the tool. Remember that making and using a tool also creates debt for future + maintenance of the tool. +
+ To adhere to this, I often choose Golang or Python as they are cheap to maintain. Golang has good + stability because it is easy to code and compile checks give early error detection. A word of caution on + AI generated tools: while they are cheap to make, the maintenance debt grows exponentially, as the code is + often poorly understood by the team and difficult to reason about over time. + +

+
+
+ Portfolio Engine +
+

A lightweight portfolio and financial data backend written in Go. Tracks companies, + currencies, revenue reports, and trades across configurable time periods — replacing spreadsheets with a + proper database, REST API, and interactive shell.

+ Roadmap +
    +
  • Price update endpoint
  • +
  • Multi-currency conversion
  • +
  • Frontend dashboard
  • +
  • Shares outstanding history database table
  • +
  • Earnings per share (EPS) based on shares owned
  • +
  • First-In, First-Out (FIFO) realized gains
  • +
  • Dividends earned and related taxes
  • +
  • Total tax obligations
  • +
+ Git repo +
+
+
+ Accounting Management +
+

+ A lightweight, SQL-backed, web-based CLI system designed to track assets and liabilities. The goal is to + provide a simple, fast, and scalable financial tracking tool that can grow from personal use to more + advanced scenarios.

+
+
+
+{{end}} \ No newline at end of file diff --git a/source/pages/bookkeeping.html b/source/pages/bookkeeping.html new file mode 100644 index 0000000..208b1e1 --- /dev/null +++ b/source/pages/bookkeeping.html @@ -0,0 +1,7 @@ +{{define "nav"}} + +{{end}} + +{{define "body"}} + +{{end}} \ No newline at end of file diff --git a/source/pages/index.html b/source/pages/index.html new file mode 100644 index 0000000..b1e175c --- /dev/null +++ b/source/pages/index.html @@ -0,0 +1,144 @@ +{{define "nav"}} +
+ + +
+ +
+ + +
+ +
+ +
+
Excel
+
Python
+
SQL
+
Go
+
Git
+
Linux
+
+
+
+ +
Denmark
Aalborg Øst, 9220
+
+
+ +
+ Danish + English +
+
+{{end}} + +{{define "body"}} +
+

About Me · 2026

+

Samantha
Vero Friis +

+

Bachelor's student in Business Administration seeking an entry-level analyst or + finance-related role while studying. Strong numerical skills, advanced Excel, and experience in company, risk, + and financial analysis. Analytical, detail-oriented, and efficiency-focused.

+
+ +
+ samantha42.xyz + me@samantha42.xyz +
+
+
+
+

experience

+
+
+ + + + +
+
+
+
+

education

+
+
+
+
2025 — present
+
+
Bachelor of Economics & Business Administration (HA)
+
Aalborg University · Expected 2028
+
+
+
+
2024 — 2025 +
+ discontinued +
+
+
Mathematics-Technology (BSc)
+
Aalborg University · 1 semester
+ +
+
+ +
+
2020 — 2023
+
+
HTX – Technical Upper Secondary Education
+
Viden Gymnasier
+
Mathematics A, Biology B · Study project: Mathematics & Technology
+
+
+
+
+ + +
+
+

interests

+
+
+ +
+
+{{end}} \ No newline at end of file diff --git a/source/pages/interests.html b/source/pages/interests.html new file mode 100644 index 0000000..dfb1d76 --- /dev/null +++ b/source/pages/interests.html @@ -0,0 +1,139 @@ +{{define "nav"}} + + +
+ + +
+ +
+ +
+
movies
+
games
+
linux
+
board games
+
+
+ +{{end}} + +{{define "body"}} +
+
+ + Boy: Do not try and bend the spoon. That's impossible. Instead... only try to realize the truth. +
+ Neo: What truth? +
+ Boy: There is no spoon. +
+
+

Movie/Show Top list

+
+
+

Top 5 Movies

+
+
+
+ Blade Runner 2049 +

Blade Runner 2049

+
+
+ A Cure of Wellness +

A Cure of Wellness

+
+
+ matrix +

The Matrix

+
+
+ Dracula: A Love Tale +

Dracula: A Love Tale

+
+
+ Girl with a Dragon Tattoos +

Girl with a Dragon Tattoo

+
+
+
+ +
+

Top 5 Shows

+
+
+
+ foundation +

Foundation

+
+
+ Chernobyl +

Chernobyl

+
+
+ Breaking Bad +

Breaking Bad

+
+
+ Avatar: The Last Airbender +

Avatar: The Last Airbender

+
+
+ Adolescence +

Adolescence

+
+
+
+
+
+

Lunix rice

+
+
+

I love my custom Linux setup. I use Hyprland, which automatically arranges app windows + on my screen so everything stays organized without dragging things around. I also use Quickshell, which + lets me customize menus, shortcuts, and system controls to work exactly how I want. + My system is based on Arch Linux or Manjaro Linux, which are versions of Linux that give you more + control and customization than a typical computer setup. The result is a clean, fast, and super + efficient desktop that’s hard to go back from. + Git repo +

+ +
+
+

programing

+
+
+ program +
+

+ Programming can be a task, but for me it's a source of joy. This is something I do as a hobby, and I + don't use AI much. It's about having fun and making a fool of yourself. It's not about how good it is, + in this case, I'd rather just listen to music and drink a cup of tea. I enjoy the struggle to debug, the + beauty in understanding why a bug is there to begin with. +

+
+

+ The text editor I use most is VS Code, but I love the Neovim setup as it enables me to code on a + low-level system without a desktop environment being necessary. I do most of my coding on my own + desktop, synced with Git to other systems, like the server that hosts this website, which I have built + myself. The creation of this website is more of a fun dumb activity for me... go click on one of the + movie posters and see. +

+
+
+
+
+

Games

+
+

Game i will play in my free time to wind down.

+
+ Loading stats… +
+
+
+{{end}} \ No newline at end of file diff --git a/source/pages/research.html b/source/pages/research.html new file mode 100644 index 0000000..05f0437 --- /dev/null +++ b/source/pages/research.html @@ -0,0 +1,72 @@ +{{define "nav"}} +
+ +
+
opinion
+
due diligence
+
performance
+
+
+ +{{end}} + +{{define "body"}} +
+ + +
+
+ Story vs Fundamental value +
+

+ + The truth I keep coming back to is simple: find value grounded in the fundamental numbers of a company. +
+ My approach is rooted in fundamental analysis using SEC filings (10-K, 10-Q), earnings reports, profitability, liquidity, risk factors, and long-term business sustainability. In low-transparency situations I rely on structured cross-validation across sources. +
+ A pattern I keep seeing is CEOs speaking just close enough to reality to generate a short-term price boost, then moving the goalposts when reality catches up. What companies state in filings and what they communicate to retail or even professional investors can be two very different stories. Guidance only needs to reach far enough into the future to say almost anything. +

+
+
+
+

Current stock analysis

+
+ +
+ +
+ +
+

Equity portfolio performance

+
+
+
+ Loading chart... +
+
+ + +
+{{end}} \ No newline at end of file diff --git a/static/secret.html b/source/secret.html similarity index 100% rename from static/secret.html rename to source/secret.html diff --git a/source/templates/main.html b/source/templates/main.html new file mode 100644 index 0000000..6873194 --- /dev/null +++ b/source/templates/main.html @@ -0,0 +1,72 @@ + + + + + + + Samantha Vero Friis + + + + + + + + + + +
+ +
+ {{template "body" .}} +
+
+ + + + + + + + \ No newline at end of file diff --git a/source/tools.html b/source/tools.html new file mode 100644 index 0000000..2c297f7 --- /dev/null +++ b/source/tools.html @@ -0,0 +1,50 @@ +
+ +
+

+ The task here is simple: to reduce work, it's a simple investment. The choice of picking the correct + bottleneck is as important as the tool itself, as this gives it use. If it's never used, it never creates + value. The same goes for uptime, it's important that stability is almost perfect, as downtime undermines + the utility of having the tool. Remember that making and using a tool also creates debt for future + maintenance of the tool. +
+ To adhere to this, I often choose Golang or Python as they are cheap to maintain. Golang has good + stability because it is easy to code and compile checks give early error detection. A word of caution on + AI generated tools: while they are cheap to make, the maintenance debt grows exponentially, as the code is + often poorly understood by the team and difficult to reason about over time. + +

+
+
+ Portfolio Engine +
+

A lightweight portfolio and financial data backend written in Go. Tracks companies, + currencies, revenue reports, and trades across configurable time periods — replacing spreadsheets with a + proper database, REST API, and interactive shell.

+ Roadmap +
    +
  • Price update endpoint
  • +
  • Multi-currency conversion
  • +
  • Frontend dashboard
  • +
  • Shares outstanding history database table
  • +
  • Earnings per share (EPS) based on shares owned
  • +
  • First-In, First-Out (FIFO) realized gains
  • +
  • Dividends earned and related taxes
  • +
  • Total tax obligations
  • +
+ Git repo +
+
+
+ Accounting Management +
+

+ A lightweight, SQL-backed, web-based CLI system designed to track assets and liabilities. The goal is to + provide a simple, fast, and scalable financial tracking tool that can grow from personal use to more + advanced scenarios.

+
+
+
\ No newline at end of file diff --git a/static/icons/leafeon-the-eevees-and-umbreon-l3kahsutrpv30hko.jpg b/static/icons/leafeon-the-eevees-and-umbreon-l3kahsutrpv30hko.jpg new file mode 100644 index 0000000..81648d5 Binary files /dev/null and b/static/icons/leafeon-the-eevees-and-umbreon-l3kahsutrpv30hko.jpg differ diff --git a/static/input.css b/static/input.css index 2b5bd2a..3e07d92 100644 --- a/static/input.css +++ b/static/input.css @@ -60,12 +60,11 @@ body::before{ } nav{ - position:sticky;top:0;z-index:100; + display: flex; background: var(--navback); backdrop-filter:blur(10px); border-bottom:1px solid var(--border); padding:0 clamp(1rem,5vw,3rem); - display:flex;align-items:center;justify-content:space-between; height:52px; } .nav-logo{ @@ -74,6 +73,8 @@ nav{ display:flex;align-items:center;gap:6px; } .nav-logo::before{content:'>';color:var(--green);font-weight:500;} +.nav-logo.sub::before{content:'/';color:var(--green);font-weight:500;} + .nav-links{display:flex;gap:0.2rem;align-items:center;} .nav-links a{ font-family:var(--ff-mono);font-size:0.72rem; @@ -125,6 +126,7 @@ aside{ display:flex;align-items:center;gap:6px; } .sidebar-links a::before{content:'//';color:var(--dim);font-size:0.65rem;} +.sidebar-links.back a::before{content:'<-';color:var(--dim);font-size:0.65rem;} .sidebar-links a:hover{color:var(--text);background:var(--surface2);} .skill-list{display:flex;flex-direction:column;gap:4px;} .skill-item{ @@ -180,10 +182,17 @@ h1 span{color:var(--accent);} transition:border-color 0.2s,box-shadow 0.2s; } .exp-card:hover{border-color:var(--border2);box-shadow:0 2px 12px rgba(0,0,0,0.06);} + .exp-header{ display:flex;align-items:baseline;justify-content:space-between; margin-bottom:0.6rem;flex-wrap:wrap;gap:0.5rem; } + +.exp-card.link:hover{ + border-color:var(--accent); +} + + .exp-title{font-family:var(--ff-mono);font-size:0.9rem;font-weight:500;color:var(--text);} .exp-tag{ font-family:var(--ff-mono);font-size:0.65rem; diff --git a/static/output.css b/static/output.css index b9d6bda..c08fa86 100644 --- a/static/output.css +++ b/static/output.css @@ -1 +1 @@ -.theme-root.night{--bg:#1c1c1c;--surface:#252525;--surface2:#2a2a2a;--border:#363636;--border2:#ccc7be;--text:#fff;--muted:#d9d9d9;--dim:#b0aa9f;--navback:#1c1c1ccc;--accent:#5389ff;--accent2:#1447b8;--green:#0eaf74;--amber:#ffd78e;--accent-dim:#5da7ca1f;--accent-border:#5d66ca40}.theme-root{--bg:#f7f6f3;--surface:#fff;--surface2:#f0eeea;--border:#e2ddd6;--border2:#ccc7be;--text:#1a1917;--muted:#6b6560;--dim:#b0aa9f;--accent:#1a56db;--accent2:#1447b8;--green:#0a7c52;--amber:#92620a;--navback:#f7f6f3cc --ff-mono:"IBM Plex Mono",monospace;--ff-sans:"IBM Plex Sans",sans-serif;--accent-dim:#5da7ca1f;--accent-border:#5d66ca40}*{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth;font-size:15px}body{background:var(--bg);color:var(--text);font-family:var(--ff-sans);min-height:100vh;font-weight:300;line-height:1.7}body:before{content:"";background-image:radial-gradient(circle, var(--border) 1px, transparent 1px);opacity:.5;pointer-events:none;z-index:0;background-size:24px 24px;position:fixed;inset:0}nav{z-index:100;background:var(--navback);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-bottom:1px solid var(--border);justify-content:space-between;align-items:center;height:52px;padding:0 clamp(1rem,5vw,3rem);display:flex;position:sticky;top:0}.nav-logo{font-family:var(--ff-mono);color:var(--accent);align-items:center;gap:6px;font-size:.78rem;font-weight:500;text-decoration:none;display:flex}.nav-logo:before{content:">";color:var(--green);font-weight:500}.nav-links{align-items:center;gap:.2rem;display:flex}.nav-links a{font-family:var(--ff-mono);color:var(--muted);border-radius:4px;padding:.3rem .7rem;font-size:.72rem;text-decoration:none;transition:color .15s,background .15s}.nav-links a:hover{color:var(--text);background:var(--surface2)}.nav-links .cta{color:var(--accent);border:1px solid var(--accent);margin-left:.4rem}.nav-links .cta:hover{background:var(--accent2);color:#fff}.shell{z-index:1;grid-template-columns:220px 1fr;max-width:1100px;min-height:calc(100vh - 52px);margin:0 auto;display:grid;position:relative}.sidebar-links a.hidden{opacity:0;transition:opacity .4s linear}.sidebar-links:hover a.hidden{opacity:1}aside{border-right:1px solid var(--border);background:var(--surface);flex-direction:column;gap:1rem;height:calc(100vh - 52px);padding:2.5rem 1.5rem;display:flex;position:sticky;top:52px;overflow-y:auto}.sidebar-label{font-family:var(--ff-mono);letter-spacing:.2em;text-transform:uppercase;color:var(--dim);margin-bottom:.8rem;font-size:.65rem}.sidebar-links{flex-direction:column;gap:2px;display:flex}.sidebar-links a{font-family:var(--ff-mono);color:var(--muted);border-radius:4px;align-items:center;gap:6px;padding:.3rem .6rem;font-size:.78rem;text-decoration:none;transition:color .15s,background .15s;display:flex}.sidebar-links a:before{content:"//";color:var(--dim);font-size:.65rem}.sidebar-links a:hover{color:var(--text);background:var(--surface2)}.skill-list{flex-direction:column;gap:4px;display:flex}.skill-item{font-family:var(--ff-mono);color:var(--muted);border-left:2px solid var(--border2);cursor:default;padding:.3rem .6rem;font-size:.78rem;transition:border-color .15s,color .15s}.skill-item:hover{border-color:var(--accent);color:var(--text)}main{flex-direction:column;gap:4rem;padding:3rem clamp(1.5rem,4vw,3rem);display:flex}.hero-eyebrow{font-family:var(--ff-mono);color:var(--green);letter-spacing:.18em;align-items:center;gap:8px;margin-bottom:1rem;font-size:.72rem;display:flex}.hero-eyebrow:before{content:"";background:var(--green);width:20px;height:1px;display:block}h1{font-family:var(--ff-mono);color:var(--text);letter-spacing:-.02em;font-size:clamp(1.8rem,4vw,2.8rem);font-weight:500;line-height:1.1}h1 span{color:var(--accent)}.hero-bio{color:var(--muted);max-width:560px;margin-top:1.2rem;font-size:.9rem;line-height:1.8}.hero-meta{font-family:var(--ff-mono);color:var(--dim);flex-wrap:wrap;gap:1.5rem;margin-top:1.5rem;font-size:.72rem;display:flex}.hero-meta span{align-items:center;gap:5px;display:flex}@keyframes pulse{0%,to{opacity:1}50%{opacity:.35}}.section-head{align-items:center;gap:10px;margin-bottom:1.8rem;display:flex}.section-head h2{font-family:var(--ff-mono);letter-spacing:.25em;text-transform:uppercase;color:var(--muted);white-space:nowrap;font-size:.72rem;font-weight:400}.section-head:after{content:"";background:var(--border);flex:1;height:1px}.exp-grid{flex-direction:column;gap:8px;display:flex}.exp-card{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:1.4rem 1.5rem;transition:border-color .2s,box-shadow .2s}.exp-card:hover{border-color:var(--border2);box-shadow:0 2px 12px #0000000f}.exp-header{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.5rem;margin-bottom:.6rem;display:flex}.exp-title{font-family:var(--ff-mono);color:var(--text);font-size:.9rem;font-weight:500}.exp-tag{font-family:var(--ff-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent);background:#1a56db12;border:1px solid #1a56db33;border-radius:3px;padding:2px 8px;font-size:.65rem}.exp-body{color:var(--muted);font-size:.85rem;line-height:1.8}.edu-grid{flex-direction:column;gap:8px;display:flex}.edu-card{background:var(--surface);border:1px solid var(--border);border-radius:6px;grid-template-columns:80px 1fr;gap:1.2rem;padding:1.2rem 1.4rem;transition:border-color .2s,box-shadow .2s;display:grid}.edu-card:hover{border-color:var(--border2);box-shadow:0 2px 12px #0000000f}.edu-year{font-family:var(--ff-mono);color:var(--green);padding-top:2px;font-size:.7rem;line-height:1.6}.edu-degree{color:var(--text);font-size:.9rem;font-weight:500}.edu-inst{font-family:var(--ff-mono);color:var(--muted);margin-top:2px;font-size:.75rem}.edu-detail{color:var(--dim);margin-top:4px;font-size:.82rem}.lang-row{flex-wrap:wrap;gap:.6rem;display:flex}.lang-badge{font-family:var(--ff-mono);color:var(--text);background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:.35rem .9rem;font-size:.78rem}.interests-row{flex-wrap:wrap;gap:.6rem;display:flex}.interest-chip{font-family:var(--ff-mono);color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:.3rem .75rem;font-size:.75rem}.interest-chip:before{content:"# ";color:var(--dim)}.opinion-grid{flex-wrap:wrap;gap:.6rem;display:flex}.opinion-link{font-family:var(--ff-mono);color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:4px;align-items:center;gap:6px;padding:.4rem .9rem;font-size:.78rem;text-decoration:none;transition:border-color .2s,color .2s,box-shadow .2s;display:flex}.opinion-link:before{content:"→";color:var(--dim);font-size:.75rem}.opinion-link:hover{border-color:var(--accent2);color:var(--text);box-shadow:0 2px 8px #0000000f}footer{z-index:1;border-top:1px solid var(--border);background:var(--surface);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.5rem;padding:1.4rem clamp(1rem,5vw,3rem);display:flex;position:relative}.footer-contact a{font-family:var(--ff-mono);color:var(--accent);font-size:.78rem;text-decoration:none}.footer-contact a:hover{text-decoration:underline}.footer-copy{font-family:var(--ff-mono);color:var(--dim);font-size:.7rem}@keyframes fadeUp{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.hero{animation:.6s both fadeUp}.exp-card:first-child{animation:.5s 50ms both fadeUp}.exp-card:nth-child(2){animation:.5s .12s both fadeUp}.edu-card:first-child{animation:.5s 80ms both fadeUp}.edu-card:nth-child(2){animation:.5s .15s both fadeUp}@media (max-width:700px){.shell{grid-template-columns:1fr}aside{display:none}.edu-card{grid-template-columns:1fr;gap:.3rem}}.edu-tag{letter-spacing:.1em;text-transform:uppercase;border-radius:3px;margin-top:8px;padding:3px 5px;font-family:DM Mono,monospace;font-size:9px;display:inline-block}.tag-dropped{background:var(--surface2);color:var(--muted)}.toplist-ranked li{counter-increment:rank;align-items:center;gap:14px;display:flex}.toplist-ranked{counter-reset:rank}.toplist-ranked li:before{content:counter(rank);color:var(--muted);text-align:right;min-width:16px;font-family:DM Mono,monospace;font-size:11px}.toplist-ranked li img{object-fit:cover;border-radius:4px;flex-shrink:0;width:42px;height:62px}.toplist-toggle{text-align:center;width:100%;display:block}.poster-grid{flex-wrap:wrap;justify-content:center;gap:1.5rem;padding:1rem 0;display:flex}.poster-item{flex-direction:column;align-items:center;width:120px;display:flex}.poster-item img{object-fit:cover;border-radius:6px;width:120px;height:180px}.poster-item p{text-align:center;margin-top:8px;font-size:12px;line-height:1.4}.game-grid{justify-content:center;gap:1.5rem;padding:1rem 0}.game-item{flex-direction:column;align-items:center;width:180px;display:flex}.page-header{flex-direction:column;gap:6px;margin-bottom:2.5rem;display:flex}.page-title{font-family:var(--ff-mono);letter-spacing:-.02em;color:var(--accent);font-size:clamp(1rem,4vw,2rem);font-weight:500;line-height:1}.page-title:before{content:"> ";color:var(--green);font-weight:500}.page-sub{font-family:var(--ff-mono);color:var(--muted);letter-spacing:.08em;padding-left:1.1rem;font-size:.72rem}.posterhover:hover img{transform:scale(1.07)}#rain-canvas{pointer-events:none;z-index:999;opacity:0;width:100%;height:100%;transition:opacity .8s;position:fixed;top:0;left:0}#dragon-grid{pointer-events:none;z-index:999;opacity:.15;position:fixed;top:0;left:0}.outlink{font-family:var(--ff-mono);color:var(--accent);font-size:.78rem;text-decoration:none}.outlink:hover{text-decoration:underline}a{font-family:var(--ff-mono);color:var(--muted);border-radius:4px;align-items:center;gap:6px;padding:.3rem .6rem;font-size:.78rem;text-decoration:none;transition:color .15s,background .15s;display:flex}.rabbit-svg path{fill:gray;transition:fill .3s}.rabbit-svg:hover path{fill:#7f77dd}.scene{font-family:var(--font-sans);flex-direction:column;justify-content:center;align-items:center;gap:1rem;padding:2rem 1rem;display:flex}.toggle-wrap{cursor:pointer;-webkit-tap-highlight-color:transparent;width:50px;height:25px;position:relative}.track{border-radius:12.5px;width:50px;height:25px;transition:background .5s;position:absolute;inset:0;overflow:hidden}.track.day{background:#87ceeb}.track.night{background:#1a1a3e}.stars{opacity:0;transition:opacity .4s;position:absolute;inset:0}.night .stars{opacity:1}.star{background:#fff;border-radius:50%;animation:2s infinite twinkle;position:absolute}.star:first-child{width:2px;height:2px;animation-delay:0s;top:4px;left:6px}.star:nth-child(2){width:1px;height:1px;animation-delay:.4s;top:10px;left:14px}.star:nth-child(3){width:1px;height:1px;animation-delay:.8s;top:5px;left:22px}.star:nth-child(4){width:2px;height:2px;animation-delay:1.2s;top:14px;left:10px}.star:nth-child(5){width:1px;height:1px;animation-delay:.2s;top:8px;left:30px}@keyframes twinkle{0%,to{opacity:1}50%{opacity:.3}}.knob{z-index:2;border-radius:50%;justify-content:center;align-items:center;width:19px;height:19px;transition:left .45s cubic-bezier(.4,0,.2,1),box-shadow .4s;display:flex;position:absolute;top:3px}.knob.night{background:radial-gradient(circle at 35% 35%,#e8e8ff,#c8c8f8);left:3px;box-shadow:0 0 0 2px #b4b4ff2e,0 0 7px 4px #8c8cff59,0 0 14px 6px #6464dc2e}.knob.day{background:radial-gradient(circle at 40% 40%,#fff8c5,gold);left:28px;box-shadow:0 0 0 3px #ffdc0040,0 0 8px 4px #ffc80073,0 0 16px 6px #ffa00033}.sun-rays{opacity:0;pointer-events:none;transition:opacity .3s .15s;position:absolute;inset:-7px}.knob.day .sun-rays{opacity:1}.ray{transform-origin:50% 16px;background:#ffdc00e6;border-radius:1px;width:1.5px;height:4px;margin-left:-.75px;position:absolute;left:50%}.moon-crater{background:#8c8cc866;border-radius:50%;transition:opacity .3s;position:absolute}.knob.day .moon-crater{opacity:0}.knob.night .moon-crater{opacity:1}.label{letter-spacing:.03em;-webkit-user-select:none;user-select:none;font-size:13px;font-weight:500;transition:color .4s}.label.day-label{color:#b8860b}.label.night-label{color:#9090d0}.state-label{color:var(--color-text-secondary);letter-spacing:.05em;text-transform:uppercase;font-size:11px}.row{align-items:center;gap:12px;display:flex}.copy-block{background:var(--color-background-secondary);border:.5px solid var(--color-border-tertiary);border-radius:var(--border-radius-lg);width:100%;max-width:420px;padding:1rem 1.25rem}.copy-block p{color:var(--color-text-secondary);margin:0 0 8px;font-size:13px}.copy-block code{font-family:var(--font-mono);color:var(--color-text-primary);white-space:nowrap;font-size:12px;display:block;overflow-x:auto}.copy-btn{border-radius:var(--border-radius-md);cursor:pointer;border:.5px solid var(--color-border-secondary);color:var(--color-text-secondary);background:0 0;margin-top:10px;padding:4px 10px;font-size:12px}.copy-btn:hover{background:var(--color-background-secondary)}.Hitchhiker{text-align:center;width:100%;max-width:480px;display:none}.matrix-hidden{color:green;font-family:var(--ff-mono);letter-spacing:.08em;padding-left:1.1rem;font-size:.72rem;display:none}.dot{border-radius:50%;width:6px;height:6px;animation:2s infinite pulse;display:inline-block}.dot--available{background:var(--green)}.dot--busy{background:red}.overlay{z-index:100;background:#0009;justify-content:center;align-items:center;display:none;position:fixed;inset:0}button{font-family:var(--ff-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent);background:#1a56db12;border:1px solid #1a56db33;border-radius:3px;padding:2px 8px;font-size:.65rem}button:hover{border:1px solid #1a57db}.hidden{display:none}section .cta{border:.5px solid var(--accent-border);background:var(--accent-dim);border-radius:8px;margin-top:3rem;padding:2rem}.cta-hed{font-family:var(--mono);color:var(--accent);margin-bottom:.4rem;font-size:13px}.cta-body{color:var(--muted);margin-bottom:1.25rem;font-size:13px;line-height:1.65}.cta-btn{font-family:var(--mono);color:var(--bg);background:var(--accent);cursor:pointer;border:none;border-radius:4px;padding:9px 20px;font-size:12px;text-decoration:none;transition:opacity .15s;display:inline-block}.cta-btn:hover{opacity:.85}.available{color:var(--green);border:1px solid var(--green);background:#41db1a12;border-radius:3px;padding:2px 8px}.busy{color:var(--red);border:1px solid var(--red);background:#db1a1a12;border-radius:3px;padding:2px 8px} \ No newline at end of file +.theme-root.night{--bg:#1c1c1c;--surface:#252525;--surface2:#2a2a2a;--border:#363636;--border2:#ccc7be;--text:#fff;--muted:#d9d9d9;--dim:#b0aa9f;--navback:#1c1c1ccc;--accent:#5389ff;--accent2:#1447b8;--green:#0eaf74;--amber:#ffd78e;--accent-dim:#5da7ca1f;--accent-border:#5d66ca40}.theme-root{--bg:#f7f6f3;--surface:#fff;--surface2:#f0eeea;--border:#e2ddd6;--border2:#ccc7be;--text:#1a1917;--muted:#6b6560;--dim:#b0aa9f;--accent:#1a56db;--accent2:#1447b8;--green:#0a7c52;--amber:#92620a;--navback:#f7f6f3cc --ff-mono:"IBM Plex Mono",monospace;--ff-sans:"IBM Plex Sans",sans-serif;--accent-dim:#5da7ca1f;--accent-border:#5d66ca40}*{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth;font-size:15px}body{background:var(--bg);color:var(--text);font-family:var(--ff-sans);min-height:100vh;font-weight:300;line-height:1.7}body:before{content:"";background-image:radial-gradient(circle, var(--border) 1px, transparent 1px);opacity:.5;pointer-events:none;z-index:0;background-size:24px 24px;position:fixed;inset:0}nav{background:var(--navback);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-bottom:1px solid var(--border);height:52px;padding:0 clamp(1rem,5vw,3rem);display:flex}.nav-logo{font-family:var(--ff-mono);color:var(--accent);align-items:center;gap:6px;font-size:.78rem;font-weight:500;text-decoration:none;display:flex}.nav-logo:before{content:">";color:var(--green);font-weight:500}.nav-logo.sub:before{content:"/";color:var(--green);font-weight:500}.nav-links{align-items:center;gap:.2rem;display:flex}.nav-links a{font-family:var(--ff-mono);color:var(--muted);border-radius:4px;padding:.3rem .7rem;font-size:.72rem;text-decoration:none;transition:color .15s,background .15s}.nav-links a:hover{color:var(--text);background:var(--surface2)}.nav-links .cta{color:var(--accent);border:1px solid var(--accent);margin-left:.4rem}.nav-links .cta:hover{background:var(--accent2);color:#fff}.shell{z-index:1;grid-template-columns:220px 1fr;max-width:1100px;min-height:calc(100vh - 52px);margin:0 auto;display:grid;position:relative}.sidebar-links a.hidden{opacity:0;transition:opacity .4s linear}.sidebar-links:hover a.hidden{opacity:1}aside{border-right:1px solid var(--border);background:var(--surface);flex-direction:column;gap:1rem;height:calc(100vh - 52px);padding:2.5rem 1.5rem;display:flex;position:sticky;top:52px;overflow-y:auto}.sidebar-label{font-family:var(--ff-mono);letter-spacing:.2em;text-transform:uppercase;color:var(--dim);margin-bottom:.8rem;font-size:.65rem}.sidebar-links{flex-direction:column;gap:2px;display:flex}.sidebar-links a{font-family:var(--ff-mono);color:var(--muted);border-radius:4px;align-items:center;gap:6px;padding:.3rem .6rem;font-size:.78rem;text-decoration:none;transition:color .15s,background .15s;display:flex}.sidebar-links a:before{content:"//";color:var(--dim);font-size:.65rem}.sidebar-links.back a:before{content:"<-";color:var(--dim);font-size:.65rem}.sidebar-links a:hover{color:var(--text);background:var(--surface2)}.skill-list{flex-direction:column;gap:4px;display:flex}.skill-item{font-family:var(--ff-mono);color:var(--muted);border-left:2px solid var(--border2);cursor:default;padding:.3rem .6rem;font-size:.78rem;transition:border-color .15s,color .15s}.skill-item:hover{border-color:var(--accent);color:var(--text)}main{flex-direction:column;gap:4rem;padding:3rem clamp(1.5rem,4vw,3rem);display:flex}.hero-eyebrow{font-family:var(--ff-mono);color:var(--green);letter-spacing:.18em;align-items:center;gap:8px;margin-bottom:1rem;font-size:.72rem;display:flex}.hero-eyebrow:before{content:"";background:var(--green);width:20px;height:1px;display:block}h1{font-family:var(--ff-mono);color:var(--text);letter-spacing:-.02em;font-size:clamp(1.8rem,4vw,2.8rem);font-weight:500;line-height:1.1}h1 span{color:var(--accent)}.hero-bio{color:var(--muted);max-width:560px;margin-top:1.2rem;font-size:.9rem;line-height:1.8}.hero-meta{font-family:var(--ff-mono);color:var(--dim);flex-wrap:wrap;gap:1.5rem;margin-top:1.5rem;font-size:.72rem;display:flex}.hero-meta span{align-items:center;gap:5px;display:flex}@keyframes pulse{0%,to{opacity:1}50%{opacity:.35}}.section-head{align-items:center;gap:10px;margin-bottom:1.8rem;display:flex}.section-head h2{font-family:var(--ff-mono);letter-spacing:.25em;text-transform:uppercase;color:var(--muted);white-space:nowrap;font-size:.72rem;font-weight:400}.section-head:after{content:"";background:var(--border);flex:1;height:1px}.exp-grid{flex-direction:column;gap:8px;display:flex}.exp-card{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:1.4rem 1.5rem;transition:border-color .2s,box-shadow .2s}.exp-card:hover{border-color:var(--border2);box-shadow:0 2px 12px #0000000f}.exp-header{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.5rem;margin-bottom:.6rem;display:flex}.exp-card.link:hover{border-color:var(--accent)}.exp-title{font-family:var(--ff-mono);color:var(--text);font-size:.9rem;font-weight:500}.exp-tag{font-family:var(--ff-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent);background:#1a56db12;border:1px solid #1a56db33;border-radius:3px;padding:2px 8px;font-size:.65rem}.exp-body{color:var(--muted);font-size:.85rem;line-height:1.8}.edu-grid{flex-direction:column;gap:8px;display:flex}.edu-card{background:var(--surface);border:1px solid var(--border);border-radius:6px;grid-template-columns:80px 1fr;gap:1.2rem;padding:1.2rem 1.4rem;transition:border-color .2s,box-shadow .2s;display:grid}.edu-card:hover{border-color:var(--border2);box-shadow:0 2px 12px #0000000f}.edu-year{font-family:var(--ff-mono);color:var(--green);padding-top:2px;font-size:.7rem;line-height:1.6}.edu-degree{color:var(--text);font-size:.9rem;font-weight:500}.edu-inst{font-family:var(--ff-mono);color:var(--muted);margin-top:2px;font-size:.75rem}.edu-detail{color:var(--dim);margin-top:4px;font-size:.82rem}.lang-row{flex-wrap:wrap;gap:.6rem;display:flex}.lang-badge{font-family:var(--ff-mono);color:var(--text);background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:.35rem .9rem;font-size:.78rem}.interests-row{flex-wrap:wrap;gap:.6rem;display:flex}.interest-chip{font-family:var(--ff-mono);color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:.3rem .75rem;font-size:.75rem}.interest-chip:before{content:"# ";color:var(--dim)}.opinion-grid{flex-wrap:wrap;gap:.6rem;display:flex}.opinion-link{font-family:var(--ff-mono);color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:4px;align-items:center;gap:6px;padding:.4rem .9rem;font-size:.78rem;text-decoration:none;transition:border-color .2s,color .2s,box-shadow .2s;display:flex}.opinion-link:before{content:"→";color:var(--dim);font-size:.75rem}.opinion-link:hover{border-color:var(--accent2);color:var(--text);box-shadow:0 2px 8px #0000000f}footer{z-index:1;border-top:1px solid var(--border);background:var(--surface);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.5rem;padding:1.4rem clamp(1rem,5vw,3rem);display:flex;position:relative}.footer-contact a{font-family:var(--ff-mono);color:var(--accent);font-size:.78rem;text-decoration:none}.footer-contact a:hover{text-decoration:underline}.footer-copy{font-family:var(--ff-mono);color:var(--dim);font-size:.7rem}@keyframes fadeUp{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.hero{animation:.6s both fadeUp}.exp-card:first-child{animation:.5s 50ms both fadeUp}.exp-card:nth-child(2){animation:.5s .12s both fadeUp}.edu-card:first-child{animation:.5s 80ms both fadeUp}.edu-card:nth-child(2){animation:.5s .15s both fadeUp}@media (max-width:700px){.shell{grid-template-columns:1fr}aside{display:none}.edu-card{grid-template-columns:1fr;gap:.3rem}}.edu-tag{letter-spacing:.1em;text-transform:uppercase;border-radius:3px;margin-top:8px;padding:3px 5px;font-family:DM Mono,monospace;font-size:9px;display:inline-block}.tag-dropped{background:var(--surface2);color:var(--muted)}.toplist-ranked li{counter-increment:rank;align-items:center;gap:14px;display:flex}.toplist-ranked{counter-reset:rank}.toplist-ranked li:before{content:counter(rank);color:var(--muted);text-align:right;min-width:16px;font-family:DM Mono,monospace;font-size:11px}.toplist-ranked li img{object-fit:cover;border-radius:4px;flex-shrink:0;width:42px;height:62px}.toplist-toggle{text-align:center;width:100%;display:block}.poster-grid{flex-wrap:wrap;justify-content:center;gap:1.5rem;padding:1rem 0;display:flex}.poster-item{flex-direction:column;align-items:center;width:120px;display:flex}.poster-item img{object-fit:cover;border-radius:6px;width:120px;height:180px}.poster-item p{text-align:center;margin-top:8px;font-size:12px;line-height:1.4}.game-grid{justify-content:center;gap:1.5rem;padding:1rem 0}.game-item{flex-direction:column;align-items:center;width:180px;display:flex}.page-header{flex-direction:column;gap:6px;margin-bottom:2.5rem;display:flex}.page-title{font-family:var(--ff-mono);letter-spacing:-.02em;color:var(--accent);font-size:clamp(1rem,4vw,2rem);font-weight:500;line-height:1}.page-title:before{content:"> ";color:var(--green);font-weight:500}.page-sub{font-family:var(--ff-mono);color:var(--muted);letter-spacing:.08em;padding-left:1.1rem;font-size:.72rem}.posterhover:hover img{transform:scale(1.07)}#rain-canvas{pointer-events:none;z-index:999;opacity:0;width:100%;height:100%;transition:opacity .8s;position:fixed;top:0;left:0}#dragon-grid{pointer-events:none;z-index:999;opacity:.15;position:fixed;top:0;left:0}.outlink{font-family:var(--ff-mono);color:var(--accent);font-size:.78rem;text-decoration:none}.outlink:hover{text-decoration:underline}a{font-family:var(--ff-mono);color:var(--muted);border-radius:4px;align-items:center;gap:6px;padding:.3rem .6rem;font-size:.78rem;text-decoration:none;transition:color .15s,background .15s;display:flex}.rabbit-svg path{fill:gray;transition:fill .3s}.rabbit-svg:hover path{fill:#7f77dd}.scene{font-family:var(--font-sans);flex-direction:column;justify-content:center;align-items:center;gap:1rem;padding:2rem 1rem;display:flex}.label{letter-spacing:.03em;-webkit-user-select:none;user-select:none;font-size:13px;font-weight:500;transition:color .4s}.label.day-label{color:#b8860b}.label.night-label{color:#9090d0}.state-label{color:var(--color-text-secondary);letter-spacing:.05em;text-transform:uppercase;font-size:11px}.row{align-items:center;gap:12px;display:flex}.copy-block{background:var(--color-background-secondary);border:.5px solid var(--color-border-tertiary);border-radius:var(--border-radius-lg);width:100%;max-width:420px;padding:1rem 1.25rem}.copy-block p{color:var(--color-text-secondary);margin:0 0 8px;font-size:13px}.copy-block code{font-family:var(--font-mono);color:var(--color-text-primary);white-space:nowrap;font-size:12px;display:block;overflow-x:auto}.copy-btn{border-radius:var(--border-radius-md);cursor:pointer;border:.5px solid var(--color-border-secondary);color:var(--color-text-secondary);background:0 0;margin-top:10px;padding:4px 10px;font-size:12px}.copy-btn:hover{background:var(--color-background-secondary)}.Hitchhiker{text-align:center;width:100%;max-width:480px;display:none}.matrix-hidden{color:green;font-family:var(--ff-mono);letter-spacing:.08em;padding-left:1.1rem;font-size:.72rem;display:none}.dot{border-radius:50%;width:6px;height:6px;animation:2s infinite pulse;display:inline-block}.dot--available{background:var(--green)}.dot--busy{background:red}.overlay{z-index:100;background:#0009;justify-content:center;align-items:center;display:none;position:fixed;inset:0}button{font-family:var(--ff-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent);background:#1a56db12;border:1px solid #1a56db33;border-radius:3px;padding:2px 8px;font-size:.65rem}button:hover{border:1px solid #1a57db}.hidden{display:none}section .cta{border:.5px solid var(--accent-border);background:var(--accent-dim);border-radius:8px;margin-top:3rem;padding:2rem}.cta-hed{font-family:var(--mono);color:var(--accent);margin-bottom:.4rem;font-size:13px}.cta-body{color:var(--muted);margin-bottom:1.25rem;font-size:13px;line-height:1.65}.cta-btn{font-family:var(--mono);color:var(--bg);background:var(--accent);cursor:pointer;border:none;border-radius:4px;padding:9px 20px;font-size:12px;text-decoration:none;transition:opacity .15s;display:inline-block}.cta-btn:hover{opacity:.85}.available{color:var(--green);border:1px solid var(--green);background:#41db1a12;border-radius:3px;padding:2px 8px}.busy{color:var(--red);border:1px solid var(--red);background:#db1a1a12;border-radius:3px;padding:2px 8px} \ No newline at end of file