diff --git a/main b/main deleted file mode 100755 index 2970fe1..0000000 Binary files a/main and /dev/null differ diff --git a/main.go b/main.go index 3e79210..689651f 100644 --- a/main.go +++ b/main.go @@ -34,9 +34,11 @@ func Infra(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./s func Finance(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/finance.html") } func Cinema(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cinema.html") } func Cyber(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cyber.html") } +func Engine(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/engine.html") } func About(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/about.html") } -func Styles(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/styles.css") } +func Styles(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/styles.css") } +func Styles2(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/styles2.css") } // TradesHandler handles POST /trades // It accepts a CSV file upload (field name: "file") and returns enriched trades as JSON. @@ -94,6 +96,7 @@ func main() { flag.Parse() http.HandleFunc("/styles.css", Styles) + http.HandleFunc("/styles2.css", Styles2) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.Redirect(w, r, "/", http.StatusFound) @@ -106,6 +109,7 @@ func main() { http.HandleFunc("/finance", Finance) http.HandleFunc("/cinema", Cinema) http.HandleFunc("/cyber", Cyber) + http.HandleFunc("/engine", Engine) http.HandleFunc("/git", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "https://git.samantha42.xyz", http.StatusFound) }) diff --git a/site b/site index e37e2a0..59fd617 100755 Binary files a/site and b/site differ diff --git a/static/about.html b/static/about.html index c28e49a..9bd82e7 100644 --- a/static/about.html +++ b/static/about.html @@ -1,201 +1,144 @@ - + - - - - About — Samantha Vero Friis - - - - - - -
- - -
-

// about me

-

Samantha
Vero Friis

-

- Denmark, Aalborg  ·  samantha42.xyz -

-
+ + + +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. -

+ - -
- -
-
- Excel -
-
-
-
-
- Python -
-
-
-
-
- SQL -
-
-
-
-
- Go -
-
-
-
-
- Git -
-
-
-
-
-
+
+ - -
- +
-
-

Independent Equity Research

-

- Perform fundamental analysis using SEC filings - (10-K, 10-Q) and earnings reports. Assess - profitability, liquidity, risk factors, and - long-term business sustainability. Handle incomplete - or low-transparency information through structured - cross-validation. -

-
+
+

portfolio · 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.

+
+ available for work + samantha42.xyz + contact@samantha42.xyz +
+
-
-

Technical Tools & Automation

-

- Build lightweight backend tools to automate - repetitive analytical and operational tasks. Use - Python for data processing, validation, and workflow - automation. Develop lightweight online services in - Go, focusing on speed and reliability. -

-
-
- - -
- - -
- 2025 — now -
-

- Bachelor of Economics & Business Administration - (HA) -

-

- Aalborg University  ·  Expected 2028 -

-
-
- -
- 2020 — 2023 -
-

- HTX – Technical Upper Secondary Education -

-

- Viden Gymnasier  ·  Mathematics A, - Biology B
Study project: Mathematics & - Technology -

-
-
-
- - -
- -
-
- Danish -
-
-
-
-
- English -
-
-
-
-
-
- - -
- -
- Gaming - Board games - Programming - Financial markets -
-
-
- - +
+

experience

+
+
+
+ Independent Equity Research + finance +
+

Fundamental analysis using SEC filings (10-K, 10-Q) and earnings reports. Assess profitability, liquidity, risk factors, and long-term business sustainability. Handle incomplete or low-transparency information through structured cross-validation.

- - +
+
+ Technical Tools & Automation + engineering +
+

Build lightweight backend tools to automate repetitive analytical and operational tasks. Python for data processing, validation, and workflow automation. Lightweight online services in Go, focusing on speed and reliability.

+
+
+
+ +
+

education

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

opinion

+ +
+ +
+

languages

+
+ Danish + English +
+
+ +
+

interests

+
+ gaming + board-games + programming + financial-markets +
+
+ + +
+ + + + + \ No newline at end of file diff --git a/static/engine.html b/static/engine.html new file mode 100644 index 0000000..e3f0c42 --- /dev/null +++ b/static/engine.html @@ -0,0 +1,314 @@ + + + + + + FP&A Budgeting Engine — Samantha Vero Friis + + + + + + + + +
+
+ ← back + +
project
+ +

FP&A Budgeting Engine

+ + +
+
+ + +
+
+
+
Language
+
Go
+
+
+
Database
+
SQLite
+
+
+
Architecture
+
REST API
+
+
+
Infrastructure
+
Zero deps
+
+
+
+ + +
+
+
+ +

+ A REST API for corporate budget management, actuals ingestion, and variance + reporting — built to replace the manual Excel workflow that smaller finance + teams still run every month. Single binary, no infrastructure, deploys anywhere. +

+ +

The problem it solves

+ +

+ At companies below a certain size — typically below 500 employees, or without + the budget for enterprise planning platforms like Anaplan or Adaptive Planning — + the monthly FP&A close still runs on spreadsheets. Actuals are exported from + the ERP, pasted into a workbook, and variances are computed by hand across + departments and GL accounts. The process is slow, error-prone, and breaks the + moment someone edits the wrong cell. +

+ +

+ Large enterprises have solved this with dedicated tooling. The gap is the + mid-market: companies that have outgrown Excel but cannot justify a six-figure + implementation project. This engine sits in that space — structured schema, + versioned budgets, on-demand actuals ingestion, and instant variance reports + with no pivot tables and no shared file conflicts. +

+ +
+ The goal was not to build something clever.
+ It was to build something that solves a real problem
+ finance teams in the mid-market actually have. +
+ +

Stack

+ +

+ Most finance tooling is Python notebooks or Excel macros — useful for analysis, + fragile in production. Go compiles to a single binary with no runtime + dependencies, makes every error path explicit, and handles concurrency safely. + SQLite was chosen over Postgres deliberately: for a tool running inside a + finance team's environment, zero infrastructure matters. The database is a + single file. Backups are a file copy. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LayerChoiceReason
LanguageGoSingle binary, no runtime, explicit errors
Routingnet/http (stdlib)No deps; method+path routing since Go 1.22
DatabaseSQLiteZero infrastructure, file-based, ships with binary
SchemaAuto-migratedCREATE TABLE IF NOT EXISTS on startup
+
+ +

API surface

+ +

+ Three resource groups. Budgets support full CRUD by department, GL account, + fiscal period, and version. Actuals are ingested via a single upsert endpoint + — idempotent by period, department, and GL code, designed to accept ERP + JSON exports directly. Variance endpoints return computed reports immediately. +

+ +
POST   /api/v1/budgets              Create a budget line
+PUT    /api/v1/budgets/{id}          Update amount or notes
+DELETE /api/v1/budgets/{id}          Remove a budget line
+
+POST   /api/v1/actuals/ingest        Upsert an actual (idempotent)
+
+GET    /api/v1/variance              Full variance report
+GET    /api/v1/variance/alerts       Lines exceeding threshold
+
+GET    /api/v1/health                Returns 200 if DB is reachable
+ +

+ Variance endpoints accept year, period, + dept, version, and threshold + as query parameters. +

+ +

Finance concepts implemented correctly

+ +

+ Favourability logic. Whether a + variance is good or bad depends on account type. Revenue accounts are + favourable when actuals exceed budget; cost accounts when actuals come in + under. The engine handles both via a favour_high flag per GL + account — a distinction generic reporting tools consistently leave to manual + configuration. +

+ +

+ Budget versioning. Original budget + and up to three forecast revisions are stored separately, enabling a full + budget vs. forecast vs. actual three-way comparison without overwriting + history — the standard FP&A reforecast workflow. +

+ +

+ GL account typing. Accounts are + typed as revenue, cogs, opex, + capex, or headcount. This drives the P&L + rollup structure and the favourability logic together, reflecting how an + actual chart of accounts is structured. +

+ +

+ Fiscal period decoupling. Periods + are stored as integers 1–12, decoupled from calendar months — required for + any company not on a January fiscal year. +

+ +

Example output

+ +

+ Engineering department, FY2024 P09. Cloud infrastructure came in under budget. + Consulting exceeded by 51%. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
GLDescriptionBudgetActualVar %Status
6100Salaries & Wages4,200k4,380k−4.29%unfavourable
6500Consulting & Contractors450k680k−51.11%unfavourable
5000Cloud Infrastructure850k791k+6.94%favourable
+
+ +

Running it

+ +
git clone https://git.samantha42.xyz/samantha/FPandA-Engine
+cd FPandA-Engine
+cp .env.example .env
+go mod tidy
+go run ./cmd/server
+# → http://localhost:8080
+# → fpa.db created on first run
+ +

+ Two environment variables: DB_PATH (use :memory: + for tests) and PORT. Demo seed data for Engineering, Sales, + and Marketing is included in scripts/seed_demo.sql. +

+ +
+ +

+ Source at + git.samantha42.xyz/samantha/FPandA-Engine +

+ +
+
+
+ + + + + \ No newline at end of file diff --git a/static/styles.css b/static/styles.css index 0cf28b6..3676981 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,17 +1,20 @@ /* ─── Variables ─── */ :root { - --bg: #252525; - --surface: #2b2b2b; - --surface2: #313131; - --border: #3a3a3a; - --border-focus: #4e4e4e; - --text: #e8e8e0; - --muted: #777770; - --accent: #b88fe0; - --accent2: #6247aa; - --green: #6fcf8a; - --red: #e07070; - --yellow: #e0c870; + --bg: #f7f6f3; + --surface: #ffffff; + --surface2: #f0eeea; + --border: #e2ddd6; + --border-focus: #ccc7be; + --text: #1a1917; + --muted: #6b6560; + --dim: #b0aa9f; + --accent: #1a56db; + --accent2: #1447b8; + --green: #0a7c52; + --red: #b91c1c; + --yellow: #92620a; + --ff-mono: 'IBM Plex Mono', monospace; + --ff-sans: 'IBM Plex Sans', sans-serif; } /* ─── Reset ─── */ @@ -30,20 +33,22 @@ html { body { background: var(--bg); color: var(--text); - font-family: "DM Mono", monospace; + font-family: var(--ff-mono); + font-weight: 300; min-height: 100vh; overflow-x: hidden; } -/* Grain overlay */ +/* Dot grid overlay */ body::before { content: ""; position: fixed; inset: 0; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E"); + background-image: radial-gradient(circle, var(--border) 1px, transparent 1px); + background-size: 24px 24px; + opacity: 0.5; pointer-events: none; - z-index: 999; - opacity: 0.35; + z-index: 0; } /* ─── Layout ─── */ @@ -67,6 +72,8 @@ body::before { header { border-bottom: 1px solid var(--border); animation: fadeUp 0.6s ease both; + position: relative; + z-index: 1; } .back { @@ -80,6 +87,7 @@ header { text-transform: uppercase; margin-bottom: 2rem; transition: color 0.2s; + font-family: var(--ff-mono); } .back:hover { color: var(--accent); @@ -91,23 +99,20 @@ header { text-transform: uppercase; color: var(--accent); margin-bottom: 1rem; + font-family: var(--ff-mono); } h1 { - font-family: "Libre Caslon Text", serif; - font-size: clamp(2.5rem, 5vw, 4rem); - font-weight: 800; + font-family: var(--ff-mono); + font-size: clamp(2rem, 5vw, 3.2rem); + font-weight: 500; line-height: 1.05; letter-spacing: -0.02em; + color: var(--text); } h1 span { color: var(--accent); - font-size: clamp(2rem, 2.5vw, 3.5rem); -} - -h1--hero { - font-size: clamp(2.8rem, 6vw, 5rem); - line-height: 1; + font-size: clamp(1.6rem, 2.5vw, 2.8rem); } .header-sub { @@ -122,6 +127,7 @@ h1--hero { color: var(--muted); display: flex; gap: 1.5rem; + font-family: var(--ff-mono); } .byline span { color: var(--accent); @@ -129,34 +135,37 @@ h1--hero { /* ─── Typography ─── */ h2 { - font-family: "Syne", sans-serif; - font-size: 1.2rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 1rem; + font-weight: 500; color: var(--accent); margin: 2.8rem 0 1rem; letter-spacing: -0.01em; } h2::before { content: "// "; - color: var(--accent2); - font-family: "DM Mono", monospace; + color: var(--dim); + font-family: var(--ff-mono); font-weight: 300; } p { - font-size: 0.85rem; + font-family: var(--ff-sans); + font-size: 0.88rem; line-height: 1.9; - color: #c8c8c0; + color: var(--muted); margin-bottom: 1.2rem; + font-weight: 300; } code { - font-family: "DM Mono", monospace; + font-family: var(--ff-mono); font-size: 0.78rem; color: var(--accent); - background: rgba(184, 143, 224, 0.08); + background: rgba(26, 86, 219, 0.06); padding: 0.1rem 0.4rem; - border: 1px solid rgba(184, 143, 224, 0.15); + border: 1px solid rgba(26, 86, 219, 0.15); + border-radius: 2px; } hr { @@ -166,17 +175,20 @@ hr { } .section-label { + font-family: var(--ff-mono); font-size: 0.65rem; letter-spacing: 0.2em; text-transform: uppercase; - color: var(--accent); + color: var(--dim); margin-bottom: 1.2rem; } .lead { - font-size: 1rem; + font-family: var(--ff-sans); + font-size: 0.95rem; line-height: 1.9; color: var(--text); + font-weight: 300; border-left: 2px solid var(--accent2); padding-left: 1.5rem; margin-bottom: 3rem; @@ -186,13 +198,14 @@ blockquote { margin: 2.5rem 0; padding: 1.6rem 2rem; background: var(--surface); + border: 1px solid var(--border); border-left: 3px solid var(--accent); - font-family: "Syne", sans-serif; - font-size: 1.05rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 0.95rem; + font-weight: 400; color: var(--text); - line-height: 1.5; - letter-spacing: -0.01em; + line-height: 1.6; + border-radius: 0 4px 4px 0; } blockquote span { color: var(--accent); @@ -213,46 +226,53 @@ blockquote span { .cards { display: grid; grid-template-columns: repeat(4, 1fr); - gap: 1px; - background: var(--border); - border: 1px solid var(--border); + gap: 8px; margin: 3rem 0 2.5rem; animation: fadeUp 0.6s 0.1s ease both; } .card { background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; padding: 1.4rem 1.6rem; + transition: border-color 0.2s, box-shadow 0.2s; +} +.card:hover { + border-color: var(--border-focus); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); } .card-label { + font-family: var(--ff-mono); font-size: 0.62rem; letter-spacing: 0.15em; text-transform: uppercase; - color: var(--muted); + color: var(--dim); margin-bottom: 0.5rem; } .card-value { - font-family: "Syne", sans-serif; - font-size: 1.5rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 1.4rem; + font-weight: 500; + color: var(--text); } /* ─── Nav grid (homepage) ─── */ .nav-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 1px; - background: var(--border); - border: 1px solid var(--border); + gap: 8px; } .nav-item { display: block; background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; padding: 2rem 2rem 1.8rem; text-decoration: none; color: var(--text); position: relative; overflow: hidden; - transition: background 0.2s ease; + transition: border-color 0.2s, box-shadow 0.2s; animation: fadeUp 0.7s ease both; } .nav-item::after { @@ -266,7 +286,8 @@ blockquote span { transition: width 0.3s ease; } .nav-item:hover { - background: #161616; + border-color: var(--accent2); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.07); } .nav-item:hover::after { width: 100%; @@ -275,20 +296,22 @@ blockquote span { transform: translate(4px, -4px); } .nav-number { + font-family: var(--ff-mono); font-size: 0.65rem; - color: var(--muted); + color: var(--dim); letter-spacing: 0.15em; margin-bottom: 1.4rem; } .nav-title { - font-family: "Syne", sans-serif; - font-size: 1.35rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 1.1rem; + font-weight: 500; margin-bottom: 0.5rem; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; + color: var(--text); } .arrow { font-size: 1rem; @@ -297,9 +320,11 @@ blockquote span { flex-shrink: 0; } .nav-desc { - font-size: 0.75rem; + font-family: var(--ff-sans); + font-size: 0.78rem; color: var(--muted); line-height: 1.6; + font-weight: 300; } /* ─── Table ─── */ @@ -307,23 +332,28 @@ blockquote span { overflow-x: auto; animation: fadeUp 0.6s 0.2s ease both; margin-bottom: 2.5rem; + border: 1px solid var(--border); + border-radius: 6px; } table { width: 100%; border-collapse: collapse; font-size: 0.78rem; + font-family: var(--ff-mono); } thead tr { border-bottom: 1px solid var(--border); + background: var(--surface2); } thead th { font-size: 0.62rem; letter-spacing: 0.12em; text-transform: uppercase; - color: var(--muted); + color: var(--dim); padding: 0.7rem 1rem; text-align: right; white-space: nowrap; + font-weight: 400; } thead th:first-child { text-align: left; @@ -331,6 +361,7 @@ thead th:first-child { tbody tr { border-bottom: 1px solid var(--border); transition: background 0.15s; + background: var(--surface); } tbody tr:hover { background: var(--surface2); @@ -346,9 +377,9 @@ tbody td:first-child { } .ticker { - font-family: "Syne", sans-serif; - font-weight: 700; - font-size: 0.9rem; + font-family: var(--ff-mono); + font-weight: 500; + font-size: 0.88rem; color: var(--text); } .currency-badge { @@ -359,6 +390,7 @@ tbody td:first-child { color: var(--muted); margin-left: 0.4rem; vertical-align: middle; + border-radius: 2px; } .weight-cell { display: flex; @@ -375,8 +407,9 @@ tbody td:first-child { } .weight-bar-fill { height: 100%; - background: linear-gradient(90deg, var(--accent2), var(--accent)); + background: var(--accent); border-radius: 2px; + opacity: 0.5; } .watchlist-row td { color: var(--muted); @@ -385,7 +418,7 @@ tbody td:first-child { font-size: 0.62rem; letter-spacing: 0.12em; text-transform: uppercase; - color: var(--muted); + color: var(--dim); padding: 1.2rem 1rem 0.5rem; } @@ -402,6 +435,7 @@ tbody td:first-child { gap: 1rem; font-size: 0.78rem; color: var(--muted); + font-family: var(--ff-mono); } .skill-bar-track { height: 3px; @@ -411,8 +445,9 @@ tbody td:first-child { } .skill-bar-fill { height: 100%; - background: linear-gradient(90deg, var(--accent2), var(--accent)); + background: var(--accent); border-radius: 2px; + opacity: 0.6; transform-origin: left; animation: growBar 0.8s ease both; } @@ -421,19 +456,23 @@ tbody td:first-child { .compare { display: grid; grid-template-columns: 1fr 1fr; - gap: 1px; - background: var(--border); - border: 1px solid var(--border); + gap: 8px; margin: 2rem 0; } .compare-col { background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; padding: 1.4rem 1.6rem; + transition: border-color 0.2s; +} +.compare-col:hover { + border-color: var(--border-focus); } .compare-col h3 { - font-family: "Syne", sans-serif; - font-size: 0.85rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 0.82rem; + font-weight: 500; margin-bottom: 1rem; padding-bottom: 0.6rem; border-bottom: 1px solid var(--border); @@ -451,11 +490,13 @@ tbody td:first-child { gap: 0.6rem; } .compare-col ul li { - font-size: 0.75rem; + font-family: var(--ff-sans); + font-size: 0.78rem; color: var(--muted); line-height: 1.5; padding-left: 1rem; position: relative; + font-weight: 300; } .compare-col.bad ul li::before { content: "—"; @@ -474,8 +515,14 @@ tbody td:first-child { .exp-block { background: var(--surface); border: 1px solid var(--border); + border-radius: 6px; padding: 1.5rem; - margin-bottom: 1px; + margin-bottom: 8px; + transition: border-color 0.2s, box-shadow 0.2s; +} +.exp-block:hover { + border-color: var(--border-focus); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); } .exp-block p { font-size: 0.78rem; @@ -497,21 +544,25 @@ tbody td:first-child { margin-bottom: 0; } .edu-year { + font-family: var(--ff-mono); font-size: 0.65rem; - color: var(--accent); + color: var(--yellow); letter-spacing: 0.1em; padding-top: 0.15rem; } .edu-title { - font-family: "Syne", sans-serif; - font-size: 0.95rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 0.92rem; + font-weight: 500; margin-bottom: 0.25rem; + color: var(--text); } .edu-sub { - font-size: 0.75rem; + font-family: var(--ff-sans); + font-size: 0.78rem; color: var(--muted); line-height: 1.6; + font-weight: 300; } .tags { @@ -520,11 +571,18 @@ tbody td:first-child { gap: 0.5rem; } .tag-pill { + font-family: var(--ff-mono); font-size: 0.7rem; padding: 0.3rem 0.75rem; border: 1px solid var(--border); color: var(--muted); letter-spacing: 0.05em; + border-radius: 3px; + transition: border-color 0.15s, color 0.15s; +} +.tag-pill:hover { + border-color: var(--accent2); + color: var(--accent); } /* ─── Forms / Login ─── */ @@ -533,6 +591,7 @@ tbody td:first-child { } label { display: block; + font-family: var(--ff-mono); font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; @@ -545,43 +604,41 @@ input[type="password"] { background: var(--bg); border: 1px solid var(--border); color: var(--text); - font-family: "DM Mono", monospace; + font-family: var(--ff-mono); font-size: 0.82rem; padding: 0.7rem 0.9rem; outline: none; - transition: - border-color 0.2s, - box-shadow 0.2s; + border-radius: 4px; + transition: border-color 0.2s, box-shadow 0.2s; } input[type="text"]:focus, input[type="password"]:focus { border-color: var(--accent2); - box-shadow: 0 0 0 2px rgba(98, 71, 170, 0.15); + box-shadow: 0 0 0 3px rgba(26, 71, 184, 0.1); } input::placeholder { - color: var(--muted); + color: var(--dim); } .btn { width: 100%; background: var(--accent2); - color: var(--text); - font-family: "DM Mono", monospace; + color: #ffffff; + font-family: var(--ff-mono); font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase; padding: 0.8rem 1rem; border: none; + border-radius: 4px; cursor: pointer; - transition: - background 0.2s, - opacity 0.2s; + transition: background 0.2s, opacity 0.2s; } .btn:hover { background: var(--accent); } .btn:active { - opacity: 0.8; + opacity: 0.85; } /* ─── Footer ─── */ @@ -594,19 +651,23 @@ footer { gap: 0.6rem; text-align: center; animation: fadeUp 0.6s 0.4s ease both; + position: relative; + z-index: 1; } footer a { + font-family: var(--ff-mono); color: var(--accent); text-decoration: none; - font-size: 0.85rem; + font-size: 0.82rem; transition: opacity 0.2s; } footer a:hover { opacity: 0.7; } .footer-copy { + font-family: var(--ff-mono); font-size: 0.68rem; - color: var(--muted); + color: var(--dim); letter-spacing: 0.05em; } @@ -614,7 +675,7 @@ footer a:hover { @keyframes fadeUp { from { opacity: 0; - transform: translateY(16px); + transform: translateY(12px); } to { opacity: 1; @@ -630,22 +691,15 @@ footer a:hover { } } @keyframes shake { - 0%, - 100% { - transform: translateX(0); - } - 20% { - transform: translateX(-6px); - } - 40% { - transform: translateX(6px); - } - 60% { - transform: translateX(-4px); - } - 80% { - transform: translateX(4px); - } + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-6px); } + 40% { transform: translateX(6px); } + 60% { transform: translateX(-4px); } + 80% { transform: translateX(4px); } +} +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } } .shake { animation: shake 0.4s ease; @@ -655,29 +709,39 @@ footer a:hover { article { padding: 3.5rem 0; animation: fadeUp 0.6s 0.1s ease both; + position: relative; + z-index: 1; } main { padding: 3.5rem 0; + position: relative; + z-index: 1; } /* ─── Classify grid (cyber page) ─── */ .classify-grid { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 1px; - background: var(--border); - border: 1px solid var(--border); + gap: 8px; margin: 2rem 0; } .classify-card { background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; padding: 1.4rem 1.3rem; + transition: border-color 0.2s, box-shadow 0.2s; +} +.classify-card:hover { + border-color: var(--border-focus); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); } .classify-card h3 { - font-family: "Syne", sans-serif; - font-size: 0.9rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 0.88rem; + font-weight: 500; margin-bottom: 0.5rem; + color: var(--text); } .classify-card p { font-size: 0.72rem; @@ -686,6 +750,7 @@ main { line-height: 1.6; } .level { + font-family: var(--ff-mono); font-size: 0.6rem; letter-spacing: 0.18em; text-transform: uppercase; @@ -706,7 +771,7 @@ main { .principles { display: flex; flex-direction: column; - gap: 1px; + gap: 8px; margin: 2rem 0; } .principle { @@ -714,22 +779,26 @@ main { grid-template-columns: 2.5rem 1fr; gap: 1.2rem; background: var(--surface); - padding: 1.2rem 1.4rem; border: 1px solid var(--border); - margin-bottom: 1px; + border-radius: 6px; + padding: 1.2rem 1.4rem; align-items: start; + transition: border-color 0.2s; +} +.principle:hover { + border-color: var(--border-focus); } .principle-num { - font-family: "Syne", sans-serif; - font-size: 1.1rem; - font-weight: 800; + font-family: var(--ff-mono); + font-size: 1rem; + font-weight: 500; color: var(--accent2); padding-top: 0.1rem; } .principle-title { - font-family: "Syne", sans-serif; - font-size: 0.9rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 0.88rem; + font-weight: 500; color: var(--text); margin-bottom: 0.35rem; } @@ -743,25 +812,33 @@ main { .counter { color: var(--green); font-size: 0.72rem; + font-family: var(--ff-mono); } /* ─── Login panel ─── */ .login-panel { border: 1px solid var(--border); + border-radius: 6px; background: var(--surface); padding: 2rem; } .login-panel h2 { - font-family: "Syne", sans-serif; - font-size: 1rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 0.95rem; + font-weight: 500; margin-bottom: 0.3rem; + color: var(--text); +} +.login-panel h2::before { + display: none; } .login-panel .desc { - font-size: 0.72rem; + font-family: var(--ff-sans); + font-size: 0.78rem; color: var(--muted); margin-bottom: 2rem; line-height: 1.6; + font-weight: 300; } .twofa-group { display: none; @@ -771,6 +848,7 @@ main { display: block; } .twofa-hint { + font-family: var(--ff-mono); font-size: 0.68rem; color: var(--muted); margin-top: 0.4rem; @@ -778,26 +856,26 @@ main { .btn-login { width: 100%; background: var(--accent2); - color: var(--text); - font-family: "DM Mono", monospace; + color: #ffffff; + font-family: var(--ff-mono); font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase; padding: 0.8rem 1rem; border: none; + border-radius: 4px; cursor: pointer; - transition: - background 0.2s, - opacity 0.2s; + transition: background 0.2s, opacity 0.2s; margin-top: 0.4rem; } .btn-login:hover { background: var(--accent); } .btn-login:active { - opacity: 0.8; + opacity: 0.85; } .login-error { + font-family: var(--ff-mono); font-size: 0.72rem; color: var(--red); margin-top: 1rem; @@ -817,52 +895,61 @@ main { .repo-list { display: flex; flex-direction: column; - gap: 1px; + gap: 8px; } .repo-item { background: var(--surface); border: 1px solid var(--border); + border-radius: 6px; padding: 1.1rem 1.3rem; display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; - transition: background 0.15s; + transition: border-color 0.2s, box-shadow 0.15s; cursor: pointer; text-decoration: none; } .repo-item:hover { - background: var(--surface2); + border-color: var(--border-focus); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); } .repo-name { - font-family: "Syne", sans-serif; + font-family: var(--ff-mono); font-size: 0.88rem; - font-weight: 700; + font-weight: 500; color: var(--text); margin-bottom: 0.2rem; } .repo-desc { - font-size: 0.7rem; + font-family: var(--ff-sans); + font-size: 0.72rem; color: var(--muted); + font-weight: 300; } .repo-badge { + font-family: var(--ff-mono); font-size: 0.6rem; padding: 0.2rem 0.5rem; border: 1px solid var(--border); color: var(--muted); white-space: nowrap; flex-shrink: 0; + border-radius: 3px; } .repo-badge.private { color: var(--red); - border-color: rgba(224, 112, 112, 0.3); + border-color: rgba(185, 28, 28, 0.25); + background: rgba(185, 28, 28, 0.04); } .repo-badge.active { color: var(--green); - border-color: rgba(111, 207, 138, 0.3); + border-color: rgba(10, 124, 82, 0.25); + background: rgba(10, 124, 82, 0.04); } .logout-btn { margin-top: 1.2rem; + font-family: var(--ff-mono); font-size: 0.65rem; letter-spacing: 0.12em; text-transform: uppercase; @@ -870,7 +957,6 @@ main { background: none; border: none; cursor: pointer; - font-family: "DM Mono", monospace; padding: 0; transition: color 0.2s; } @@ -881,6 +967,7 @@ main { /* ─── Empty state ─── */ .empty-state { border: 1px dashed var(--border); + border-radius: 6px; padding: 3rem 2rem; display: flex; flex-direction: column; @@ -889,23 +976,26 @@ main { text-align: center; gap: 0.8rem; min-height: 280px; + background: var(--surface); } .empty-icon { font-size: 1.8rem; opacity: 0.2; } .empty-title { - font-family: "Syne", sans-serif; - font-size: 0.9rem; - font-weight: 700; + font-family: var(--ff-mono); + font-size: 0.88rem; + font-weight: 500; color: var(--muted); } .empty-sub { - font-size: 0.7rem; + font-family: var(--ff-sans); + font-size: 0.72rem; color: var(--muted); opacity: 0.6; max-width: 200px; line-height: 1.6; + font-weight: 300; } /* ─── Two column main ─── */ @@ -915,6 +1005,111 @@ main.two-col { grid-template-columns: 1fr 1fr; gap: 3rem; animation: fadeUp 0.6s 0.1s ease both; + position: relative; + z-index: 1; +} + +/* ─── Page nav ─── */ +.page-nav { + display: flex; + justify-content: center; + gap: 6px; + flex-wrap: wrap; + margin-bottom: 40px; + padding-top: 6px; + width: 100%; + font-family: var(--ff-mono); +} +.page-nav a { + font-family: var(--ff-mono); + font-size: 0.72rem; + color: var(--muted); + border: 1px solid var(--border); + padding: 5px 12px; + border-radius: 3px; + text-decoration: none; + letter-spacing: 0.04em; + transition: color 0.15s, border-color 0.15s, background 0.15s; + position: relative; + overflow: hidden; +} +.page-nav a:hover { + color: var(--accent); + border-color: var(--accent2); + background: rgba(26, 86, 219, 0.05); +} +.page-nav a span { + position: relative; + z-index: 1; +} +.page-nav a.active { + color: var(--accent); + border-color: var(--accent); + background: rgba(26, 86, 219, 0.06); +} + +/* ─── Attack table (cyber page) ─── */ +.attack-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + font-size: 0.76rem; + margin: 2rem 0; + font-family: var(--ff-mono); + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--border); +} +.attack-table thead th { + font-size: 0.6rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--dim); + padding: 0.6rem 1rem; + text-align: left; + border-bottom: 1px solid var(--border); + word-wrap: break-word; + background: var(--surface2); + font-weight: 400; +} +.attack-table tbody tr { + border-bottom: 1px solid var(--border); + transition: background 0.15s; + background: var(--surface); +} +.attack-table tbody tr:hover { + background: var(--surface2); +} +.attack-table tbody td { + padding: 0.9rem 1rem; + color: var(--muted); + vertical-align: top; + line-height: 1.55; + text-align: left; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; +} +.attack-table td:first-child { + color: var(--red); + font-weight: 500; + width: 18%; +} +.attack-table td:nth-child(2) { + width: 40%; +} +.attack-table td:nth-child(3) { + width: 42%; + color: var(--green); + font-size: 0.72rem; +} +.attack-table thead th:nth-child(1) { width: 18%; } +.attack-table thead th:nth-child(2) { width: 42%; } +.attack-table thead th:nth-child(3) { width: 40%; } + +nav .section-label { + margin: 0; + padding: 0; } /* ─── Responsive ─── */ @@ -947,136 +1142,12 @@ main.two-col { flex-direction: column; gap: 0.5rem; } + .classify-grid { + grid-template-columns: 1fr; + } } @media (max-width: 420px) { .cards { grid-template-columns: 1fr; } -} - -/* Add to styles.css */ -.page-nav { - display: flex; - gap: 6px; - flex-wrap: wrap; - margin-bottom: 40px; - width: 100%; -} - -.page-nav a { - font-family: var(--font-mono); - font-size: 11px; - color: var(--muted); - border: 1px solid var(--border); - padding: 5px 12px; - border-radius: 2px; - text-decoration: none; - letter-spacing: 0.04em; - transition: - color 0.18s, - border-color 0.18s, - background 0.18s; - position: relative; - overflow: hidden; -} - -.page-nav a::before { - content: ""; - position: absolute; - inset: 0; - background: var(--accent); - opacity: 0; - transition: opacity 0.18s; -} - -.page-nav a:hover { - color: var(--bg); - border-color: var(--accent); -} -.page-nav a:hover::before { - opacity: 1; -} -.page-nav a span { - position: relative; - z-index: 1; -} -.page-nav a.active { - color: var(--accent); - border-color: var(--accent); -} - -.page-nav { - display: flex; - justify-content: center; /* ← this */ - gap: 6px; - flex-wrap: wrap; - margin-bottom: 40px; - padding-top: 6px; -} - -/* ─── Attack table (cyber page) ─── */ -.attack-table { - width: 100%; - border-collapse: collapse; - table-layout: fixed; /* ← must be present */ - font-size: 0.76rem; - margin: 2rem 0; -} -.attack-table thead th { - font-size: 0.6rem; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--muted); - padding: 0.6rem 1rem; - text-align: left; - border-bottom: 1px solid var(--border); - word-wrap: break-word; -} -.attack-table tbody tr { - border-bottom: 1px solid var(--border); - transition: background 0.15s; -} -.attack-table tbody tr:hover { - background: var(--surface2); -} -.attack-table tbody td { - padding: 0.9rem 1rem; - color: #c8c8c0; - vertical-align: top; - line-height: 1.55; - text-align: left; - word-wrap: break-word; - overflow-wrap: break-word; -} -.attack-table td:first-child { - color: var(--red); - font-weight: 500; - width: 18%; -} -.attack-table td:nth-child(2) { - width: 40%; -} -.attack-table td:nth-child(3) { - width: 42%; - color: var(--green); - font-size: 0.72rem; -} - -.attack-table tbody td { - white-space: normal; /* ← overrides the global tbody td rule */ -} - -.attack-table thead th:nth-child(1) { - width: 18%; -} -.attack-table thead th:nth-child(2) { - width: 42%; -} -.attack-table thead th:nth-child(3) { - width: 40%; -} - -nav .section-label { - margin: 0; - padding: 0; -} +} \ No newline at end of file diff --git a/static/styles.css.old b/static/styles.css.old new file mode 100644 index 0000000..3676981 --- /dev/null +++ b/static/styles.css.old @@ -0,0 +1,1153 @@ +/* ─── Variables ─── */ +:root { + --bg: #f7f6f3; + --surface: #ffffff; + --surface2: #f0eeea; + --border: #e2ddd6; + --border-focus: #ccc7be; + --text: #1a1917; + --muted: #6b6560; + --dim: #b0aa9f; + --accent: #1a56db; + --accent2: #1447b8; + --green: #0a7c52; + --red: #b91c1c; + --yellow: #92620a; + --ff-mono: 'IBM Plex Mono', monospace; + --ff-sans: 'IBM Plex Sans', sans-serif; +} + +/* ─── Reset ─── */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} +html { + scroll-behavior: smooth; +} + +/* ─── Base ─── */ +body { + background: var(--bg); + color: var(--text); + font-family: var(--ff-mono); + font-weight: 300; + min-height: 100vh; + overflow-x: hidden; +} + +/* Dot grid overlay */ +body::before { + content: ""; + position: fixed; + inset: 0; + background-image: radial-gradient(circle, var(--border) 1px, transparent 1px); + background-size: 24px 24px; + opacity: 0.5; + pointer-events: none; + z-index: 0; +} + +/* ─── Layout ─── */ +.container { + max-width: 860px; + margin: 0 auto; + padding: 0 2rem; +} +.container--wide { + max-width: 1100px; + margin: 0 auto; + padding: 0 2rem; +} +.container--narrow { + max-width: 720px; + margin: 0 auto; + padding: 0 2rem; +} + +/* ─── Header ─── */ +header { + border-bottom: 1px solid var(--border); + animation: fadeUp 0.6s ease both; + position: relative; + z-index: 1; +} + +.back { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.72rem; + color: var(--muted); + text-decoration: none; + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 2rem; + transition: color 0.2s; + font-family: var(--ff-mono); +} +.back:hover { + color: var(--accent); +} + +.tag { + font-size: 0.7rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 1rem; + font-family: var(--ff-mono); +} + +h1 { + font-family: var(--ff-mono); + font-size: clamp(2rem, 5vw, 3.2rem); + font-weight: 500; + line-height: 1.05; + letter-spacing: -0.02em; + color: var(--text); +} +h1 span { + color: var(--accent); + font-size: clamp(1.6rem, 2.5vw, 2.8rem); +} + +.header-sub { + margin-top: 0.6rem; + font-size: 0.78rem; + color: var(--muted); +} + +.byline { + margin-top: 1.2rem; + font-size: 0.72rem; + color: var(--muted); + display: flex; + gap: 1.5rem; + font-family: var(--ff-mono); +} +.byline span { + color: var(--accent); +} + +/* ─── Typography ─── */ +h2 { + font-family: var(--ff-mono); + font-size: 1rem; + font-weight: 500; + color: var(--accent); + margin: 2.8rem 0 1rem; + letter-spacing: -0.01em; +} +h2::before { + content: "// "; + color: var(--dim); + font-family: var(--ff-mono); + font-weight: 300; +} + +p { + font-family: var(--ff-sans); + font-size: 0.88rem; + line-height: 1.9; + color: var(--muted); + margin-bottom: 1.2rem; + font-weight: 300; +} + +code { + font-family: var(--ff-mono); + font-size: 0.78rem; + color: var(--accent); + background: rgba(26, 86, 219, 0.06); + padding: 0.1rem 0.4rem; + border: 1px solid rgba(26, 86, 219, 0.15); + border-radius: 2px; +} + +hr { + border: none; + border-top: 1px solid var(--border); + margin: 2.5rem 0; +} + +.section-label { + font-family: var(--ff-mono); + font-size: 0.65rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--dim); + margin-bottom: 1.2rem; +} + +.lead { + font-family: var(--ff-sans); + font-size: 0.95rem; + line-height: 1.9; + color: var(--text); + font-weight: 300; + border-left: 2px solid var(--accent2); + padding-left: 1.5rem; + margin-bottom: 3rem; +} + +blockquote { + margin: 2.5rem 0; + padding: 1.6rem 2rem; + background: var(--surface); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + font-family: var(--ff-mono); + font-size: 0.95rem; + font-weight: 400; + color: var(--text); + line-height: 1.6; + border-radius: 0 4px 4px 0; +} +blockquote span { + color: var(--accent); +} + +/* ─── Color utilities ─── */ +.up { + color: var(--green); +} +.down { + color: var(--red); +} +.neutral { + color: var(--muted); +} + +/* ─── Cards ─── */ +.cards { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin: 3rem 0 2.5rem; + animation: fadeUp 0.6s 0.1s ease both; +} +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.4rem 1.6rem; + transition: border-color 0.2s, box-shadow 0.2s; +} +.card:hover { + border-color: var(--border-focus); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); +} +.card-label { + font-family: var(--ff-mono); + font-size: 0.62rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--dim); + margin-bottom: 0.5rem; +} +.card-value { + font-family: var(--ff-mono); + font-size: 1.4rem; + font-weight: 500; + color: var(--text); +} + +/* ─── Nav grid (homepage) ─── */ +.nav-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} +.nav-item { + display: block; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 2rem 2rem 1.8rem; + text-decoration: none; + color: var(--text); + position: relative; + overflow: hidden; + transition: border-color 0.2s, box-shadow 0.2s; + animation: fadeUp 0.7s ease both; +} +.nav-item::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background: var(--accent); + transition: width 0.3s ease; +} +.nav-item:hover { + border-color: var(--accent2); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.07); +} +.nav-item:hover::after { + width: 100%; +} +.nav-item:hover .arrow { + transform: translate(4px, -4px); +} +.nav-number { + font-family: var(--ff-mono); + font-size: 0.65rem; + color: var(--dim); + letter-spacing: 0.15em; + margin-bottom: 1.4rem; +} +.nav-title { + font-family: var(--ff-mono); + font-size: 1.1rem; + font-weight: 500; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + color: var(--text); +} +.arrow { + font-size: 1rem; + color: var(--accent); + transition: transform 0.25s ease; + flex-shrink: 0; +} +.nav-desc { + font-family: var(--ff-sans); + font-size: 0.78rem; + color: var(--muted); + line-height: 1.6; + font-weight: 300; +} + +/* ─── Table ─── */ +.table-wrap { + overflow-x: auto; + animation: fadeUp 0.6s 0.2s ease both; + margin-bottom: 2.5rem; + border: 1px solid var(--border); + border-radius: 6px; +} +table { + width: 100%; + border-collapse: collapse; + font-size: 0.78rem; + font-family: var(--ff-mono); +} +thead tr { + border-bottom: 1px solid var(--border); + background: var(--surface2); +} +thead th { + font-size: 0.62rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--dim); + padding: 0.7rem 1rem; + text-align: right; + white-space: nowrap; + font-weight: 400; +} +thead th:first-child { + text-align: left; +} +tbody tr { + border-bottom: 1px solid var(--border); + transition: background 0.15s; + background: var(--surface); +} +tbody tr:hover { + background: var(--surface2); +} +tbody td { + padding: 1rem; + text-align: right; + white-space: nowrap; + color: var(--text); +} +tbody td:first-child { + text-align: left; +} + +.ticker { + font-family: var(--ff-mono); + font-weight: 500; + font-size: 0.88rem; + color: var(--text); +} +.currency-badge { + display: inline-block; + font-size: 0.6rem; + padding: 0.15rem 0.4rem; + border: 1px solid var(--border); + color: var(--muted); + margin-left: 0.4rem; + vertical-align: middle; + border-radius: 2px; +} +.weight-cell { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.6rem; +} +.weight-bar-track { + width: 60px; + height: 3px; + background: var(--border); + border-radius: 2px; + overflow: hidden; +} +.weight-bar-fill { + height: 100%; + background: var(--accent); + border-radius: 2px; + opacity: 0.5; +} +.watchlist-row td { + color: var(--muted); +} +.watchlist-label { + font-size: 0.62rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--dim); + padding: 1.2rem 1rem 0.5rem; +} + +/* ─── Skill bars ─── */ +.skills-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} +.skill-row { + display: grid; + grid-template-columns: 80px 1fr; + align-items: center; + gap: 1rem; + font-size: 0.78rem; + color: var(--muted); + font-family: var(--ff-mono); +} +.skill-bar-track { + height: 3px; + background: var(--border); + border-radius: 2px; + overflow: hidden; +} +.skill-bar-fill { + height: 100%; + background: var(--accent); + border-radius: 2px; + opacity: 0.6; + transform-origin: left; + animation: growBar 0.8s ease both; +} + +/* ─── Compare grid ─── */ +.compare { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin: 2rem 0; +} +.compare-col { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.4rem 1.6rem; + transition: border-color 0.2s; +} +.compare-col:hover { + border-color: var(--border-focus); +} +.compare-col h3 { + font-family: var(--ff-mono); + font-size: 0.82rem; + font-weight: 500; + margin-bottom: 1rem; + padding-bottom: 0.6rem; + border-bottom: 1px solid var(--border); +} +.compare-col.bad h3 { + color: var(--red); +} +.compare-col.good h3 { + color: var(--green); +} +.compare-col ul { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.6rem; +} +.compare-col ul li { + font-family: var(--ff-sans); + font-size: 0.78rem; + color: var(--muted); + line-height: 1.5; + padding-left: 1rem; + position: relative; + font-weight: 300; +} +.compare-col.bad ul li::before { + content: "—"; + position: absolute; + left: 0; + color: var(--red); +} +.compare-col.good ul li::before { + content: "+"; + position: absolute; + left: 0; + color: var(--green); +} + +/* ─── Experience & Education ─── */ +.exp-block { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.5rem; + margin-bottom: 8px; + transition: border-color 0.2s, box-shadow 0.2s; +} +.exp-block:hover { + border-color: var(--border-focus); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); +} +.exp-block p { + font-size: 0.78rem; + color: var(--muted); + line-height: 1.7; + margin-top: 0.4rem; +} + +.edu-item { + display: grid; + grid-template-columns: 80px 1fr; + gap: 1.5rem; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border); +} +.edu-item:last-child { + border-bottom: none; + margin-bottom: 0; +} +.edu-year { + font-family: var(--ff-mono); + font-size: 0.65rem; + color: var(--yellow); + letter-spacing: 0.1em; + padding-top: 0.15rem; +} +.edu-title { + font-family: var(--ff-mono); + font-size: 0.92rem; + font-weight: 500; + margin-bottom: 0.25rem; + color: var(--text); +} +.edu-sub { + font-family: var(--ff-sans); + font-size: 0.78rem; + color: var(--muted); + line-height: 1.6; + font-weight: 300; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} +.tag-pill { + font-family: var(--ff-mono); + font-size: 0.7rem; + padding: 0.3rem 0.75rem; + border: 1px solid var(--border); + color: var(--muted); + letter-spacing: 0.05em; + border-radius: 3px; + transition: border-color 0.15s, color 0.15s; +} +.tag-pill:hover { + border-color: var(--accent2); + color: var(--accent); +} + +/* ─── Forms / Login ─── */ +.form-group { + margin-bottom: 1.2rem; +} +label { + display: block; + font-family: var(--ff-mono); + font-size: 0.62rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 0.5rem; +} +input[type="text"], +input[type="password"] { + width: 100%; + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + font-family: var(--ff-mono); + font-size: 0.82rem; + padding: 0.7rem 0.9rem; + outline: none; + border-radius: 4px; + transition: border-color 0.2s, box-shadow 0.2s; +} +input[type="text"]:focus, +input[type="password"]:focus { + border-color: var(--accent2); + box-shadow: 0 0 0 3px rgba(26, 71, 184, 0.1); +} +input::placeholder { + color: var(--dim); +} + +.btn { + width: 100%; + background: var(--accent2); + color: #ffffff; + font-family: var(--ff-mono); + font-size: 0.78rem; + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 0.8rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s, opacity 0.2s; +} +.btn:hover { + background: var(--accent); +} +.btn:active { + opacity: 0.85; +} + +/* ─── Footer ─── */ +footer { + padding: 2.5rem 0 3rem; + border-top: 1px solid var(--border); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.6rem; + text-align: center; + animation: fadeUp 0.6s 0.4s ease both; + position: relative; + z-index: 1; +} +footer a { + font-family: var(--ff-mono); + color: var(--accent); + text-decoration: none; + font-size: 0.82rem; + transition: opacity 0.2s; +} +footer a:hover { + opacity: 0.7; +} +.footer-copy { + font-family: var(--ff-mono); + font-size: 0.68rem; + color: var(--dim); + letter-spacing: 0.05em; +} + +/* ─── Animations ─── */ +@keyframes fadeUp { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +@keyframes growBar { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } +} +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-6px); } + 40% { transform: translateX(6px); } + 60% { transform: translateX(-4px); } + 80% { transform: translateX(4px); } +} +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} +.shake { + animation: shake 0.4s ease; +} + +/* ─── Article layout ─── */ +article { + padding: 3.5rem 0; + animation: fadeUp 0.6s 0.1s ease both; + position: relative; + z-index: 1; +} +main { + padding: 3.5rem 0; + position: relative; + z-index: 1; +} + +/* ─── Classify grid (cyber page) ─── */ +.classify-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin: 2rem 0; +} +.classify-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.4rem 1.3rem; + transition: border-color 0.2s, box-shadow 0.2s; +} +.classify-card:hover { + border-color: var(--border-focus); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); +} +.classify-card h3 { + font-family: var(--ff-mono); + font-size: 0.88rem; + font-weight: 500; + margin-bottom: 0.5rem; + color: var(--text); +} +.classify-card p { + font-size: 0.72rem; + color: var(--muted); + margin: 0; + line-height: 1.6; +} +.level { + font-family: var(--ff-mono); + font-size: 0.6rem; + letter-spacing: 0.18em; + text-transform: uppercase; + margin-bottom: 0.6rem; + font-weight: 500; +} +.level-critical { + color: var(--red); +} +.level-high { + color: var(--yellow); +} +.level-standard { + color: var(--green); +} + +/* ─── Principles (cyber page) ─── */ +.principles { + display: flex; + flex-direction: column; + gap: 8px; + margin: 2rem 0; +} +.principle { + display: grid; + grid-template-columns: 2.5rem 1fr; + gap: 1.2rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.2rem 1.4rem; + align-items: start; + transition: border-color 0.2s; +} +.principle:hover { + border-color: var(--border-focus); +} +.principle-num { + font-family: var(--ff-mono); + font-size: 1rem; + font-weight: 500; + color: var(--accent2); + padding-top: 0.1rem; +} +.principle-title { + font-family: var(--ff-mono); + font-size: 0.88rem; + font-weight: 500; + color: var(--text); + margin-bottom: 0.35rem; +} +.principle p { + font-size: 0.77rem; + color: var(--muted); + line-height: 1.65; + margin: 0; +} + +.counter { + color: var(--green); + font-size: 0.72rem; + font-family: var(--ff-mono); +} + +/* ─── Login panel ─── */ +.login-panel { + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface); + padding: 2rem; +} +.login-panel h2 { + font-family: var(--ff-mono); + font-size: 0.95rem; + font-weight: 500; + margin-bottom: 0.3rem; + color: var(--text); +} +.login-panel h2::before { + display: none; +} +.login-panel .desc { + font-family: var(--ff-sans); + font-size: 0.78rem; + color: var(--muted); + margin-bottom: 2rem; + line-height: 1.6; + font-weight: 300; +} +.twofa-group { + display: none; + margin-bottom: 1.2rem; +} +.twofa-group.visible { + display: block; +} +.twofa-hint { + font-family: var(--ff-mono); + font-size: 0.68rem; + color: var(--muted); + margin-top: 0.4rem; +} +.btn-login { + width: 100%; + background: var(--accent2); + color: #ffffff; + font-family: var(--ff-mono); + font-size: 0.78rem; + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 0.8rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s, opacity 0.2s; + margin-top: 0.4rem; +} +.btn-login:hover { + background: var(--accent); +} +.btn-login:active { + opacity: 0.85; +} +.login-error { + font-family: var(--ff-mono); + font-size: 0.72rem; + color: var(--red); + margin-top: 1rem; + display: none; +} +.login-error.visible { + display: block; +} + +/* ─── Private dashboard ─── */ +#private-dashboard { + display: none; +} +#private-dashboard.visible { + display: block; +} +.repo-list { + display: flex; + flex-direction: column; + gap: 8px; +} +.repo-item { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.1rem 1.3rem; + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + transition: border-color 0.2s, box-shadow 0.15s; + cursor: pointer; + text-decoration: none; +} +.repo-item:hover { + border-color: var(--border-focus); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); +} +.repo-name { + font-family: var(--ff-mono); + font-size: 0.88rem; + font-weight: 500; + color: var(--text); + margin-bottom: 0.2rem; +} +.repo-desc { + font-family: var(--ff-sans); + font-size: 0.72rem; + color: var(--muted); + font-weight: 300; +} +.repo-badge { + font-family: var(--ff-mono); + font-size: 0.6rem; + padding: 0.2rem 0.5rem; + border: 1px solid var(--border); + color: var(--muted); + white-space: nowrap; + flex-shrink: 0; + border-radius: 3px; +} +.repo-badge.private { + color: var(--red); + border-color: rgba(185, 28, 28, 0.25); + background: rgba(185, 28, 28, 0.04); +} +.repo-badge.active { + color: var(--green); + border-color: rgba(10, 124, 82, 0.25); + background: rgba(10, 124, 82, 0.04); +} +.logout-btn { + margin-top: 1.2rem; + font-family: var(--ff-mono); + font-size: 0.65rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--muted); + background: none; + border: none; + cursor: pointer; + padding: 0; + transition: color 0.2s; +} +.logout-btn:hover { + color: var(--red); +} + +/* ─── Empty state ─── */ +.empty-state { + border: 1px dashed var(--border); + border-radius: 6px; + padding: 3rem 2rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 0.8rem; + min-height: 280px; + background: var(--surface); +} +.empty-icon { + font-size: 1.8rem; + opacity: 0.2; +} +.empty-title { + font-family: var(--ff-mono); + font-size: 0.88rem; + font-weight: 500; + color: var(--muted); +} +.empty-sub { + font-family: var(--ff-sans); + font-size: 0.72rem; + color: var(--muted); + opacity: 0.6; + max-width: 200px; + line-height: 1.6; + font-weight: 300; +} + +/* ─── Two column main ─── */ +main.two-col { + padding: 3.5rem 0; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3rem; + animation: fadeUp 0.6s 0.1s ease both; + position: relative; + z-index: 1; +} + +/* ─── Page nav ─── */ +.page-nav { + display: flex; + justify-content: center; + gap: 6px; + flex-wrap: wrap; + margin-bottom: 40px; + padding-top: 6px; + width: 100%; + font-family: var(--ff-mono); +} +.page-nav a { + font-family: var(--ff-mono); + font-size: 0.72rem; + color: var(--muted); + border: 1px solid var(--border); + padding: 5px 12px; + border-radius: 3px; + text-decoration: none; + letter-spacing: 0.04em; + transition: color 0.15s, border-color 0.15s, background 0.15s; + position: relative; + overflow: hidden; +} +.page-nav a:hover { + color: var(--accent); + border-color: var(--accent2); + background: rgba(26, 86, 219, 0.05); +} +.page-nav a span { + position: relative; + z-index: 1; +} +.page-nav a.active { + color: var(--accent); + border-color: var(--accent); + background: rgba(26, 86, 219, 0.06); +} + +/* ─── Attack table (cyber page) ─── */ +.attack-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + font-size: 0.76rem; + margin: 2rem 0; + font-family: var(--ff-mono); + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--border); +} +.attack-table thead th { + font-size: 0.6rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--dim); + padding: 0.6rem 1rem; + text-align: left; + border-bottom: 1px solid var(--border); + word-wrap: break-word; + background: var(--surface2); + font-weight: 400; +} +.attack-table tbody tr { + border-bottom: 1px solid var(--border); + transition: background 0.15s; + background: var(--surface); +} +.attack-table tbody tr:hover { + background: var(--surface2); +} +.attack-table tbody td { + padding: 0.9rem 1rem; + color: var(--muted); + vertical-align: top; + line-height: 1.55; + text-align: left; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; +} +.attack-table td:first-child { + color: var(--red); + font-weight: 500; + width: 18%; +} +.attack-table td:nth-child(2) { + width: 40%; +} +.attack-table td:nth-child(3) { + width: 42%; + color: var(--green); + font-size: 0.72rem; +} +.attack-table thead th:nth-child(1) { width: 18%; } +.attack-table thead th:nth-child(2) { width: 42%; } +.attack-table thead th:nth-child(3) { width: 40%; } + +nav .section-label { + margin: 0; + padding: 0; +} + +/* ─── Responsive ─── */ +@media (max-width: 700px) { + .cards { + grid-template-columns: 1fr 1fr; + } +} +@media (max-width: 640px) { + main.two-col { + grid-template-columns: 1fr; + gap: 2.5rem; + } +} +@media (max-width: 560px) { + .nav-grid { + grid-template-columns: 1fr; + } + .edu-item { + grid-template-columns: 1fr; + gap: 0.4rem; + } + .skill-row { + grid-template-columns: 70px 1fr; + } + .compare { + grid-template-columns: 1fr; + } + footer { + flex-direction: column; + gap: 0.5rem; + } + .classify-grid { + grid-template-columns: 1fr; + } +} +@media (max-width: 420px) { + .cards { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/static/styles2.css b/static/styles2.css new file mode 100644 index 0000000..e082bca --- /dev/null +++ b/static/styles2.css @@ -0,0 +1,231 @@ +:root{ + --bg:#f7f6f3; + --surface:#ffffff; + --surface2:#f0eeea; + --border:#e2ddd6; + --border2:#ccc7be; + --text:#1a1917; + --muted:#6b6560; + --dim:#b0aa9f; + --accent:#1a56db; + --accent2:#1447b8; + --green:#0a7c52; + --amber:#92620a; + --ff-mono:'IBM Plex Mono',monospace; + --ff-sans:'IBM Plex Sans',sans-serif; +} +*{box-sizing:border-box;margin:0;padding:0;} +html{font-size:15px;scroll-behavior:smooth;} +body{ + background:var(--bg); + color:var(--text); + font-family:var(--ff-sans); + font-weight:300; + line-height:1.7; + min-height:100vh; +} + +/* subtle dot grid */ +body::before{ + content:''; + position:fixed;inset:0; + background-image:radial-gradient(circle, var(--border) 1px, transparent 1px); + background-size:24px 24px; + opacity:0.5; + pointer-events:none;z-index:0; +} + +nav{ + position:sticky;top:0;z-index:100; + background:rgba(247,246,243,0.94); + 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{ + font-family:var(--ff-mono);font-size:0.78rem;font-weight:500; + color:var(--accent);text-decoration:none; + display:flex;align-items:center;gap:6px; +} +.nav-logo::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; + color:var(--muted);text-decoration:none; + padding:0.3rem 0.7rem;border-radius:4px; + transition:color 0.15s,background 0.15s; +} +.nav-links a:hover{color:var(--text);background:var(--surface2);} +.nav-links .cta{color:var(--accent);border:1px solid var(--accent2);margin-left:0.4rem;} +.nav-links .cta:hover{background:var(--accent2);color:#fff;} + +.shell{ + position:relative;z-index:1; + display:grid; + grid-template-columns:220px 1fr; + min-height:calc(100vh - 52px); + max-width:1100px; + margin:0 auto; +} + +aside{ + border-right:1px solid var(--border); + padding:2.5rem 1.5rem; + display:flex;flex-direction:column;gap:2.5rem; + position:sticky;top:52px;height:calc(100vh - 52px); + overflow-y:auto; + background:var(--surface); +} +.sidebar-label{ + font-family:var(--ff-mono);font-size:0.65rem; + letter-spacing:0.2em;text-transform:uppercase; + color:var(--dim);margin-bottom:0.8rem; +} +.sidebar-links{display:flex;flex-direction:column;gap:2px;} +.sidebar-links a{ + font-family:var(--ff-mono);font-size:0.78rem;color:var(--muted); + text-decoration:none;padding:0.3rem 0.6rem;border-radius:4px; + transition:color 0.15s,background 0.15s; + display:flex;align-items:center;gap:6px; +} +.sidebar-links 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{ + font-family:var(--ff-mono);font-size:0.78rem; + color:var(--muted);padding:0.3rem 0.6rem; + border-left:2px solid var(--border2); + transition:border-color 0.15s,color 0.15s;cursor:default; +} +.skill-item:hover{border-color:var(--accent);color:var(--text);} + +main{ + padding:3rem clamp(1.5rem,4vw,3rem); + display:flex;flex-direction:column;gap:4rem; +} + +.hero{padding-bottom:3rem;border-bottom:1px solid var(--border);} +.hero-eyebrow{ + font-family:var(--ff-mono);font-size:0.72rem;color:var(--green); + letter-spacing:0.18em;margin-bottom:1rem; + display:flex;align-items:center;gap:8px; +} +.hero-eyebrow::before{content:'';display:block;width:20px;height:1px;background:var(--green);} +h1{ + font-family:var(--ff-mono); + font-size:clamp(1.8rem,4vw,2.8rem); + font-weight:500;color:var(--text); + letter-spacing:-0.02em;line-height:1.1; +} +h1 span{color:var(--accent);} +.hero-bio{ + margin-top:1.2rem;font-size:0.9rem;color:var(--muted); + max-width:560px;line-height:1.8; +} +.hero-meta{ + margin-top:1.5rem;display:flex;gap:1.5rem;flex-wrap:wrap; + font-family:var(--ff-mono);font-size:0.72rem;color:var(--dim); +} +.hero-meta span{display:flex;align-items:center;gap:5px;} +.dot{ + width:6px;height:6px;border-radius:50%; + background:var(--green);display:inline-block; + animation:pulse 2s infinite; +} +@keyframes pulse{0%,100%{opacity:1;}50%{opacity:0.35;}} + +.section-head{display:flex;align-items:center;gap:10px;margin-bottom:1.8rem;} +.section-head h2{ + font-family:var(--ff-mono);font-size:0.72rem;font-weight:400; + letter-spacing:0.25em;text-transform:uppercase;color:var(--muted); + white-space:nowrap; +} +.section-head::after{content:'';flex:1;height:1px;background:var(--border);} + +.exp-grid{display:flex;flex-direction:column;gap:8px;} +.exp-card{ + background:var(--surface);border:1px solid var(--border); + border-radius:6px;padding:1.4rem 1.5rem; + 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-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; + letter-spacing:0.1em;text-transform:uppercase; + background:rgba(26,86,219,0.07);color:var(--accent); + border:1px solid rgba(26,86,219,0.2); + padding:2px 8px;border-radius:3px; +} +.exp-body{font-size:0.85rem;color:var(--muted);line-height:1.8;} + +.edu-grid{display:flex;flex-direction:column;gap:8px;} +.edu-card{ + display:grid;grid-template-columns:80px 1fr;gap:1.2rem; + background:var(--surface);border:1px solid var(--border); + border-radius:6px;padding:1.2rem 1.4rem; + transition:border-color 0.2s,box-shadow 0.2s; +} +.edu-card:hover{border-color:var(--border2);box-shadow:0 2px 12px rgba(0,0,0,0.06);} +.edu-year{font-family:var(--ff-mono);font-size:0.7rem;color:var(--amber);padding-top:2px;line-height:1.6;} +.edu-degree{font-size:0.9rem;font-weight:500;color:var(--text);} +.edu-inst{font-family:var(--ff-mono);font-size:0.75rem;color:var(--muted);margin-top:2px;} +.edu-detail{font-size:0.82rem;color:var(--dim);margin-top:4px;} + +.lang-row{display:flex;gap:0.6rem;flex-wrap:wrap;} +.lang-badge{ + font-family:var(--ff-mono);font-size:0.78rem;color:var(--text); + background:var(--surface);border:1px solid var(--border); + padding:0.35rem 0.9rem;border-radius:4px; +} + +.interests-row{display:flex;gap:0.6rem;flex-wrap:wrap;} +.interest-chip{ + font-family:var(--ff-mono);font-size:0.75rem;color:var(--muted); + background:var(--surface);border:1px solid var(--border); + padding:0.3rem 0.75rem;border-radius:4px; +} +.interest-chip::before{content:'# ';color:var(--dim);} + +.opinion-grid{display:flex;flex-wrap:wrap;gap:0.6rem;} +.opinion-link{ + font-family:var(--ff-mono);font-size:0.78rem; + color:var(--muted);text-decoration:none; + background:var(--surface);border:1px solid var(--border); + padding:0.4rem 0.9rem;border-radius:4px; + display:flex;align-items:center;gap:6px; + transition:border-color 0.2s,color 0.2s,box-shadow 0.2s; +} +.opinion-link::before{content:'→';color:var(--dim);font-size:0.75rem;} +.opinion-link:hover{border-color:var(--accent2);color:var(--text);box-shadow:0 2px 8px rgba(0,0,0,0.06);} + +footer{ + position:relative;z-index:1; + border-top:1px solid var(--border); + padding:1.4rem clamp(1rem,5vw,3rem); + display:flex;align-items:center;justify-content:space-between; + flex-wrap:wrap;gap:0.5rem; + background:var(--surface); +} +.footer-contact a{font-family:var(--ff-mono);font-size:0.78rem;color:var(--accent);text-decoration:none;} +.footer-contact a:hover{text-decoration:underline;} +.footer-copy{font-family:var(--ff-mono);font-size:0.7rem;color:var(--dim);} + +@keyframes fadeUp{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:translateY(0);}} +.hero{animation:fadeUp 0.6s ease both;} +.exp-card:nth-child(1){animation:fadeUp 0.5s 0.05s ease both;} +.exp-card:nth-child(2){animation:fadeUp 0.5s 0.12s ease both;} +.edu-card:nth-child(1){animation:fadeUp 0.5s 0.08s ease both;} +.edu-card:nth-child(2){animation:fadeUp 0.5s 0.15s ease both;} + +@media(max-width:700px){ + .shell{grid-template-columns:1fr;} + aside{display:none;} + .edu-card{grid-template-columns:1fr;gap:0.3rem;} +}