Full design refactor of the public surface (home, blog, layout) using the taste-skill anti-slop rules. Admin CMS is untouched. - Single locked light theme: #fafafa bg, #18181b text, one accent #2563eb - Syne headings + system body + Vazirmatn (fa); hairline rules, no glows/cards - Remove AI tells: 5-colour palette, gradient text, neon glows, custom cursor, particle canvas, typewriter, scroll cue, per-section eyebrows, progress bars - Replace window scroll listener with an IntersectionObserver sentinel - 8 distinct section layouts; portfolio uses typographic covers (no broken imgs) - Zero em-dashes in visible copy; fix relative-path-safe asset refs - Add missing wwwroot/logo-mark.svg (was 404) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+15
-16
@@ -1,31 +1,30 @@
|
||||
@page "/blog"
|
||||
@model SoroushAsadi.Pages.Blog.BlogIndexModel
|
||||
@{
|
||||
ViewData["Title"] = Model.IsFa ? "بلاگ — سروش اسعدی" : "Blog — Soroush Asadi";
|
||||
ViewData["Title"] = Model.IsFa ? "بلاگ - سروش اسعدی" : "Blog - Soroush Asadi";
|
||||
var fa = Model.IsFa;
|
||||
}
|
||||
|
||||
<div class="min-h-screen pt-28 pb-20 px-5 sm:px-8">
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<div class="section-header mb-14">
|
||||
<div class="eyebrow mb-4"><span class="chip">@(fa ? "بلاگ" : "Journal")</span></div>
|
||||
<h1 class="font-display text-4xl font-extrabold text-white @(fa ? "font-fa" : "")">
|
||||
<div class="px-5 pt-28 pb-24 sm:px-8 sm:pt-32">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="sec-head">
|
||||
<h1 class="@(fa ? "font-fa" : "")" style="font-size:clamp(2rem,4vw,2.75rem)">
|
||||
@(fa ? "یادداشتهای مهندسی" : "Engineering notes")
|
||||
</h1>
|
||||
<p class="mt-4 text-slate-400">@(fa ? "یافتهها از پروژههای واقعی — نه ترجمهی مقاله، نه فهرست hype." : "Findings from real engagements — not translated articles, not hype lists.")</p>
|
||||
<p class="lede mt-4">@(fa ? "یافتهها از پروژههای واقعی. نه ترجمهی مقاله، نه فهرست هیجان." : "Findings from real engagements. Not translated articles, not hype lists.")</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="border-b border-zinc-200">
|
||||
@foreach (var post in Model.Posts)
|
||||
{
|
||||
<a href="/blog/@post.Slug" class="group glass block p-6 transition-all duration-300 hover:-translate-y-1 hover:border-electric/40 reveal">
|
||||
<span class="label-mono text-electric mb-3 block">@post.Category</span>
|
||||
<h2 class="font-display font-semibold leading-snug text-white group-hover:text-electric transition-colors @(fa ? "font-fa" : "")"
|
||||
style="font-size:clamp(1rem,1.4vw,1.15rem)">@post.Title</h2>
|
||||
<p class="mt-3 text-[.88rem] leading-relaxed text-slate-400 line-clamp-3">@post.Excerpt</p>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="label-mono">@post.ReadTime @(fa ? "دقیقه" : "min") @(fa ? "مطالعه" : "read")</span>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" class="text-electric @(fa ? "rotate-180" : "")"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
||||
<a href="/blog/@post.Slug" class="group reveal grid grid-cols-1 gap-2 border-t border-zinc-200 py-6 sm:grid-cols-[8rem_1fr] sm:gap-8">
|
||||
<div class="flex items-baseline justify-between sm:flex-col sm:gap-1">
|
||||
<span class="kicker">@post.Category</span>
|
||||
<span class="text-[.78rem] text-zinc-400">@post.ReadTime @(fa ? "دقیقه" : "min")</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-[1.1rem] font-semibold transition-colors group-hover:text-accent @(fa ? "font-fa" : "")">@post.Title</h2>
|
||||
<p class="mt-1.5 text-[.9rem] leading-relaxed text-zinc-600">@post.Excerpt</p>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
+12
-28
@@ -1,48 +1,32 @@
|
||||
@page "/blog/{slug}"
|
||||
@model SoroushAsadi.Pages.Blog.PostModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Title + " — Soroush Asadi";
|
||||
ViewData["Title"] = Model.Title + " - Soroush Asadi";
|
||||
var fa = Model.IsFa;
|
||||
}
|
||||
|
||||
<div class="min-h-screen pt-28 pb-20 px-5 sm:px-8">
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<a href="/blog" class="label-mono mb-8 inline-flex items-center gap-2 text-slate-400 hover:text-white transition-colors @(fa ? "flex-row-reverse" : "")">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" class="@(fa ? "" : "rotate-180")"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
||||
<div class="px-5 pt-28 pb-24 sm:px-8 sm:pt-32">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<a href="/blog" class="mb-10 inline-flex items-center gap-2 text-sm text-zinc-500 transition-colors hover:text-zinc-900 @(fa ? "flex-row-reverse" : "")">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" class="@(fa ? "" : "rotate-180")"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
||||
@(fa ? "بازگشت به بلاگ" : "Back to blog")
|
||||
</a>
|
||||
|
||||
@if (Model.PostNotFound)
|
||||
{
|
||||
<p class="text-slate-400">@(fa ? "مقاله پیدا نشد." : "Post not found.")</p>
|
||||
<p class="text-zinc-600">@(fa ? "مقاله پیدا نشد." : "Post not found.")</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-6">
|
||||
<span class="label-mono text-electric">@Model.Category</span>
|
||||
<h1 class="mt-3 font-display text-3xl font-extrabold leading-tight text-white @(fa ? "font-fa" : "")">@Model.Title</h1>
|
||||
<p class="mt-2 label-mono text-slate-500">@Model.ReadTime @(fa ? "دقیقه مطالعه" : "min read")</p>
|
||||
</div>
|
||||
<header class="mb-8">
|
||||
<span class="kicker">@Model.Category</span>
|
||||
<h1 class="mt-3 @(fa ? "font-fa" : "")" style="font-size:clamp(1.8rem,4vw,2.5rem)">@Model.Title</h1>
|
||||
<p class="mt-3 text-sm text-zinc-400">@Model.ReadTime @(fa ? "دقیقه مطالعه" : "min read")</p>
|
||||
</header>
|
||||
|
||||
<article class="prose-custom glass p-8">
|
||||
<article class="prose-custom">
|
||||
@Html.Raw(Model.BodyHtml)
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<style>
|
||||
.prose-custom { color:#cbd5e1; line-height:1.8; }
|
||||
.prose-custom h2,.prose-custom h3 { font-family:'Syne',sans-serif; font-weight:700; color:#fff; margin:2rem 0 .75rem; }
|
||||
.prose-custom p { margin-bottom:1.25rem; }
|
||||
.prose-custom code { font-family:'SpaceMono',monospace; background:rgba(56,189,248,.08); border:1px solid rgba(56,189,248,.2); border-radius:.35rem; padding:.15em .45em; font-size:.85em; color:#38bdf8; }
|
||||
.prose-custom pre { background:#050a1a; border:1px solid rgba(255,255,255,.06); border-radius:.75rem; padding:1.25rem; overflow-x:auto; margin:1.5rem 0; }
|
||||
.prose-custom pre code { background:none; border:none; padding:0; color:#e2e8f0; }
|
||||
.prose-custom ul,.prose-custom ol { padding-inline-start:1.5rem; margin-bottom:1.25rem; }
|
||||
.prose-custom li { margin-bottom:.4rem; }
|
||||
.prose-custom blockquote { border-inline-start:3px solid #38bdf8; padding-inline-start:1rem; color:#94a3b8; margin:1.5rem 0; }
|
||||
.prose-custom strong { color:#e2e8f0; }
|
||||
.prose-custom a { color:#38bdf8; text-decoration:underline; text-underline-offset:.2em; }
|
||||
</style>
|
||||
}
|
||||
|
||||
+195
-373
@@ -5,239 +5,117 @@
|
||||
var locale = Model.Locale;
|
||||
}
|
||||
|
||||
<!-- ─── HERO ─────────────────────────────────────────────────────────── -->
|
||||
<section id="top" class="relative isolate overflow-hidden min-h-[100svh] pt-28 pb-20 sm:pt-32">
|
||||
<!-- Particle canvas -->
|
||||
<div class="pointer-events-none absolute inset-0 -z-10">
|
||||
<canvas id="particle-canvas" class="h-full w-full opacity-60"></canvas>
|
||||
<div aria-hidden class="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-b from-transparent to-base"></div>
|
||||
</div>
|
||||
<!-- ─── HERO (editorial, centered) ───────────────────────────────────── -->
|
||||
<section id="top" class="px-5 pt-28 pb-24 sm:px-8 sm:pt-32">
|
||||
<div class="mx-auto max-w-3xl text-center">
|
||||
<p class="kicker reveal">@(fa ? "مهندس هوش مصنوعی، مشاور، معمار راهکار" : "AI Engineer, Consultant, Solution Architect")</p>
|
||||
|
||||
<!-- Aurora background -->
|
||||
<div aria-hidden class="pointer-events-none absolute inset-0 -z-20"
|
||||
style="background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(56,189,248,.18),transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 80% 10%,rgba(232,121,249,.10),transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 10% 30%,rgba(129,140,248,.10),transparent 60%)"></div>
|
||||
|
||||
<div class="mx-auto flex max-w-7xl flex-col items-center px-5 text-center sm:px-8">
|
||||
|
||||
<!-- Availability chip -->
|
||||
<div class="mb-7 reveal">
|
||||
<span class="chip">
|
||||
<span class="relative inline-flex h-2 w-2">
|
||||
<span class="absolute inset-0 animate-pulse-dot rounded-full bg-emerald"></span>
|
||||
<span class="relative inline-block h-2 w-2 rounded-full bg-emerald"></span>
|
||||
</span>
|
||||
@(fa ? "پذیرش پروژههای منتخب فصل سوم ۲۰۲۶" : "Available for select Q3 2026 engagements")
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Eyebrow -->
|
||||
<p class="label-mono mb-6 inline-flex items-center gap-3 reveal" style="transition-delay:.08s">
|
||||
<span class="h-px w-10 bg-electric/60" aria-hidden></span>
|
||||
@(fa ? "مهندس هوش مصنوعی · مشاور · معمار راهکار" : "AI Engineer · Consultant · Solution Architect")
|
||||
<span class="h-px w-10 bg-electric/60" aria-hidden></span>
|
||||
</p>
|
||||
|
||||
<!-- Name -->
|
||||
<h1 class="font-display text-balance font-extrabold leading-[1.02] tracking-tight text-white reveal @(fa ? "font-fa" : "")"
|
||||
style="font-size:clamp(2.4rem,7vw,5.4rem);transition-delay:.15s">
|
||||
<h1 class="reveal mt-6 @(fa ? "font-fa" : "")" style="font-size:clamp(2.6rem,7vw,4.75rem);transition-delay:.05s">
|
||||
@(fa ? "سروش اسعدی" : "Soroush Asadi")
|
||||
</h1>
|
||||
|
||||
<!-- Headline -->
|
||||
<p class="mt-5 max-w-4xl text-balance font-medium leading-[1.25] text-slate-200 reveal"
|
||||
style="font-size:clamp(1.15rem,2.2vw,1.75rem);transition-delay:.25s">
|
||||
@(fa ? "طراحی سامانههای" : "Architecting")
|
||||
<span class="gradient-text font-semibold">@(fa ? "هوش مصنوعی" : "production-grade AI")</span>
|
||||
@(fa ? "در مقیاس سازمانی." : "for the enterprise.")
|
||||
</p>
|
||||
|
||||
<!-- Typewriter -->
|
||||
<div class="mt-5 flex items-center gap-3 font-mono uppercase tracking-[.15em] text-slate-400 reveal"
|
||||
style="font-size:clamp(.9rem,1.4vw,1.05rem);transition-delay:.35s">
|
||||
<span class="h-px w-6 bg-slate-700" aria-hidden></span>
|
||||
<span id="typewriter"
|
||||
data-words='@(fa
|
||||
? "[\"راهبرد هوش مصنوعی\",\"مهندسی LLM و RAG\",\"معماری راهکار\",\"اتوماسیون عاملمحور\",\"استک گوگل کلود\"]"
|
||||
: "[\"AI Strategy\",\"LLM & RAG Engineering\",\"Solution Architecture\",\"Agentic Automation\",\"Google Cloud Stack\"]")'></span>
|
||||
<span class="inline-block w-px h-[1em] bg-electric animate-caret-blink" aria-hidden></span>
|
||||
<span class="h-px w-6 bg-slate-700" aria-hidden></span>
|
||||
</div>
|
||||
|
||||
<!-- Sub -->
|
||||
<p class="mt-7 max-w-2xl text-balance leading-relaxed text-slate-400 reveal"
|
||||
style="font-size:clamp(.95rem,1.4vw,1.08rem);transition-delay:.42s">
|
||||
<p class="lede reveal mx-auto mt-6 text-balance" style="font-size:clamp(1.05rem,2vw,1.3rem);transition-delay:.1s">
|
||||
@(fa
|
||||
? "از راهبرد تا تولید — ساخت پایپلاینهای LLM، عاملهای خودکار، و معماریهای ابری که در میلیونها رویداد در روز پایدار میمانند."
|
||||
: "From strategy to deployment — building LLM pipelines, autonomous agents, and cloud architectures that hold up at millions of events per day.")
|
||||
? "طراحی و استقرار سامانههای هوش مصنوعی در مقیاس سازمانی؛ از نخستین جلسهی راهبرد تا استقرار در تولید."
|
||||
: "I design and ship production-grade AI systems for the enterprise, from the first strategy session to live deployment.")
|
||||
</p>
|
||||
|
||||
<!-- CTAs -->
|
||||
<div class="mt-9 flex flex-wrap items-center justify-center gap-3 reveal" style="transition-delay:.5s">
|
||||
<a href="#contact" class="btn-primary">
|
||||
@(fa ? "رزرو جلسه مشاوره" : "Book a consultation")
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" class="@(fa ? "rotate-180" : "")" aria-hidden="true"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
||||
</a>
|
||||
<a href="#services" class="btn-ghost">@(fa ? "مشاهده خدمات" : "View services")</a>
|
||||
<div class="reveal mt-9 flex flex-wrap items-center justify-center gap-3" style="transition-delay:.15s">
|
||||
<a href="#contact" class="btn">@(fa ? "رزرو جلسه" : "Book a call")</a>
|
||||
<a href="#portfolio" class="btn-ghost">@(fa ? "نمونهکارها" : "View work")</a>
|
||||
</div>
|
||||
|
||||
<!-- Metrics -->
|
||||
<div class="mt-16 grid w-full max-w-4xl grid-cols-2 gap-4 sm:grid-cols-4 reveal" style="transition-delay:.6s">
|
||||
@{
|
||||
var metrics = fa
|
||||
? new[]{ ("۱۸+","مدل هوش مصنوعی مستقر","text-electric"), ("۴۰+","میکروسرویس تولید","text-violet"), ("۱۲ms","تأخیر استنتاج","text-magenta"), ("۹۹٪","پایداری SLA","text-emerald") }
|
||||
: new[]{ ("18+","AI models in production","text-electric"), ("40+","microservices shipped","text-violet"), ("12ms","inference latency","text-magenta"), ("99%","SLA uptime","text-emerald") };
|
||||
}
|
||||
@foreach (var (val, label, color) in metrics)
|
||||
{
|
||||
<div class="glass relative overflow-hidden px-5 py-5 text-start">
|
||||
<span aria-hidden class="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-electric/50 to-transparent"></span>
|
||||
<div class="font-display font-bold leading-none @color" style="font-size:clamp(1.6rem,3vw,2.25rem)">@val</div>
|
||||
<div class="mt-2 text-[.78rem] leading-snug text-slate-400">@label</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Scroll cue -->
|
||||
<a href="#services" class="mt-14 inline-flex flex-col items-center gap-2 text-slate-500 transition-colors hover:text-slate-200 reveal" style="transition-delay:.75s" aria-label="@(fa ? "اسکرول" : "Scroll")">
|
||||
<span class="label-mono">@(fa ? "اسکرول" : "Scroll")</span>
|
||||
<span class="relative block h-9 w-5 rounded-full border border-slate-700">
|
||||
<span class="absolute left-1/2 top-1.5 inline-block h-1.5 w-0.5 -translate-x-1/2 animate-float-y rounded-full bg-electric"></span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── SERVICES ─────────────────────────────────────────────────────── -->
|
||||
<section id="services" class="relative px-5 py-28 sm:px-8">
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<div class="section-header">
|
||||
<div class="eyebrow"><span class="chip">@(fa ? "خدمات" : "Services")</span></div>
|
||||
<h2>@(fa ? "شش حوزه تخصصی" : "Six areas of practice")</h2>
|
||||
<p>@(fa ? "از اولین جلسهی راهبرد تا استقرار تولید — یک شریک مهندسی برای کل چرخهی عمر هوش مصنوعی شما." : "From the first strategy session to production rollout — one engineering partner for the full AI lifecycle.")</p>
|
||||
<!-- ─── SERVICES (text-block grid) ───────────────────────────────────── -->
|
||||
<section id="services" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<div class="sec-head">
|
||||
<h2>@(fa ? "شش حوزهی تخصص" : "Six areas of practice")</h2>
|
||||
<p class="lede">@(fa ? "از نخستین جلسهی راهبرد تا استقرار تولید؛ یک شریک مهندسی برای کل چرخهی عمر هوش مصنوعی." : "From the first strategy session to production rollout, one engineering partner for the full AI lifecycle.")</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-10 gap-y-10 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@{
|
||||
var services = fa ? new[]{
|
||||
("strategy","راهبرد و نقشه راه هوش مصنوعی","ارزیابی بلوغ سازمانی، شناسایی موارد کاربری با بیشترین بازده، و طراحی نقشه راه ۱۲–۱۸ ماهه با KPIهای روشن.","electric",new[]{"Discovery","ROI Mapping","Roadmap"}),
|
||||
("automation","اتوماسیون هوش مصنوعی","ساخت عاملهای خودکار و گردشکارهای n8n که فرایندهای دستی را به سامانههای قابل ممیزی تبدیل میکنند.","violet",new[]{"n8n","Agents","Workflows"}),
|
||||
("llm-rag","مهندسی LLM و RAG","طراحی pipelineهای RAG با پایگاههای برداری، evaluation framework، و سرویسدهی با تأخیر زیر ۵۰ میلیثانیه.","magenta",new[]{"RAG","Vector DB","Eval"}),
|
||||
("architecture","معماری راهکار","طراحی سامانههای توزیعشده روی Kubernetes با میکروسرویسها، event streaming، و الگوهای پایداری در مقیاس بالا.","emerald",new[]{"K8s","Microservices","Event-Driven"}),
|
||||
("mobile","اپلیکیشنهای موبایل هوش مصنوعی","برنامههای Flutter، Swift و Kotlin با on-device inference، استریم LLM و تجربهی کاربری بومی.","electric",new[]{"Flutter","Swift","Kotlin"}),
|
||||
("google-stack","تخصص استک گوگل","استقرار روی Vertex AI، GKE و Gemini با بهینهسازی هزینه و الگوهای امنیتی سطح enterprise.","cyan",new[]{"Vertex AI","GKE","Gemini"}),
|
||||
("strategy","راهبرد و نقشه راه هوش مصنوعی","ارزیابی بلوغ سازمانی، شناسایی موارد کاربری با بیشترین بازده، و طراحی نقشه راه ۱۲ تا ۱۸ ماهه با KPIهای روشن.",new[]{"Discovery","ROI Mapping","Roadmap"}),
|
||||
("automation","اتوماسیون هوش مصنوعی","ساخت عاملهای خودکار و گردشکارهای n8n که فرایندهای دستی را به سامانههای قابل ممیزی تبدیل میکنند.",new[]{"n8n","Agents","Workflows"}),
|
||||
("llm-rag","مهندسی LLM و RAG","طراحی pipelineهای RAG با پایگاههای برداری، evaluation framework، و سرویسدهی با تأخیر زیر ۵۰ میلیثانیه.",new[]{"RAG","Vector DB","Eval"}),
|
||||
("architecture","معماری راهکار","طراحی سامانههای توزیعشده روی Kubernetes با میکروسرویسها، event streaming، و الگوهای پایداری در مقیاس بالا.",new[]{"K8s","Microservices","Event-Driven"}),
|
||||
("mobile","اپلیکیشنهای موبایل هوش مصنوعی","برنامههای Flutter، Swift و Kotlin با on-device inference، استریم LLM و تجربهی کاربری بومی.",new[]{"Flutter","Swift","Kotlin"}),
|
||||
("google-stack","تخصص استک گوگل","استقرار روی Vertex AI، GKE و Gemini با بهینهسازی هزینه و الگوهای امنیتی سطح enterprise.",new[]{"Vertex AI","GKE","Gemini"}),
|
||||
} : new[]{
|
||||
("strategy","AI Strategy & Roadmap","Maturity assessment, highest-ROI use-case discovery, and a 12–18 month roadmap with measurable KPIs.","electric",new[]{"Discovery","ROI Mapping","Roadmap"}),
|
||||
("automation","AI Automation","Autonomous agents and n8n workflows that turn manual processes into auditable, observable systems.","violet",new[]{"n8n","Agents","Workflows"}),
|
||||
("llm-rag","LLM & RAG Engineering","Production RAG pipelines with vector stores, evaluation frameworks, and sub-50ms serving.","magenta",new[]{"RAG","Vector DB","Eval"}),
|
||||
("architecture","Solution Architecture","Distributed systems on Kubernetes — microservices, event streaming, and resilience patterns at scale.","emerald",new[]{"K8s","Microservices","Event-Driven"}),
|
||||
("mobile","Mobile AI Apps","Flutter, Swift, and Kotlin apps with on-device inference, streaming LLM UX, and native polish.","electric",new[]{"Flutter","Swift","Kotlin"}),
|
||||
("google-stack","Google Stack Specialist","Vertex AI, GKE, and Gemini deployments with cost optimization and enterprise security patterns.","cyan",new[]{"Vertex AI","GKE","Gemini"}),
|
||||
("strategy","AI Strategy and Roadmap","Maturity assessment, highest-ROI use-case discovery, and a 12 to 18 month roadmap with measurable KPIs.",new[]{"Discovery","ROI Mapping","Roadmap"}),
|
||||
("automation","AI Automation","Autonomous agents and n8n workflows that turn manual processes into auditable, observable systems.",new[]{"n8n","Agents","Workflows"}),
|
||||
("llm-rag","LLM and RAG Engineering","Production RAG pipelines with vector stores, evaluation frameworks, and sub-50ms serving.",new[]{"RAG","Vector DB","Eval"}),
|
||||
("architecture","Solution Architecture","Distributed systems on Kubernetes: microservices, event streaming, and resilience patterns at scale.",new[]{"K8s","Microservices","Event-Driven"}),
|
||||
("mobile","Mobile AI Apps","Flutter, Swift, and Kotlin apps with on-device inference, streaming LLM UX, and native polish.",new[]{"Flutter","Swift","Kotlin"}),
|
||||
("google-stack","Google Stack Specialist","Vertex AI, GKE, and Gemini deployments with cost optimization and enterprise security patterns.",new[]{"Vertex AI","GKE","Gemini"}),
|
||||
};
|
||||
int si = 0;
|
||||
}
|
||||
@foreach (var (id, title, desc, color, tags) in services)
|
||||
@foreach (var (id, title, desc, tags) in services)
|
||||
{
|
||||
var (ringCls, glowCls, textCls, chipCls) = color switch {
|
||||
"violet" => ("group-hover:border-violet/50", "group-hover:shadow-glow-violet", "text-violet", "border-violet/30 bg-violet/5 text-violet/90"),
|
||||
"magenta" => ("group-hover:border-magenta/50", "group-hover:shadow-glow-magenta", "text-magenta", "border-magenta/30 bg-magenta/5 text-magenta/90"),
|
||||
"emerald" => ("group-hover:border-emerald/50", "group-hover:shadow-glow-emerald", "text-emerald", "border-emerald/30 bg-emerald/5 text-emerald/90"),
|
||||
"cyan" => ("group-hover:border-cyan/50", "group-hover:shadow-glow-electric","text-cyan", "border-cyan/30 bg-cyan/5 text-cyan/90"),
|
||||
_ => ("group-hover:border-electric/50","group-hover:shadow-glow-electric","text-electric","border-electric/30 bg-electric/5 text-electric/90"),
|
||||
};
|
||||
<article class="group relative isolate overflow-hidden p-6 sm:p-7 glass service-card reveal @ringCls @glowCls" style="transition-delay:@(si * 50)ms">
|
||||
<div class="flex items-start justify-between">
|
||||
<span class="label-mono">@((si + 1).ToString("D2"))</span>
|
||||
<span class="@textCls">
|
||||
@Html.Raw(ServiceIcon(id))
|
||||
</span>
|
||||
<article class="reveal border-t border-zinc-200 pt-6">
|
||||
<span class="text-zinc-400" aria-hidden="true">@Html.Raw(ServiceIcon(id))</span>
|
||||
<h3 class="mt-5 text-lg font-semibold @(fa ? "font-fa" : "")">@title</h3>
|
||||
<p class="mt-2.5 text-[.95rem] leading-relaxed text-zinc-600">@desc</p>
|
||||
<div class="mt-4 flex flex-wrap gap-1.5">
|
||||
@foreach (var tag in tags) { <span class="chip">@tag</span> }
|
||||
</div>
|
||||
<h3 class="mt-6 font-display font-semibold leading-snug text-white @(fa ? "font-fa" : "")" style="font-size:clamp(1.15rem,1.8vw,1.4rem)">@title</h3>
|
||||
<p class="mt-3 text-[.94rem] leading-relaxed text-slate-400">@desc</p>
|
||||
<div class="mt-5 flex flex-wrap gap-1.5">
|
||||
@foreach (var tag in tags)
|
||||
{
|
||||
<span class="rounded-full border px-2.5 py-0.5 font-mono text-[.65rem] uppercase tracking-wider @chipCls">@tag</span>
|
||||
}
|
||||
</div>
|
||||
<span aria-hidden class="absolute inset-x-6 bottom-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"></span>
|
||||
</article>
|
||||
si++;
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── DATA FLOW ────────────────────────────────────────────────────── -->
|
||||
<section id="dataflow" class="relative overflow-hidden px-5 py-28 sm:px-8">
|
||||
<div class="mx-auto max-w-5xl">
|
||||
<div class="section-header">
|
||||
<div class="eyebrow"><span class="chip">@(fa ? "پایپلاین" : "Pipeline")</span></div>
|
||||
<h2>@(fa ? "از سند خام تا پاسخ قابل اتکا" : "From raw document to trustworthy answer")</h2>
|
||||
<p>@(fa ? "مسیری که هر پرسش در یک سامانهی RAG تولیدی طی میکند — هر مرحله قابل اندازهگیری، قابل ممیزی و بهینهشده برای تأخیر." : "The path every query takes through a production RAG system — each stage measurable, auditable, and tuned for latency.")</p>
|
||||
<!-- ─── PIPELINE (horizontal stepper) ────────────────────────────────── -->
|
||||
<section id="dataflow" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<div class="sec-head">
|
||||
<h2>@(fa ? "از سند خام تا پاسخ قابل اتکا" : "From raw document to a trustworthy answer")</h2>
|
||||
<p class="lede">@(fa ? "مسیری که هر پرسش در یک سامانهی RAG تولیدی طی میکند. هر مرحله قابل اندازهگیری، قابل ممیزی و بهینهشده برای تأخیر." : "The path every query takes through a production RAG system. Each stage is measurable, auditable, and tuned for latency.")</p>
|
||||
</div>
|
||||
<!-- Flow diagram -->
|
||||
<div class="reveal mt-10 overflow-x-auto">
|
||||
<div class="flex min-w-max items-center gap-0 mx-auto w-fit">
|
||||
|
||||
<ol class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-3 lg:grid-cols-5">
|
||||
@{
|
||||
var nodes = fa ? new[]{
|
||||
("ingest","دریافت","نرمالسازی، قطعهبندی و پاکسازی اسناد منبع","electric"),
|
||||
("embed","برداریسازی","تولید embedding و نمایهسازی در پایگاه برداری","violet"),
|
||||
("retrieve","بازیابی","جستجوی ترکیبی معنایی و کلیدواژهای","cyan"),
|
||||
("rerank","بازرتبهبندی","مرتبسازی مجدد نامزدها با cross-encoder","magenta"),
|
||||
("generate","تولید","پاسخ مستند با ارجاع به منبع","emerald"),
|
||||
("دریافت","نرمالسازی، قطعهبندی و پاکسازی اسناد منبع"),
|
||||
("برداریسازی","تولید embedding و نمایهسازی در پایگاه برداری"),
|
||||
("بازیابی","جستجوی ترکیبی معنایی و کلیدواژهای"),
|
||||
("بازرتبهبندی","مرتبسازی مجدد نامزدها با cross-encoder"),
|
||||
("تولید","پاسخ مستند با ارجاع به منبع"),
|
||||
} : new[]{
|
||||
("ingest","Ingest","Normalize, chunk, and clean source documents","electric"),
|
||||
("embed","Embed","Generate embeddings and index in vector store","violet"),
|
||||
("retrieve","Retrieve","Hybrid semantic + keyword search","cyan"),
|
||||
("rerank","Rerank","Re-order candidates with a cross-encoder","magenta"),
|
||||
("generate","Generate","Grounded answer with source citations","emerald"),
|
||||
};
|
||||
var colorMap2 = new Dictionary<string,(string border, string text, string bg)>{
|
||||
["electric"] = ("border-electric/40","text-electric","bg-electric/10"),
|
||||
["violet"] = ("border-violet/40", "text-violet", "bg-violet/10"),
|
||||
["cyan"] = ("border-cyan/40", "text-cyan", "bg-cyan/10"),
|
||||
["magenta"] = ("border-magenta/40", "text-magenta", "bg-magenta/10"),
|
||||
["emerald"] = ("border-emerald/40", "text-emerald", "bg-emerald/10"),
|
||||
("Ingest","Normalize, chunk, and clean source documents"),
|
||||
("Embed","Generate embeddings and index in the vector store"),
|
||||
("Retrieve","Hybrid semantic and keyword search"),
|
||||
("Rerank","Re-order candidates with a cross-encoder"),
|
||||
("Generate","Grounded answer with source citations"),
|
||||
};
|
||||
int stepN = 0;
|
||||
}
|
||||
@for (int ni = 0; ni < nodes.Length; ni++)
|
||||
@foreach (var (nlabel, ndesc) in nodes)
|
||||
{
|
||||
var (nid, nlabel, ndesc, naccent) = nodes[ni];
|
||||
var (nborder, ntext, nbg) = colorMap2[naccent];
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="glass @nborder @nbg flex flex-col items-center gap-2 rounded-2xl border p-5 text-center w-40">
|
||||
<span class="label-mono @ntext">@nlabel</span>
|
||||
<p class="text-[.72rem] leading-snug text-slate-400">@ndesc</p>
|
||||
</div>
|
||||
</div>
|
||||
if (ni < nodes.Length - 1)
|
||||
{
|
||||
<div class="flex items-center px-2">
|
||||
<svg width="40" height="16" viewBox="0 0 40 16" fill="none" aria-hidden="true">
|
||||
<line x1="0" y1="8" x2="32" y2="8" stroke="rgba(56,189,248,0.4)" stroke-width="1.5" stroke-dasharray="4 3">
|
||||
<animate attributeName="stroke-dashoffset" values="0;-14" dur="1.1s" repeatCount="indefinite"/>
|
||||
</line>
|
||||
<polygon points="32,4 40,8 32,12" fill="rgba(56,189,248,0.6)"/>
|
||||
</svg>
|
||||
</div>
|
||||
stepN++;
|
||||
<li class="reveal border-t border-zinc-200 pt-4" style="transition-delay:@((stepN-1) * 40)ms">
|
||||
<span class="font-display text-sm text-zinc-400">@stepN.ToString("D2")</span>
|
||||
<h3 class="mt-2 text-base font-semibold @(fa ? "font-fa" : "")">@nlabel</h3>
|
||||
<p class="mt-1.5 text-[.85rem] leading-relaxed text-zinc-600">@ndesc</p>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-8 text-center label-mono text-slate-500 reveal">@(fa ? "تأخیر سرتاسری زیر ۵۰ میلیثانیه · هر مرحله مشاهدهپذیر" : "Sub-50ms end-to-end · every stage observable")</p>
|
||||
</ol>
|
||||
<p class="mt-8 text-sm text-zinc-500">@(fa ? "تأخیر سرتاسری زیر ۵۰ میلیثانیه؛ هر مرحله مشاهدهپذیر." : "Sub-50ms end-to-end, every stage observable.")</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── STACK ─────────────────────────────────────────────────────────── -->
|
||||
<section id="stack" class="relative px-5 py-28 sm:px-8">
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<div class="section-header">
|
||||
<div class="eyebrow"><span class="chip">@(fa ? "استک" : "Stack")</span></div>
|
||||
<h2>@(fa ? "ابزارهای روزانه" : "Daily tooling")</h2>
|
||||
<p>@(fa ? "هر چه ساخته میشود از این پایهها بیرون میآید — انتخابشده برای عمر طولانی، نه ترند روز." : "Everything I ship sits on this foundation — chosen for longevity, not hype cycles.")</p>
|
||||
<!-- ─── STACK (grouped tag clusters) ─────────────────────────────────── -->
|
||||
<section id="stack" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<div class="sec-head">
|
||||
<h2>@(fa ? "ابزار روزمره" : "Daily tooling")</h2>
|
||||
<p class="lede">@(fa ? "هر چه میسازم بر این پایهها استوار است؛ انتخابشده برای دوام، نه چرخههای هیجان." : "Everything I ship sits on this foundation, chosen for longevity, not hype cycles.")</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4 reveal">
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-8 gap-y-9 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@{
|
||||
var cats = fa ? new[]{
|
||||
("زبانها", new[]{"Python","TypeScript","Go","Rust","SQL"}),
|
||||
@@ -250,201 +128,147 @@
|
||||
("Infrastructure",new[]{"Kubernetes","Terraform","Postgres","Redis","Kafka","NATS"}),
|
||||
("AI / ML", new[]{"Vertex AI","Gemini","OpenAI","Anthropic","LangGraph","Pinecone","pgvector"}),
|
||||
};
|
||||
string[] catColors = ["text-electric","text-violet","text-emerald","text-magenta"];
|
||||
int ci2 = 0;
|
||||
}
|
||||
@foreach (var (catLabel, items) in cats)
|
||||
{
|
||||
<div class="glass p-6">
|
||||
<h3 class="font-display font-semibold text-white mb-4 @catColors[ci2]">@catLabel</h3>
|
||||
<ul class="space-y-2">
|
||||
@foreach (var item in items)
|
||||
{
|
||||
<li class="flex items-center gap-2 text-[.9rem] text-slate-300">
|
||||
<span class="h-1 w-1 rounded-full @catColors[ci2].Replace("text-","bg-")" aria-hidden></span>
|
||||
@item
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="reveal border-t border-zinc-200 pt-5">
|
||||
<h3 class="mb-4 text-sm font-semibold @(fa ? "font-fa" : "")">@catLabel</h3>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach (var item in items) { <span class="chip">@item</span> }
|
||||
</div>
|
||||
</div>
|
||||
ci2++;
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── EXPERTISE ────────────────────────────────────────────────────── -->
|
||||
<section id="expertise" class="relative px-5 py-28 sm:px-8">
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<div class="section-header">
|
||||
<div class="eyebrow"><span class="chip">@(fa ? "تخصص" : "Expertise")</span></div>
|
||||
<h2>@(fa ? "اعدادی که اهمیت دارند" : "The numbers that matter")</h2>
|
||||
<p>@(fa ? "سامانههایی که در میلیونها رویداد در روز پایدار میمانند — اینها معیارهایی هستند که اندازه میگیریم." : "Systems that survive millions of events per day — these are the metrics I optimize for.")</p>
|
||||
<!-- ─── EXPERTISE (definition list) ──────────────────────────────────── -->
|
||||
<section id="expertise" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="sec-head">
|
||||
<h2>@(fa ? "آنچه در آن عمیق میشوم" : "What I go deep on")</h2>
|
||||
<p class="lede">@(fa ? "سامانههایی که میلیونها رویداد در روز را دوام میآورند. اینها حوزههاییاند که برایشان بهینه میکنم." : "Systems that survive millions of events per day. These are the areas I optimize for.")</p>
|
||||
</div>
|
||||
<div class="space-y-6 reveal">
|
||||
|
||||
<dl>
|
||||
@{
|
||||
var bars = fa ? new[]{
|
||||
("مهندسی LLM و RAG", 95),
|
||||
("معماری ابری و Kubernetes", 92),
|
||||
("سیستمهای عاملمحور و اتوماسیون", 90),
|
||||
("استک گوگل کلود (Vertex / GKE)", 88),
|
||||
("موبایل بومی و cross-platform", 82),
|
||||
var areas = fa ? new[]{
|
||||
("مهندسی LLM و RAG","پایپلاینهای بازیابی، ارزیابی و تولید مستند در محیط تولید."),
|
||||
("معماری ابری و Kubernetes","سرویسهای توزیعشده، مقیاس خودکار و پایداری در مقیاس بالا."),
|
||||
("سیستمهای عاملمحور و اتوماسیون","گردشکارهای خودکار قابل ممیزی با n8n و LangGraph."),
|
||||
("استک گوگل کلود (Vertex / GKE)","Vertex AI، GKE و Gemini با انضباط هزینه."),
|
||||
("موبایل بومی و cross-platform","Flutter، Swift و Kotlin با استنتاج روی دستگاه."),
|
||||
} : new[]{
|
||||
("LLM & RAG engineering", 95),
|
||||
("Cloud architecture & Kubernetes", 92),
|
||||
("Agentic systems & automation", 90),
|
||||
("Google Cloud stack (Vertex / GKE)", 88),
|
||||
("Native + cross-platform mobile", 82),
|
||||
("LLM and RAG engineering","Retrieval pipelines, evals, and grounded generation in production."),
|
||||
("Cloud architecture and Kubernetes","Distributed services, autoscaling, and resilience at scale."),
|
||||
("Agentic systems and automation","Auditable autonomous workflows with n8n and LangGraph."),
|
||||
("Google Cloud stack (Vertex / GKE)","Vertex AI, GKE, and Gemini with real cost discipline."),
|
||||
("Native and cross-platform mobile","Flutter, Swift, and Kotlin with on-device inference."),
|
||||
};
|
||||
string[] barColors = ["bg-electric","bg-violet","bg-cyan","bg-magenta","bg-emerald"];
|
||||
int bi = 0;
|
||||
}
|
||||
@foreach (var (blabel, bval) in bars)
|
||||
@foreach (var (alabel, adesc) in areas)
|
||||
{
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-[.9rem] text-slate-300">@blabel</span>
|
||||
<span class="label-mono text-slate-400">@bval%</span>
|
||||
<div class="reveal grid grid-cols-1 gap-1 border-t border-zinc-200 py-5 sm:grid-cols-[1fr_1.5fr] sm:gap-8">
|
||||
<dt class="text-base font-semibold @(fa ? "font-fa" : "")">@alabel</dt>
|
||||
<dd class="text-[.95rem] leading-relaxed text-zinc-600">@adesc</dd>
|
||||
</div>
|
||||
<div class="bar-track">
|
||||
<div class="bar-fill @barColors[bi]" data-w="@bval%" style="width:0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
bi++;
|
||||
}
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── PORTFOLIO ────────────────────────────────────────────────────── -->
|
||||
<section id="portfolio" class="relative px-5 py-28 sm:px-8">
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<div class="section-header">
|
||||
<div class="eyebrow"><span class="chip">@(fa ? "نمونهکارها" : "Selected work")</span></div>
|
||||
<h2>@(fa ? "سامانههایی که در تولید کار میکنند" : "Systems that run in production")</h2>
|
||||
<p>@(fa ? "گزیدهای از پروژههای واقعی. روی هر کارت بزنید تا جزئیات معماری را ببینید." : "A selection of real engagements. Tap any card for the gallery and architecture details.")</p>
|
||||
<!-- ─── PORTFOLIO (card grid, typographic covers) ────────────────────── -->
|
||||
<section id="portfolio" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<div class="sec-head">
|
||||
<h2>@(fa ? "نمونهکارهای منتخب" : "Selected work")</h2>
|
||||
<p class="lede">@(fa ? "گزیدهای از پروژههای واقعی در حوزهی هوش مصنوعی، داده و موبایل." : "A selection of real engagements across AI, data, and mobile.")</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@{
|
||||
var projects = fa ? new[]{
|
||||
("atlas-rag","اطلس — پلتفرم RAG سازمانی","بانک ردیفاول","مهندس ارشد هوش مصنوعی","۲۰۲۵","دستیار دانش روی بیش از ۴ میلیون سند داخلی؛ بازیابی ترکیبی با pgvector و reranker.","electric",new[]{"RAG","pgvector","Vertex AI","Eval"},new[]{("۴M+","سند نمایهشده"),("۳۸ms","تأخیر p95"),("۹۲٪","دقت پاسخ")},"/portfolio/atlas-rag/cover.svg",new[]{"/portfolio/atlas-rag/01.svg","/portfolio/atlas-rag/02.svg","/portfolio/atlas-rag/03.svg"}),
|
||||
("sentinel-agents","Sentinel — اتوماسیون Ops عاملمحور","SaaS scale-up","معمار راهکار","۲۰۲۵","پاسخ خودکار به حوادث با ترکیب n8n و LangGraph — عاملهای قابل ممیزی که alert تریاژ میکنند.","violet",new[]{"n8n","LangGraph","Agents"},new[]{("۷۰٪","کاهش MTTR"),("۲۴/۷","پوشش on-call"),("۱۵۰+","جریان خودکار")},"/portfolio/sentinel-agents/cover.svg",new[]{"/portfolio/sentinel-agents/01.svg","/portfolio/sentinel-agents/02.svg","/portfolio/sentinel-agents/03.svg"}),
|
||||
("vertex-vision","Vertex Vision — استنتاج بینایی بلادرنگ","زنجیره خردهفروشی","مهندس هوش مصنوعی","۲۰۲۴","استنتاج بینایی بلادرنگ روی GKE با Triton و Vertex AI برای تحلیل قفسه و جریان مشتری.","cyan",new[]{"Vertex AI","GKE","Triton"},new[]{("۱.۲B","استنتاج ماهانه"),("۳۰۰+","فروشگاه"),("۶۰٪","کاهش هزینه GPU")},"/portfolio/vertex-vision/cover.svg",new[]{"/portfolio/vertex-vision/01.svg","/portfolio/vertex-vision/02.svg","/portfolio/vertex-vision/03.svg"}),
|
||||
("mirage-mobile","Mirage — مجموعه هوش مصنوعی on-device","محصول مصرفی","رهبر موبایل + هوش مصنوعی","۲۰۲۴","اپلیکیشن Flutter با استنتاج کاملاً آفلاین با Gemini Nano و LiteRT.","magenta",new[]{"Flutter","Gemini Nano","LiteRT"},new[]{("۰","وابستگی شبکه"),("<80ms","پاسخ"),("۴.۸★","امتیاز کاربران")},"/portfolio/mirage-mobile/cover.svg",new[]{"/portfolio/mirage-mobile/01.svg","/portfolio/mirage-mobile/02.svg","/portfolio/mirage-mobile/03.svg"}),
|
||||
("flux-stream","Flux — مش داده رویدادمحور","پلتفرم لجستیک","معمار پلتفرم","۲۰۲۳","ستون استریمینگ روی Kafka و NATS روی Kubernetes — ۴۰+ میکروسرویس با الگوهای پایداری.","emerald",new[]{"Kafka","NATS","Kubernetes","Go"},new[]{("۴۰+","میکروسرویس"),("۲M/s","رویداد در ثانیه"),("۹۹.۹٪","uptime")},"/portfolio/flux-stream/cover.svg",new[]{"/portfolio/flux-stream/01.svg","/portfolio/flux-stream/02.svg","/portfolio/flux-stream/03.svg"}),
|
||||
("oracle-forecast","Oracle — موتور پیشبینی تقاضا","زنجیره تامین","مهندس ML","۲۰۲۳","پایپلاین پیشبینی سری زمانی روی BigQuery و dbt با بازآموزی خودکار.","electric",new[]{"Forecasting","BigQuery","dbt","MLOps"},new[]{("۲۳٪","کاهش ضایعات"),("۸۹٪","دقت پیشبینی"),("روزانه","بازآموزی")},"/portfolio/oracle-forecast/cover.svg",new[]{"/portfolio/oracle-forecast/01.svg","/portfolio/oracle-forecast/02.svg","/portfolio/oracle-forecast/03.svg"}),
|
||||
("atlas-rag","اطلس - پلتفرم RAG سازمانی","بانک ردیفاول","۲۰۲۵","دستیار دانش روی بیش از ۴ میلیون سند داخلی؛ بازیابی ترکیبی با pgvector و reranker.",new[]{"RAG","pgvector","Vertex AI"},new[]{("۴M+","سند نمایهشده"),("۳۸ms","تأخیر p95"),("۹۲٪","دقت پاسخ")}),
|
||||
("sentinel-agents","Sentinel - اتوماسیون Ops عاملمحور","SaaS scale-up","۲۰۲۵","پاسخ خودکار به حوادث با ترکیب n8n و LangGraph؛ عاملهای قابل ممیزی که alert تریاژ میکنند.",new[]{"n8n","LangGraph","Agents"},new[]{("۷۰٪","کاهش MTTR"),("۲۴/۷","پوشش on-call"),("۱۵۰+","جریان خودکار")}),
|
||||
("vertex-vision","Vertex Vision - استنتاج بینایی بلادرنگ","زنجیره خردهفروشی","۲۰۲۴","استنتاج بینایی بلادرنگ روی GKE با Triton و Vertex AI برای تحلیل قفسه و جریان مشتری.",new[]{"Vertex AI","GKE","Triton"},new[]{("۱.۲B","استنتاج ماهانه"),("۳۰۰+","فروشگاه"),("۶۰٪","کاهش هزینه")}),
|
||||
("mirage-mobile","Mirage - مجموعه هوش مصنوعی on-device","محصول مصرفی","۲۰۲۴","اپلیکیشن Flutter با استنتاج کاملاً آفلاین با Gemini Nano و LiteRT.",new[]{"Flutter","Gemini Nano","LiteRT"},new[]{("۰","وابستگی شبکه"),("<80ms","پاسخ"),("۴.۸★","امتیاز کاربران")}),
|
||||
("flux-stream","Flux - مش داده رویدادمحور","پلتفرم لجستیک","۲۰۲۳","ستون استریمینگ روی Kafka و NATS روی Kubernetes؛ ۴۰+ میکروسرویس با الگوهای پایداری.",new[]{"Kafka","NATS","Go"},new[]{("۴۰+","میکروسرویس"),("۲M/s","رویداد بر ثانیه"),("۹۹.۹٪","uptime")}),
|
||||
("oracle-forecast","Oracle - موتور پیشبینی تقاضا","زنجیره تامین","۲۰۲۳","پایپلاین پیشبینی سری زمانی روی BigQuery و dbt با بازآموزی خودکار.",new[]{"BigQuery","dbt","MLOps"},new[]{("۲۳٪","کاهش ضایعات"),("۸۹٪","دقت پیشبینی"),("روزانه","بازآموزی")}),
|
||||
} : new[]{
|
||||
("atlas-rag","Atlas — Enterprise RAG Platform","Tier-1 bank","Lead AI Engineer","2025","A knowledge assistant over 4M+ internal documents — hybrid retrieval with pgvector and a reranker, sub-40ms serving on Vertex AI.","electric",new[]{"RAG","pgvector","Vertex AI","Eval"},new[]{("4M+","docs indexed"),("38ms","p95 latency"),("92%","answer accuracy")},"/portfolio/atlas-rag/cover.svg",new[]{"/portfolio/atlas-rag/01.svg","/portfolio/atlas-rag/02.svg","/portfolio/atlas-rag/03.svg"}),
|
||||
("sentinel-agents","Sentinel — Agentic Ops Automation","SaaS scale-up","Solution Architect","2025","Autonomous incident response combining n8n and LangGraph — auditable agents that triage alerts and self-heal.","violet",new[]{"n8n","LangGraph","Agents"},new[]{("70%","MTTR reduction"),("24/7","on-call coverage"),("150+","automated flows")},"/portfolio/sentinel-agents/cover.svg",new[]{"/portfolio/sentinel-agents/01.svg","/portfolio/sentinel-agents/02.svg","/portfolio/sentinel-agents/03.svg"}),
|
||||
("vertex-vision","Vertex Vision — Realtime Vision Inference","Retail chain","AI Engineer","2024","Real-time vision inference on GKE with Triton and Vertex AI for shelf analytics and customer flow across 300+ stores.","cyan",new[]{"Vertex AI","GKE","Triton"},new[]{("1.2B","inferences / mo"),("300+","stores"),("60%","GPU cost cut")},"/portfolio/vertex-vision/cover.svg",new[]{"/portfolio/vertex-vision/01.svg","/portfolio/vertex-vision/02.svg","/portfolio/vertex-vision/03.svg"}),
|
||||
("mirage-mobile","Mirage — On-device AI Suite","Consumer product","Mobile + AI Lead","2024","A Flutter app with fully offline inference via Gemini Nano and LiteRT — streaming response UX with zero network dependency.","magenta",new[]{"Flutter","Gemini Nano","LiteRT"},new[]{("0","network deps"),("<80ms","response"),("4.8★","user rating")},"/portfolio/mirage-mobile/cover.svg",new[]{"/portfolio/mirage-mobile/01.svg","/portfolio/mirage-mobile/02.svg","/portfolio/mirage-mobile/03.svg"}),
|
||||
("flux-stream","Flux — Event-Driven Data Mesh","Logistics platform","Platform Architect","2023","Streaming backbone on Kafka and NATS over Kubernetes — 40+ microservices with resilience patterns and exactly-once delivery.","emerald",new[]{"Kafka","NATS","Kubernetes","Go"},new[]{("40+","microservices"),("2M/s","events / sec"),("99.9%","uptime")},"/portfolio/flux-stream/cover.svg",new[]{"/portfolio/flux-stream/01.svg","/portfolio/flux-stream/02.svg","/portfolio/flux-stream/03.svg"}),
|
||||
("oracle-forecast","Oracle — Demand Forecasting Engine","Supply chain","ML Engineer","2023","Time-series forecasting pipeline on BigQuery and dbt with automated retraining — reduced inventory waste significantly.","electric",new[]{"Forecasting","BigQuery","dbt","MLOps"},new[]{("23%","waste reduction"),("89%","forecast accuracy"),("daily","retraining")},"/portfolio/oracle-forecast/cover.svg",new[]{"/portfolio/oracle-forecast/01.svg","/portfolio/oracle-forecast/02.svg","/portfolio/oracle-forecast/03.svg"}),
|
||||
("atlas-rag","Atlas - Enterprise RAG Platform","Tier-1 bank","2025","A knowledge assistant over 4M+ internal documents. Hybrid retrieval with pgvector and a reranker, sub-40ms serving.",new[]{"RAG","pgvector","Vertex AI"},new[]{("4M+","docs indexed"),("38ms","p95 latency"),("92%","answer accuracy")}),
|
||||
("sentinel-agents","Sentinel - Agentic Ops Automation","SaaS scale-up","2025","Autonomous incident response combining n8n and LangGraph. Auditable agents that triage alerts and self-heal.",new[]{"n8n","LangGraph","Agents"},new[]{("70%","MTTR cut"),("24/7","on-call cover"),("150+","automated flows")}),
|
||||
("vertex-vision","Vertex Vision - Realtime Vision Inference","Retail chain","2024","Real-time vision inference on GKE with Triton and Vertex AI for shelf analytics and customer flow across 300+ stores.",new[]{"Vertex AI","GKE","Triton"},new[]{("1.2B","inferences / mo"),("300+","stores"),("60%","GPU cost cut")}),
|
||||
("mirage-mobile","Mirage - On-device AI Suite","Consumer product","2024","A Flutter app with fully offline inference via Gemini Nano and LiteRT. Streaming response UX with zero network dependency.",new[]{"Flutter","Gemini Nano","LiteRT"},new[]{("0","network deps"),("<80ms","response"),("4.8★","user rating")}),
|
||||
("flux-stream","Flux - Event-Driven Data Mesh","Logistics platform","2023","Streaming backbone on Kafka and NATS over Kubernetes. 40+ microservices with resilience and exactly-once delivery.",new[]{"Kafka","NATS","Go"},new[]{("40+","microservices"),("2M/s","events / sec"),("99.9%","uptime")}),
|
||||
("oracle-forecast","Oracle - Demand Forecasting Engine","Supply chain","2023","Time-series forecasting pipeline on BigQuery and dbt with automated retraining, reducing inventory waste significantly.",new[]{"BigQuery","dbt","MLOps"},new[]{("23%","waste cut"),("89%","forecast accuracy"),("daily","retraining")}),
|
||||
};
|
||||
}
|
||||
@foreach (var (pid, ptitle, pclient, prole, pyear, psummary, paccent, ptags, pmetrics, pcover, pgallery) in projects)
|
||||
@foreach (var (pid, ptitle, pclient, pyear, psummary, ptags, pmetrics) in projects)
|
||||
{
|
||||
var (pborder, ptext) = paccent switch {
|
||||
"violet" => ("border-violet/30", "text-violet"),
|
||||
"cyan" => ("border-cyan/30", "text-cyan"),
|
||||
"magenta" => ("border-magenta/30", "text-magenta"),
|
||||
"emerald" => ("border-emerald/30", "text-emerald"),
|
||||
_ => ("border-electric/30", "text-electric"),
|
||||
};
|
||||
var galleryJson = System.Text.Json.JsonSerializer.Serialize(pgallery);
|
||||
<div class="glass cursor-pointer select-none p-6 transition-all duration-300 hover:-translate-y-1 reveal @pborder"
|
||||
data-portfolio-card
|
||||
tabindex="0"
|
||||
data-title="@ptitle"
|
||||
data-summary="@psummary"
|
||||
data-gallery="@galleryJson"
|
||||
role="button"
|
||||
aria-label="@ptitle">
|
||||
<div class="mb-4 aspect-video w-full overflow-hidden rounded-xl bg-base-700">
|
||||
<img src="@pcover" alt="@ptitle" class="h-full w-full object-cover" loading="lazy" />
|
||||
var initial = char.ToUpperInvariant(pid[0]);
|
||||
<article class="card card-link reveal overflow-hidden">
|
||||
<div class="flex aspect-[16/9] items-center justify-center bg-zinc-100" aria-hidden="true">
|
||||
<span class="font-display text-5xl font-bold text-zinc-300">@initial</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="mb-3 flex flex-wrap gap-1.5">
|
||||
@foreach (var tag in ptags)
|
||||
{
|
||||
<span class="rounded-full border @pborder px-2 py-0.5 font-mono text-[.62rem] uppercase tracking-wider @ptext/80">@tag</span>
|
||||
}
|
||||
@foreach (var tag in ptags) { <span class="chip">@tag</span> }
|
||||
</div>
|
||||
<h3 class="font-display font-semibold text-white @(fa ? "font-fa" : "")" style="font-size:clamp(1rem,1.5vw,1.2rem)">@ptitle</h3>
|
||||
<p class="mt-1 text-[.82rem] text-slate-400">@pclient · @pyear</p>
|
||||
<div class="mt-4 grid grid-cols-3 gap-3 border-t border-white/5 pt-4">
|
||||
<h3 class="text-[1.05rem] font-semibold @(fa ? "font-fa" : "")">@ptitle</h3>
|
||||
<p class="mt-1 text-[.8rem] text-zinc-500">@pclient · @pyear</p>
|
||||
<p class="mt-3 text-[.88rem] leading-relaxed text-zinc-600">@psummary</p>
|
||||
<div class="mt-4 grid grid-cols-3 gap-3 border-t border-zinc-200 pt-4">
|
||||
@foreach (var (mv, ml) in pmetrics)
|
||||
{
|
||||
<div class="text-center">
|
||||
<div class="font-display font-bold @ptext text-lg">@mv</div>
|
||||
<div class="text-[.68rem] text-slate-500">@ml</div>
|
||||
<div>
|
||||
<div class="font-display text-base font-bold text-zinc-900">@mv</div>
|
||||
<div class="mt-0.5 text-[.68rem] leading-tight text-zinc-500">@ml</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Portfolio modal -->
|
||||
<div id="portfolio-modal" class="fixed inset-0 z-[100] hidden" role="dialog" aria-modal="true">
|
||||
<div id="modal-overlay" class="absolute inset-0 bg-black/80 backdrop-blur-sm"></div>
|
||||
<div class="relative z-10 flex h-full items-center justify-center p-4">
|
||||
<div class="glass w-full max-w-3xl max-h-[90vh] overflow-auto rounded-2xl p-6">
|
||||
<div class="mb-4 flex items-start justify-between gap-4">
|
||||
<h2 id="modal-title" class="font-display text-xl font-bold text-white"></h2>
|
||||
<button id="modal-close" class="text-slate-400 hover:text-white transition-colors p-1" aria-label="Close">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4 aspect-video overflow-hidden rounded-xl bg-base-700">
|
||||
<img id="modal-img" src="" alt="" class="h-full w-full object-contain" />
|
||||
</div>
|
||||
<div class="mb-4 flex justify-between gap-2">
|
||||
<button id="modal-prev" class="btn-ghost text-sm py-2 px-4 disabled:opacity-30">@(fa ? "قبلی" : "Previous")</button>
|
||||
<button id="modal-next" class="btn-ghost text-sm py-2 px-4 disabled:opacity-30">@(fa ? "بعدی" : "Next")</button>
|
||||
</div>
|
||||
<p id="modal-body" class="text-sm leading-relaxed text-slate-400"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── BLOG ──────────────────────────────────────────────────────────── -->
|
||||
<section id="blog" class="relative px-5 py-28 sm:px-8">
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<div class="section-header">
|
||||
<div class="eyebrow"><span class="chip">@(fa ? "بلاگ" : "Journal")</span></div>
|
||||
<!-- ─── BLOG (editorial list) ────────────────────────────────────────── -->
|
||||
<section id="blog" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="sec-head">
|
||||
<h2>@(fa ? "یادداشتهای مهندسی" : "Engineering notes")</h2>
|
||||
<p>@(fa ? "یافتهها از پروژههای واقعی — نه ترجمهی مقاله، نه فهرست hype." : "Findings from real engagements — not translated articles, not hype lists.")</p>
|
||||
<p class="lede">@(fa ? "یافتهها از پروژههای واقعی. نه ترجمهی مقاله، نه فهرست هیجان." : "Findings from real engagements. Not translated articles, not hype lists.")</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
<div class="border-b border-zinc-200">
|
||||
@{
|
||||
var posts = fa ? new[]{
|
||||
("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در تولید کار میکند","چرا BLEU و ROUGE برای RAG ناکافیاند، و معیارهایی که در پروژههای واقعی تصمیم میسازند.",8),
|
||||
("rag-eval-framework","LLM","چارچوب ارزیابی RAG که در تولید دوام میآورد","چرا BLEU و ROUGE برای RAG کافی نیستند، و معیارهایی که در پروژههای واقعی تصمیم میسازند.",8),
|
||||
("agentic-n8n-patterns","Automation","الگوهای عاملمحور با n8n برای سازمان","چگونه n8n را با LangGraph ترکیب کنیم تا گردشکارهای قابل ممیزی بسازیم.",11),
|
||||
("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه ضدالگو که در ۸۰٪ پروژههای Vertex میبینم، و چگونه ۶۰٪ هزینه را کاهش دادیم.",6),
|
||||
("k8s-llm-inference","Infra","استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ میلیثانیه","الگوی استقرار با KEDA، GPU sharing، و request hedging برای سرویسدهی پایدار.",14),
|
||||
("flutter-on-device-ai","Mobile","هوش مصنوعی on-device در Flutter","استفاده از Gemini Nano و LiteRT برای استنتاج آفلاین در اپلیکیشنهای موبایل.",9),
|
||||
("enterprise-ai-roadmap","Strategy","نقشه راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای CTOها میسازم — از کشف موارد کاربری تا اولین استقرار تولید.",7),
|
||||
("enterprise-ai-roadmap","Strategy","نقشه راه هوش مصنوعی سازمانی در ۹۰ روز","چارچوبی که برای CTOها میسازم؛ از کشف موارد کاربری تا اولین استقرار تولید.",7),
|
||||
} : new[]{
|
||||
("rag-eval-framework","LLM","A RAG evaluation framework that holds up in production","Why BLEU and ROUGE fall short for RAG, and the metrics that actually drive decisions in real projects.",8),
|
||||
("agentic-n8n-patterns","Automation","Agentic patterns with n8n for the enterprise","How to combine n8n with LangGraph to build auditable, debuggable autonomous workflows.",11),
|
||||
("vertex-cost-control","Google Stack","Vertex AI cost control at scale","Three anti-patterns I see in 80% of Vertex projects — and how we cut 60% of monthly spend.",6),
|
||||
("vertex-cost-control","Google Stack","Vertex AI cost control at scale","Three anti-patterns I see in 80% of Vertex projects, and how we cut 60% of monthly spend.",6),
|
||||
("k8s-llm-inference","Infra","Sub-50ms LLM inference on Kubernetes","Deployment pattern with KEDA, GPU sharing, and request hedging for stable serving.",14),
|
||||
("flutter-on-device-ai","Mobile","On-device AI in Flutter","Using Gemini Nano and LiteRT for offline inference inside mobile apps.",9),
|
||||
("enterprise-ai-roadmap","Strategy","A 90-day enterprise AI roadmap","The framework I build for CTOs — from use-case discovery to first production deployment.",7),
|
||||
("enterprise-ai-roadmap","Strategy","A 90-day enterprise AI roadmap","The framework I build for CTOs, from use-case discovery to first production deployment.",7),
|
||||
};
|
||||
}
|
||||
@foreach (var (slug, cat, btitle, excerpt, readTime) in posts)
|
||||
{
|
||||
<a href="/blog/@slug" class="group glass block p-6 transition-all duration-300 hover:-translate-y-1 hover:border-electric/40 reveal">
|
||||
<span class="label-mono text-electric mb-3 block">@cat</span>
|
||||
<h3 class="font-display font-semibold leading-snug text-white group-hover:text-electric transition-colors @(fa ? "font-fa" : "")" style="font-size:clamp(1rem,1.4vw,1.15rem)">@btitle</h3>
|
||||
<p class="mt-3 text-[.88rem] leading-relaxed text-slate-400 line-clamp-3">@excerpt</p>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="label-mono">@readTime @(fa ? "دقیقه" : "min") @(fa ? "ادامه" : "read")</span>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="text-electric @(fa ? "rotate-180" : "")"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg>
|
||||
<a href="/blog/@slug" class="group reveal grid grid-cols-1 gap-2 border-t border-zinc-200 py-6 sm:grid-cols-[8rem_1fr] sm:gap-8">
|
||||
<div class="flex items-baseline justify-between sm:flex-col sm:gap-1">
|
||||
<span class="kicker">@cat</span>
|
||||
<span class="text-[.78rem] text-zinc-400">@readTime @(fa ? "دقیقه" : "min")</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-[1.1rem] font-semibold transition-colors group-hover:text-accent @(fa ? "font-fa" : "")">@btitle</h3>
|
||||
<p class="mt-1.5 text-[.9rem] leading-relaxed text-zinc-600">@excerpt</p>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
@@ -452,37 +276,35 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── CONTACT ───────────────────────────────────────────────────────── -->
|
||||
<section id="contact" class="relative px-5 py-28 sm:px-8">
|
||||
<!-- ─── CONTACT ──────────────────────────────────────────────────────── -->
|
||||
<section id="contact" class="px-5 py-24 sm:px-8 sm:py-28">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<div class="section-header">
|
||||
<div class="eyebrow"><span class="chip">@(fa ? "تماس" : "Contact")</span></div>
|
||||
<h2>@(fa ? "رزرو یک جلسه ۳۰ دقیقهای" : "Book a 30-minute call")</h2>
|
||||
<p>@(fa ? "بدون هزینه، بدون تعهد. موارد کاربردی، محدودیتها و گام بعدی را با هم بررسی میکنیم." : "No cost, no commitment. We map the use case, the constraints, and the next step together.")</p>
|
||||
<div class="sec-head">
|
||||
<h2>@(fa ? "رزرو یک جلسهی ۳۰ دقیقهای" : "Book a 30-minute call")</h2>
|
||||
<p class="lede">@(fa ? "بدون هزینه، بدون تعهد. مورد کاربری، محدودیتها و گام بعدی را با هم مشخص میکنیم." : "No cost, no commitment. We map the use case, the constraints, and the next step together.")</p>
|
||||
</div>
|
||||
|
||||
<form id="contact-form"
|
||||
class="glass p-8 space-y-5"
|
||||
data-success-msg="@(fa ? "پیام ارسال شد. معمولاً ظرف ۲۴ ساعت کاری پاسخ میدهم." : "Sent! Typical reply within 24 working hours.")"
|
||||
<form id="contact-form" class="card space-y-5 p-6 sm:p-8"
|
||||
data-success-msg="@(fa ? "پیام ارسال شد. معمولاً ظرف ۲۴ ساعت کاری پاسخ میدهم." : "Sent. Typical reply within 24 working hours.")"
|
||||
data-error-msg="@(fa ? "خطایی رخ داد. لطفاً دوباره امتحان کنید." : "Something went wrong. Please try again.")">
|
||||
<input type="hidden" name="locale" value="@locale" />
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label-mono mb-2 block" for="name">@(fa ? "نام" : "Name")</label>
|
||||
<input id="name" name="name" type="text" required placeholder="@(fa ? "نام و نام خانوادگی" : "Full name")" class="w-full rounded-xl border border-white/10 bg-white/[.03] px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-electric/60 transition-colors" />
|
||||
<label class="flabel" for="name">@(fa ? "نام" : "Name")</label>
|
||||
<input id="name" name="name" type="text" required placeholder="@(fa ? "نام و نام خانوادگی" : "Full name")" class="field" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label-mono mb-2 block" for="company">@(fa ? "سازمان" : "Company")</label>
|
||||
<input id="company" name="company" type="text" placeholder="@(fa ? "نام سازمان" : "Organization")" class="w-full rounded-xl border border-white/10 bg-white/[.03] px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-electric/60 transition-colors" />
|
||||
<label class="flabel" for="company">@(fa ? "سازمان" : "Company")</label>
|
||||
<input id="company" name="company" type="text" placeholder="@(fa ? "نام سازمان" : "Organization")" class="field" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label-mono mb-2 block" for="service">@(fa ? "خدمت" : "Service")</label>
|
||||
<select id="service" name="service" required class="w-full rounded-xl border border-white/10 bg-base-800 px-4 py-3 text-sm text-white outline-none focus:border-electric/60 transition-colors">
|
||||
<option value="" disabled selected>—</option>
|
||||
<label class="flabel" for="service">@(fa ? "خدمت" : "Service")</label>
|
||||
<select id="service" name="service" required class="field">
|
||||
<option value="" disabled selected>@(fa ? "انتخاب کنید" : "Select…")</option>
|
||||
@if (fa)
|
||||
{
|
||||
<option value="strategy">راهبرد و نقشه راه</option>
|
||||
@@ -494,9 +316,9 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="strategy">AI Strategy & Roadmap</option>
|
||||
<option value="strategy">AI Strategy and Roadmap</option>
|
||||
<option value="automation">AI Automation</option>
|
||||
<option value="llm-rag">LLM & RAG Engineering</option>
|
||||
<option value="llm-rag">LLM and RAG Engineering</option>
|
||||
<option value="architecture">Solution Architecture</option>
|
||||
<option value="mobile">Mobile AI Apps</option>
|
||||
<option value="google-stack">Google Stack</option>
|
||||
@@ -504,47 +326,47 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label-mono mb-2 block" for="budget">@(fa ? "بودجه (تقریبی)" : "Budget (rough)")</label>
|
||||
<select id="budget" name="budget" required class="w-full rounded-xl border border-white/10 bg-base-800 px-4 py-3 text-sm text-white outline-none focus:border-electric/60 transition-colors">
|
||||
<option value="" disabled selected>—</option>
|
||||
<label class="flabel" for="budget">@(fa ? "بودجه (تقریبی)" : "Budget (rough)")</label>
|
||||
<select id="budget" name="budget" required class="field">
|
||||
<option value="" disabled selected>@(fa ? "انتخاب کنید" : "Select…")</option>
|
||||
<option>Under $10k</option>
|
||||
<option>$10k–$50k</option>
|
||||
<option>$50k–$200k</option>
|
||||
<option>$10k - $50k</option>
|
||||
<option>$50k - $200k</option>
|
||||
<option>$200k+</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label-mono mb-2 block" for="message">@(fa ? "پیام" : "Message")</label>
|
||||
<textarea id="message" name="message" required rows="4" placeholder="@(fa ? "هدف، بازه زمانی، موانع فعلی…" : "Goal, timeline, current blockers…")" class="w-full rounded-xl border border-white/10 bg-white/[.03] px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-electric/60 transition-colors resize-none"></textarea>
|
||||
<label class="flabel" for="message">@(fa ? "پیام" : "Message")</label>
|
||||
<textarea id="message" name="message" required rows="4" placeholder="@(fa ? "هدف، بازه زمانی، موانع فعلی…" : "Goal, timeline, current blockers…")" class="field resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full justify-center">
|
||||
@(fa ? "ارسال درخواست" : "Send request")
|
||||
</button>
|
||||
<p id="contact-status" class="text-center text-sm text-slate-500">@(fa ? "معمولاً ظرف ۲۴ ساعت کاری پاسخ میدهم." : "Typical reply within 24 working hours.")</p>
|
||||
<button type="submit" class="btn w-full">@(fa ? "ارسال درخواست" : "Send request")</button>
|
||||
<p id="contact-status" class="mt-1 text-sm text-zinc-500">@(fa ? "معمولاً ظرف ۲۴ ساعت کاری پاسخ میدهم." : "Typical reply within 24 working hours.")</p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── FOOTER ────────────────────────────────────────────────────────── -->
|
||||
<footer class="border-t border-white/5 px-5 py-10 sm:px-8">
|
||||
<div class="mx-auto flex max-w-7xl flex-col items-center gap-3 text-center">
|
||||
<img src="/logo-mark.svg" alt="" width="24" height="24" />
|
||||
<p class="label-mono">@(fa ? "طراحیشده در تهران · ساختهشده برای سازمانها" : "Designed in Tehran · Built for the enterprise")</p>
|
||||
<p class="text-[.78rem] text-slate-600">© 2026 Soroush Asadi. @(fa ? "تمام حقوق محفوظ است." : "All rights reserved.")</p>
|
||||
<!-- ─── FOOTER ───────────────────────────────────────────────────────── -->
|
||||
<footer class="border-t border-zinc-200 px-5 py-10 sm:px-8">
|
||||
<div class="mx-auto flex max-w-6xl flex-col items-center gap-3 text-center sm:flex-row sm:justify-between sm:text-start">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<img src="/logo-mark.svg" alt="" width="22" height="22" />
|
||||
<span class="text-sm text-zinc-600">@(fa ? "مهندسی سامانههای هوش مصنوعی برای سازمانها." : "AI systems engineering for the enterprise.")</span>
|
||||
</div>
|
||||
<p class="text-[.78rem] text-zinc-400">© 2026 Soroush Asadi. @(fa ? "تمام حقوق محفوظ است." : "All rights reserved.")</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@functions {
|
||||
static string ServiceIcon(string id) => id switch {
|
||||
"strategy" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M9 20H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4"/><polyline points="9,9 4,9"/><polyline points="9,12 4,12"/><polyline points="9,15 4,15"/><rect x="9" y="2" width="6" height="6"/><path d="M15 8h4a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-4"/></svg>""",
|
||||
"automation" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.22 4.22l2.12 2.12M17.66 17.66l2.12 2.12M2 12h3M19 12h3M4.22 19.78l2.12-2.12M17.66 6.34l2.12-2.12"/></svg>""",
|
||||
"llm-rag" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>""",
|
||||
"architecture" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="2" y="3" width="6" height="6"/><rect x="16" y="3" width="6" height="6"/><rect x="9" y="15" width="6" height="6"/><path d="M5 9v3h14V9M12 12v3"/></svg>""",
|
||||
"mobile" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="5" y="2" width="14" height="20" rx="2"/><path d="M12 18h.01"/></svg>""",
|
||||
"google-stack" => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>""",
|
||||
_ => """<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="10"/></svg>""",
|
||||
"strategy" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M9 20H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4"/><polyline points="9,9 4,9"/><polyline points="9,12 4,12"/><polyline points="9,15 4,15"/><rect x="9" y="2" width="6" height="6"/><path d="M15 8h4a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-4"/></svg>""",
|
||||
"automation" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.22 4.22l2.12 2.12M17.66 17.66l2.12 2.12M2 12h3M19 12h3M4.22 19.78l2.12-2.12M17.66 6.34l2.12-2.12"/></svg>""",
|
||||
"llm-rag" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>""",
|
||||
"architecture" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="2" y="3" width="6" height="6"/><rect x="16" y="3" width="6" height="6"/><rect x="9" y="15" width="6" height="6"/><path d="M5 9v3h14V9M12 12v3"/></svg>""",
|
||||
"mobile" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="5" y="2" width="14" height="20" rx="2"/><path d="M12 18h.01"/></svg>""",
|
||||
"google-stack" => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>""",
|
||||
_ => """<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>""",
|
||||
};
|
||||
}
|
||||
|
||||
+27
-72
@@ -4,8 +4,8 @@
|
||||
var dir = isRtl ? "rtl" : "ltr";
|
||||
var lang = locale == "fa" ? "fa" : "en";
|
||||
var title = (string?)ViewData["Title"] ?? (locale == "fa"
|
||||
? "سروش اسعدی — مهندس هوش مصنوعی، مشاور، معمار راهکار"
|
||||
: "Soroush Asadi — AI Engineer, Consultant, Solution Architect");
|
||||
? "سروش اسعدی - مهندس هوش مصنوعی، مشاور، معمار راهکار"
|
||||
: "Soroush Asadi - AI Engineer, Consultant, Solution Architect");
|
||||
}
|
||||
<!doctype html>
|
||||
<html lang="@lang" dir="@dir">
|
||||
@@ -14,89 +14,53 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>@title</title>
|
||||
<meta name="description" content="@(locale == "fa"
|
||||
? "طراحی و پیادهسازی سامانههای هوش مصنوعی در مقیاس سازمانی — راهبرد، LLM و RAG، اتوماسیون عاملمحور، زیرساخت ابری و استک گوگل."
|
||||
: "Designing and deploying enterprise-grade AI systems — strategy, LLM & RAG, agentic automation, cloud infrastructure, and Google Stack.")" />
|
||||
? "طراحی و پیادهسازی سامانههای هوش مصنوعی در مقیاس سازمانی. راهبرد، LLM و RAG، اتوماسیون عاملمحور، زیرساخت ابری و استک گوگل."
|
||||
: "Designing and deploying enterprise-grade AI systems. Strategy, LLM and RAG, agentic automation, cloud infrastructure, and the Google stack.")" />
|
||||
<meta name="theme-color" content="#fafafa" />
|
||||
|
||||
<!-- Fonts -->
|
||||
<!-- Fonts: Syne (display) + Vazirmatn (Persian). Body is system sans. -->
|
||||
<style>
|
||||
@@font-face { font-family:'Syne'; src:url('/fonts/Syne-Variable.woff2') format('woff2'); font-weight:100 900; font-display:swap; }
|
||||
@@font-face { font-family:'Vazirmatn'; src:url('/fonts/Vazirmatn-Arabic.woff2') format('woff2'); font-display:swap; }
|
||||
@@font-face { font-family:'VazirmatnLat'; src:url('/fonts/Vazirmatn-Latin.woff2') format('woff2'); font-display:swap; }
|
||||
@@font-face { font-family:'SpaceMono'; src:url('/fonts/SpaceMono-Regular.woff2') format('woff2'); font-weight:400; font-display:swap; }
|
||||
@@font-face { font-family:'SpaceMono'; src:url('/fonts/SpaceMono-Bold.woff2') format('woff2'); font-weight:700; font-display:swap; }
|
||||
</style>
|
||||
|
||||
<!-- Tailwind CDN (play) + custom config -->
|
||||
<!-- Tailwind Play CDN - minimal config: one accent, neutral zinc scale is built in -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
base: { DEFAULT:'#020510', 800:'#050a1a', 700:'#0a1224', 600:'#0f1b33' },
|
||||
electric:'#38bdf8',
|
||||
violet: '#818cf8',
|
||||
magenta: '#e879f9',
|
||||
emerald: '#34d399',
|
||||
cyan: '#22d3ee',
|
||||
},
|
||||
colors: { accent: '#2563eb', accentink: '#1d4ed8' },
|
||||
fontFamily: {
|
||||
sans: ['Syne','Vazirmatn','VazirmatnLat','system-ui','sans-serif'],
|
||||
display: ['Syne','Vazirmatn','sans-serif'],
|
||||
fa: ['Vazirmatn','VazirmatnLat','sans-serif'],
|
||||
mono: ['SpaceMono','ui-monospace','monospace'],
|
||||
},
|
||||
keyframes: {
|
||||
'pulse-dot': {'0%,100%':{opacity:'1',transform:'scale(1)'},'50%':{opacity:'.6',transform:'scale(1.4)'}},
|
||||
'gradient-pan': {'0%,100%':{backgroundPosition:'0% 50%'},'50%':{backgroundPosition:'100% 50%'}},
|
||||
'caret-blink': {'0%,49%':{opacity:'1'},'50%,100%':{opacity:'0'}},
|
||||
'float-y': {'0%,100%':{transform:'translateY(0)'},'50%':{transform:'translateY(-6px)'}},
|
||||
'flow-dash': {'0%':{strokeDashoffset:'0'},'100%':{strokeDashoffset:'-66'}},
|
||||
'fade-up': {'0%':{opacity:'0',transform:'translateY(24px)'},'100%':{opacity:'1',transform:'translateY(0)'}},
|
||||
'bar-grow': {'0%':{width:'0%'},'100%':{width:'var(--bar-w)'}},
|
||||
},
|
||||
animation: {
|
||||
'pulse-dot': 'pulse-dot 1.8s ease-in-out infinite',
|
||||
'caret-blink':'caret-blink 1s steps(2) infinite',
|
||||
'float-y': 'float-y 4s ease-in-out infinite',
|
||||
'flow-dash': 'flow-dash 1.1s linear infinite',
|
||||
'fade-up': 'fade-up .7s cubic-bezier(.22,1,.36,1) forwards',
|
||||
'bar-grow': 'bar-grow 1.2s cubic-bezier(.22,1,.36,1) forwards',
|
||||
},
|
||||
boxShadow: {
|
||||
'glow-electric':'0 0 40px -8px rgba(56,189,248,.55)',
|
||||
'glow-magenta': '0 0 40px -8px rgba(232,121,249,.55)',
|
||||
'glow-violet': '0 0 40px -8px rgba(129,140,248,.55)',
|
||||
'glow-emerald': '0 0 40px -8px rgba(52,211,153,.55)',
|
||||
display: ['Syne', 'system-ui', 'sans-serif'],
|
||||
fa: ['Vazirmatn', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Site CSS -->
|
||||
<link rel="stylesheet" href="/css/site.css" />
|
||||
<link rel="icon" href="/logo-mark.svg" type="image/svg+xml" />
|
||||
</head>
|
||||
<body class="bg-base text-slate-200 antialiased">
|
||||
<body class="site antialiased">
|
||||
|
||||
<!-- Custom cursor (desktop only) -->
|
||||
<div id="cursor" class="pointer-events-none fixed z-[9999] hidden h-5 w-5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-electric/70 transition-transform duration-100 lg:block" aria-hidden="true"></div>
|
||||
<div id="cursor-dot" class="pointer-events-none fixed z-[9999] hidden h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-electric lg:block" aria-hidden="true"></div>
|
||||
<!-- Sentinel for the navbar border (observed by IntersectionObserver) -->
|
||||
<div id="nav-sentinel" aria-hidden="true"></div>
|
||||
|
||||
<!-- Navbar -->
|
||||
<header id="navbar" class="fixed inset-x-0 top-0 z-50 transition-all duration-300">
|
||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-5 py-4 sm:px-8">
|
||||
<header id="navbar" class="fixed inset-x-0 top-0 z-50">
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-5 py-3.5 sm:px-8">
|
||||
<!-- Logo -->
|
||||
<a href="/#top" class="flex items-center gap-2.5" aria-label="Home">
|
||||
<img src="/logo-mark.svg" alt="" width="28" height="28" class="h-7 w-7" />
|
||||
<span class="font-display font-bold text-white @(isRtl ? "font-fa" : "")">
|
||||
<a href="/#top" class="flex items-center gap-2.5" aria-label="@(locale == "fa" ? "خانه" : "Home")">
|
||||
<img src="/logo-mark.svg" alt="" width="26" height="26" class="h-[26px] w-[26px]" />
|
||||
<span class="font-display text-[15px] font-bold text-zinc-900 @(isRtl ? "font-fa" : "")">
|
||||
@(locale == "fa" ? "سروش اسعدی" : "Soroush Asadi")
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop nav -->
|
||||
<nav class="hidden items-center gap-6 md:flex" aria-label="Main">
|
||||
<nav class="hidden items-center gap-7 md:flex" aria-label="Main">
|
||||
@if (locale == "fa")
|
||||
{
|
||||
<a href="/#services" class="nav-link">خدمات</a>
|
||||
@@ -104,7 +68,6 @@
|
||||
<a href="/#expertise" class="nav-link">تخصص</a>
|
||||
<a href="/#portfolio" class="nav-link">نمونهکارها</a>
|
||||
<a href="/#blog" class="nav-link">بلاگ</a>
|
||||
<a href="/#contact" class="nav-link">تماس</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -113,31 +76,26 @@
|
||||
<a href="/#expertise" class="nav-link">Expertise</a>
|
||||
<a href="/#portfolio" class="nav-link">Portfolio</a>
|
||||
<a href="/#blog" class="nav-link">Blog</a>
|
||||
<a href="/#contact" class="nav-link">Contact</a>
|
||||
}
|
||||
<a href="/#contact" class="btn-primary text-sm">
|
||||
@(locale == "fa" ? "رزرو جلسه" : "Book a call")
|
||||
</a>
|
||||
<a href="/#contact" class="btn text-sm">@(locale == "fa" ? "رزرو جلسه" : "Book a call")</a>
|
||||
<!-- Locale toggle -->
|
||||
<form method="post" action="/locale">
|
||||
<input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" />
|
||||
<input type="hidden" name="returnUrl" value="@Context.Request.Path@Context.Request.QueryString" />
|
||||
<button type="submit" class="label-mono text-slate-400 hover:text-white transition-colors">
|
||||
@(locale == "fa" ? "EN" : "FA")
|
||||
</button>
|
||||
<button type="submit" class="nav-link text-xs tracking-wide">@(locale == "fa" ? "EN" : "FA")</button>
|
||||
</form>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<button id="menu-btn" class="flex flex-col gap-1.5 p-2 md:hidden" aria-label="Menu">
|
||||
<span class="block h-0.5 w-5 bg-slate-300 transition-all"></span>
|
||||
<span class="block h-0.5 w-5 bg-slate-300 transition-all"></span>
|
||||
<span class="block h-0.5 w-5 bg-slate-300 transition-all"></span>
|
||||
<button id="menu-btn" class="flex flex-col gap-1.5 p-2 md:hidden" aria-label="@(locale == "fa" ? "منو" : "Menu")" aria-expanded="false">
|
||||
<span class="block h-0.5 w-5 bg-zinc-800"></span>
|
||||
<span class="block h-0.5 w-5 bg-zinc-800"></span>
|
||||
<span class="block h-0.5 w-5 bg-zinc-800"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile drawer -->
|
||||
<div id="mobile-menu" class="hidden border-t border-white/5 bg-base-800/95 backdrop-blur-xl md:hidden">
|
||||
<div id="mobile-menu" class="hidden border-t border-zinc-200 bg-white/95 backdrop-blur-xl md:hidden">
|
||||
<nav class="flex flex-col gap-1 px-5 py-4">
|
||||
@if (locale == "fa")
|
||||
{
|
||||
@@ -160,9 +118,7 @@
|
||||
<form method="post" action="/locale" class="mt-2">
|
||||
<input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" />
|
||||
<input type="hidden" name="returnUrl" value="@Context.Request.Path@Context.Request.QueryString" />
|
||||
<button type="submit" class="label-mono text-slate-400">
|
||||
@(locale == "fa" ? "Switch to English" : "تغییر به فارسی")
|
||||
</button>
|
||||
<button type="submit" class="nav-link text-xs tracking-wide">@(locale == "fa" ? "Switch to English" : "تغییر به فارسی")</button>
|
||||
</form>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -172,7 +128,6 @@
|
||||
@RenderBody()
|
||||
</main>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/js/app.js" defer></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
|
||||
+127
-89
@@ -1,115 +1,152 @@
|
||||
/* ─── Design tokens ──────────────────────────────────────────────────── */
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
Minimal editorial theme - light, single accent.
|
||||
Public site styles are scoped under `.site` so the dark admin (which shares
|
||||
this file via _AdminLayout) is left untouched. Admin-only classes (.glass,
|
||||
.label-mono, .admin-nav-link) keep their dark definitions at the bottom.
|
||||
════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
--bg: #020510;
|
||||
--electric: #38bdf8;
|
||||
--violet: #818cf8;
|
||||
--magenta: #e879f9;
|
||||
--emerald: #34d399;
|
||||
--cyan: #22d3ee;
|
||||
--radius: 14px;
|
||||
color-scheme: dark;
|
||||
--bg: #fafafa;
|
||||
--surface: #ffffff;
|
||||
--text: #18181b; /* zinc-900 */
|
||||
--text-2: #52525b; /* zinc-600 */
|
||||
--text-3: #a1a1aa; /* zinc-400 */
|
||||
--line: #e4e4e7; /* zinc-200 */
|
||||
--line-strong: #d4d4d8; /* zinc-300 */
|
||||
--accent: #2563eb; /* blue-600 - the single accent */
|
||||
--accent-ink: #1d4ed8; /* blue-700 */
|
||||
--accent-weak: #eff4ff;
|
||||
--radius: 8px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
background: var(--bg);
|
||||
font-feature-settings: 'ss01','cv11';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; background: var(--bg); }
|
||||
|
||||
::selection { background: #dbe5ff; color: #18181b; }
|
||||
|
||||
a:focus-visible, button:focus-visible,
|
||||
input:focus-visible, textarea:focus-visible, select:focus-visible {
|
||||
outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px;
|
||||
}
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
[dir='rtl'] body { font-family: 'Vazirmatn','VazirmatnLat','Syne',system-ui,sans-serif; }
|
||||
[dir='ltr'] body { font-family: 'Syne','Vazirmatn','VazirmatnLat',system-ui,sans-serif; }
|
||||
|
||||
::selection { background: rgba(56,189,248,.35); color: #f8fafc; }
|
||||
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-track { background: #050a1a; }
|
||||
::-webkit-scrollbar-thumb { background: linear-gradient(180deg,#38bdf8,#818cf8); border-radius:999px; border:2px solid #050a1a; }
|
||||
::-webkit-scrollbar-track { background: var(--bg); }
|
||||
::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; border: 3px solid var(--bg); }
|
||||
|
||||
/* ─── Component classes (used across Razor templates) ────────────────── */
|
||||
/* ─── Public base (scoped to .site) ──────────────────────────────────── */
|
||||
body.site {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Vazirmatn', sans-serif;
|
||||
font-size: 1rem; line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
|
||||
}
|
||||
[dir='rtl'] body.site { font-family: 'Vazirmatn', system-ui, sans-serif; }
|
||||
|
||||
.glass {
|
||||
background: linear-gradient(180deg,rgba(255,255,255,.04) 0%,rgba(255,255,255,.015) 100%);
|
||||
border: 1px solid rgba(56,189,248,.14);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: inset 0 1px 0 0 rgba(255,255,255,.06), 0 30px 60px -30px rgba(0,0,0,.6);
|
||||
border-radius: var(--radius);
|
||||
.site h1, .site h2, .site h3, .site h4 {
|
||||
font-family: 'Syne', system-ui, sans-serif;
|
||||
font-weight: 700; color: var(--text);
|
||||
letter-spacing: -0.02em; line-height: 1.12;
|
||||
}
|
||||
[dir='rtl'] .site h1, [dir='rtl'] .site h2,
|
||||
[dir='rtl'] .site h3, [dir='rtl'] .site h4 {
|
||||
font-family: 'Vazirmatn', system-ui, sans-serif; letter-spacing: 0;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: .5rem;
|
||||
padding: .35rem .75rem; border-radius: 999px;
|
||||
border: 1px solid rgba(52,211,153,.25); background: rgba(52,211,153,.06);
|
||||
color: #a7f3d0;
|
||||
font-family: 'SpaceMono',ui-monospace,monospace;
|
||||
font-size: .72rem; letter-spacing: .04em; text-transform: uppercase;
|
||||
}
|
||||
/* ─── Navigation ─────────────────────────────────────────────────────── */
|
||||
.nav-link { font-size: .9rem; color: var(--text-2); transition: color .18s ease; }
|
||||
.nav-link:hover { color: var(--text); }
|
||||
#navbar { transition: background .25s ease, border-color .25s ease; border-bottom: 1px solid transparent; }
|
||||
#navbar.scrolled { background: rgba(250,250,250,.82); backdrop-filter: blur(12px); border-bottom-color: var(--line); }
|
||||
|
||||
.label-mono {
|
||||
font-family: 'SpaceMono',ui-monospace,monospace;
|
||||
font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: #94a3b8;
|
||||
/* ─── Buttons (one shape: 8px radius, no pills, no glow) ──────────────── */
|
||||
.btn, .btn-primary {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
|
||||
padding: .7rem 1.15rem; border-radius: var(--radius);
|
||||
font-weight: 600; font-size: .92rem;
|
||||
background: var(--accent); color: #fff; border: 1px solid var(--accent);
|
||||
transition: background .18s ease, transform .12s ease;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg,#38bdf8 0%,#818cf8 45%,#e879f9 100%);
|
||||
-webkit-background-clip: text; background-clip: text; color: transparent;
|
||||
background-size: 200% 200%; animation: gradient-pan 8s ease-in-out infinite;
|
||||
}
|
||||
@keyframes gradient-pan { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex; align-items: center; gap: .6rem;
|
||||
padding: .85rem 1.4rem; border-radius: 999px;
|
||||
font-weight: 600; color: #020510;
|
||||
background: linear-gradient(135deg,#38bdf8 0%,#818cf8 60%,#e879f9 100%);
|
||||
background-size: 200% 200%;
|
||||
transition: transform .25s ease, box-shadow .25s ease, background-position .6s ease;
|
||||
box-shadow: 0 12px 40px -12px rgba(56,189,248,.55);
|
||||
}
|
||||
.btn-primary:hover { transform: translateY(-1px); background-position: 100% 0; box-shadow: 0 18px 50px -10px rgba(232,121,249,.55); }
|
||||
.btn:hover, .btn-primary:hover { background: var(--accent-ink); border-color: var(--accent-ink); }
|
||||
.btn:active, .btn-primary:active { transform: translateY(1px); }
|
||||
|
||||
.btn-ghost {
|
||||
display: inline-flex; align-items: center; gap: .6rem;
|
||||
padding: .8rem 1.35rem; border-radius: 999px;
|
||||
font-weight: 500; color: #e2e8f0;
|
||||
border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.02);
|
||||
transition: border-color .25s ease, background .25s ease, transform .25s ease;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
|
||||
padding: .7rem 1.15rem; border-radius: var(--radius);
|
||||
font-weight: 500; font-size: .92rem;
|
||||
color: var(--text); background: transparent; border: 1px solid var(--line-strong);
|
||||
transition: border-color .18s ease, background .18s ease, transform .12s ease;
|
||||
}
|
||||
.btn-ghost:hover { border-color: rgba(56,189,248,.6); background: rgba(56,189,248,.06); transform: translateY(-1px); }
|
||||
.btn-ghost:hover { border-color: var(--text); background: #fff; }
|
||||
.btn-ghost:active { transform: translateY(1px); }
|
||||
|
||||
.nav-link {
|
||||
font-size: .875rem; color: #94a3b8;
|
||||
transition: color .2s ease; text-decoration: none;
|
||||
/* ─── Type helpers ───────────────────────────────────────────────────── */
|
||||
.kicker {
|
||||
font-size: .76rem; letter-spacing: .14em; text-transform: uppercase;
|
||||
color: var(--text-3); font-weight: 500;
|
||||
}
|
||||
.nav-link:hover { color: #e2e8f0; }
|
||||
[dir='rtl'] .kicker { letter-spacing: .06em; }
|
||||
.lede { color: var(--text-2); font-size: 1.05rem; line-height: 1.7; max-width: 44rem; }
|
||||
|
||||
#navbar.scrolled { background: rgba(2,5,16,.85); backdrop-filter: blur(20px); border-bottom: 1px solid rgba(255,255,255,.05); }
|
||||
.sec-head { margin-bottom: 3rem; max-width: 46rem; }
|
||||
.sec-head h2 { font-size: clamp(1.6rem, 3vw, 2.25rem); }
|
||||
.sec-head .lede { margin-top: .9rem; }
|
||||
|
||||
/* ─── Section header ─────────────────────────────────────────────── */
|
||||
.section-header { text-align: center; margin-bottom: 3.5rem; }
|
||||
.section-header .eyebrow { margin-bottom: 1rem; }
|
||||
.section-header h2 { font-family:'Syne',sans-serif; font-size:clamp(1.8rem,3.5vw,2.75rem); font-weight:800; color:#fff; margin-bottom:1rem; }
|
||||
[dir='rtl'] .section-header h2 { font-family:'Vazirmatn',sans-serif; }
|
||||
.section-header p { max-width:42rem; margin:0 auto; color:#94a3b8; font-size:1.0625rem; line-height:1.7; }
|
||||
/* ─── Neutral tag chip (one shape) ───────────────────────────────────── */
|
||||
.chip {
|
||||
display: inline-flex; align-items: center;
|
||||
border: 1px solid var(--line); border-radius: 6px;
|
||||
padding: .2rem .55rem; font-size: .72rem; color: var(--text-2);
|
||||
background: #fff; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ─── Service cards ──────────────────────────────────────────────── */
|
||||
.service-card { transition: border-color .3s, box-shadow .3s, transform .3s; }
|
||||
.service-card:hover { transform: translateY(-2px); }
|
||||
/* ─── Surface card (portfolio, contact) ──────────────────────────────── */
|
||||
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
|
||||
.card-link { transition: border-color .18s ease, transform .18s ease; }
|
||||
.card-link:hover { border-color: var(--line-strong); transform: translateY(-2px); }
|
||||
|
||||
/* ─── Expertise bars ─────────────────────────────────────────────── */
|
||||
.bar-track { height: .375rem; background: rgba(255,255,255,.07); border-radius:999px; overflow:hidden; }
|
||||
.bar-fill { height: 100%; border-radius:999px; transform-origin:left; }
|
||||
[dir='rtl'] .bar-fill { transform-origin:right; }
|
||||
/* ─── Form fields ────────────────────────────────────────────────────── */
|
||||
.flabel { display: block; font-size: .85rem; font-weight: 500; color: var(--text-2); margin-bottom: .4rem; }
|
||||
.field {
|
||||
width: 100%; font-family: inherit; font-size: .92rem; color: var(--text);
|
||||
background: #fff; border: 1px solid var(--line-strong); border-radius: var(--radius);
|
||||
padding: .7rem .85rem; transition: border-color .18s ease, box-shadow .18s ease;
|
||||
}
|
||||
.field::placeholder { color: var(--text-3); }
|
||||
.field:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
|
||||
|
||||
/* ─── Scroll-reveal ──────────────────────────────────────────────── */
|
||||
.reveal { opacity: 0; transform: translateY(24px); transition: opacity .7s cubic-bezier(.22,1,.36,1), transform .7s cubic-bezier(.22,1,.36,1); }
|
||||
/* ─── Scroll reveal (MOTION_INTENSITY 3: subtle, transform+opacity only) ─ */
|
||||
.reveal { opacity: 0; transform: translateY(14px); transition: opacity .6s ease, transform .6s ease; }
|
||||
.reveal.visible { opacity: 1; transform: none; }
|
||||
|
||||
/* ─── Portfolio modal ────────────────────────────────────────────── */
|
||||
#portfolio-modal { transition: opacity .25s ease; }
|
||||
#portfolio-modal.hidden { pointer-events: none; }
|
||||
/* ─── Blog prose ─────────────────────────────────────────────────────── */
|
||||
.prose-custom { color: var(--text-2); line-height: 1.8; font-size: 1.02rem; }
|
||||
.prose-custom h2, .prose-custom h3 { color: var(--text); font-family: 'Syne', system-ui, sans-serif; margin: 2rem 0 .6rem; }
|
||||
[dir='rtl'] .prose-custom h2, [dir='rtl'] .prose-custom h3 { font-family: 'Vazirmatn', system-ui, sans-serif; }
|
||||
.prose-custom p { margin-bottom: 1.2rem; }
|
||||
.prose-custom a { color: var(--accent); text-decoration: underline; text-underline-offset: .2em; }
|
||||
.prose-custom strong { color: var(--text); }
|
||||
.prose-custom code { font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace; background: #f4f4f5; border: 1px solid var(--line); border-radius: 5px; padding: .12em .4em; font-size: .88em; color: #3f3f46; }
|
||||
.prose-custom pre { background: #f4f4f5; border: 1px solid var(--line); border-radius: 8px; padding: 1.1rem; overflow-x: auto; margin: 1.4rem 0; }
|
||||
.prose-custom pre code { background: none; border: 0; padding: 0; color: #27272a; }
|
||||
.prose-custom ul, .prose-custom ol { padding-inline-start: 1.4rem; margin-bottom: 1.2rem; }
|
||||
.prose-custom li { margin-bottom: .4rem; }
|
||||
.prose-custom blockquote { border-inline-start: 3px solid var(--accent); padding-inline-start: 1rem; color: var(--text-2); margin: 1.4rem 0; }
|
||||
|
||||
/* ─── Admin nav ──────────────────────────────────────────────────── */
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
Admin (dark) - preserved. _AdminLayout has its own dark Tailwind config;
|
||||
these classes keep working on its dark surface. Do not light-theme these.
|
||||
════════════════════════════════════════════════════════════════════════ */
|
||||
.glass {
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.015));
|
||||
border: 1px solid rgba(255,255,255,.10);
|
||||
border-radius: 14px;
|
||||
box-shadow: inset 0 1px 0 0 rgba(255,255,255,.05);
|
||||
}
|
||||
.label-mono {
|
||||
font-family: 'SpaceMono', ui-monospace, monospace;
|
||||
font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: #94a3b8;
|
||||
}
|
||||
.admin-nav-link {
|
||||
display: block; padding: .5rem .75rem; border-radius: .5rem;
|
||||
color: #94a3b8; font-size: .875rem; text-decoration: none;
|
||||
@@ -118,10 +155,11 @@ html { scroll-behavior: smooth; }
|
||||
.admin-nav-link:hover { background: rgba(255,255,255,.05); color: #e2e8f0; }
|
||||
.admin-nav-link.active { background: rgba(56,189,248,.1); color: #38bdf8; }
|
||||
|
||||
/* ─── Reduced motion ─────────────────────────────────────────────── */
|
||||
/* ─── Reduced motion ─────────────────────────────────────────────────── */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,*::before,*::after {
|
||||
*, *::before, *::after {
|
||||
animation-duration: .001ms !important; animation-iteration-count: 1 !important;
|
||||
transition-duration: .001ms !important; scroll-behavior: auto !important;
|
||||
}
|
||||
.reveal { opacity: 1 !important; transform: none !important; }
|
||||
}
|
||||
|
||||
+36
-215
@@ -1,257 +1,78 @@
|
||||
/* ─── Custom cursor ───────────────────────────────────────────────────── */
|
||||
(function () {
|
||||
const ring = document.getElementById('cursor');
|
||||
const dot = document.getElementById('cursor-dot');
|
||||
if (!ring || !dot) return;
|
||||
let mx = 0, my = 0, rx = 0, ry = 0;
|
||||
document.addEventListener('mousemove', e => { mx = e.clientX; my = e.clientY; });
|
||||
document.addEventListener('mouseenter', () => { ring.style.opacity = '1'; dot.style.opacity = '1'; });
|
||||
document.addEventListener('mouseleave', () => { ring.style.opacity = '0'; dot.style.opacity = '0'; });
|
||||
(function raf() {
|
||||
rx += (mx - rx) * 0.12; ry += (my - ry) * 0.12;
|
||||
ring.style.left = rx + 'px'; ring.style.top = ry + 'px';
|
||||
dot.style.left = mx + 'px'; dot.style.top = my + 'px';
|
||||
requestAnimationFrame(raf);
|
||||
})();
|
||||
document.querySelectorAll('a,button,label,input,textarea,select').forEach(el => {
|
||||
el.addEventListener('mouseenter', () => ring.style.transform = 'translate(-50%,-50%) scale(1.6)');
|
||||
el.addEventListener('mouseleave', () => ring.style.transform = 'translate(-50%,-50%) scale(1)');
|
||||
});
|
||||
})();
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
Minimal interactions. MOTION_INTENSITY 3: hover + a single subtle reveal.
|
||||
No custom cursor, no particle canvas, no typewriter, no scroll listeners.
|
||||
════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ─── Navbar scroll shadow ────────────────────────────────────────────── */
|
||||
/* ─── Navbar border on scroll (IntersectionObserver, not a scroll listener) */
|
||||
(function () {
|
||||
const nav = document.getElementById('navbar');
|
||||
if (!nav) return;
|
||||
const update = () => nav.classList.toggle('scrolled', window.scrollY > 30);
|
||||
window.addEventListener('scroll', update, { passive: true });
|
||||
update();
|
||||
const sentinel = document.getElementById('nav-sentinel');
|
||||
if (!nav || !sentinel) return;
|
||||
const io = new IntersectionObserver(
|
||||
([entry]) => nav.classList.toggle('scrolled', !entry.isIntersecting),
|
||||
{ threshold: 0 }
|
||||
);
|
||||
io.observe(sentinel);
|
||||
})();
|
||||
|
||||
/* ─── Mobile menu toggle ──────────────────────────────────────────────── */
|
||||
/* ─── Mobile menu toggle ─────────────────────────────────────────────── */
|
||||
(function () {
|
||||
const btn = document.getElementById('menu-btn');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
if (!btn || !menu) return;
|
||||
btn.addEventListener('click', () => {
|
||||
const open = menu.classList.toggle('hidden');
|
||||
btn.setAttribute('aria-expanded', String(!open));
|
||||
const hidden = menu.classList.toggle('hidden');
|
||||
btn.setAttribute('aria-expanded', String(!hidden));
|
||||
});
|
||||
menu.querySelectorAll('a').forEach(a => a.addEventListener('click', () => menu.classList.add('hidden')));
|
||||
})();
|
||||
|
||||
/* ─── Scroll-reveal (Intersection Observer) ───────────────────────────── */
|
||||
/* ─── Scroll reveal (one subtle entry per element, then unobserve) ────── */
|
||||
(function () {
|
||||
const els = document.querySelectorAll('.reveal');
|
||||
if (!els.length) return;
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
els.forEach(el => el.classList.add('visible'));
|
||||
return;
|
||||
}
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); } });
|
||||
}, { threshold: 0.12 });
|
||||
document.querySelectorAll('.reveal').forEach(el => io.observe(el));
|
||||
})();
|
||||
|
||||
/* ─── Particle canvas (hero background) ──────────────────────────────── */
|
||||
(function () {
|
||||
const canvas = document.getElementById('particle-canvas');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
let W, H, particles = [], animId;
|
||||
|
||||
function resize() {
|
||||
W = canvas.width = canvas.offsetWidth;
|
||||
H = canvas.height = canvas.offsetHeight;
|
||||
}
|
||||
|
||||
function makeParticle() {
|
||||
return {
|
||||
x: Math.random() * W, y: Math.random() * H,
|
||||
vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3,
|
||||
r: Math.random() * 1.5 + 0.5,
|
||||
a: Math.random() * 0.5 + 0.1
|
||||
};
|
||||
}
|
||||
|
||||
function init() { particles = Array.from({ length: 90 }, makeParticle); }
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
const MAX_DIST = 120;
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
const p = particles[i];
|
||||
p.x += p.vx; p.y += p.vy;
|
||||
if (p.x < 0) p.x = W; if (p.x > W) p.x = 0;
|
||||
if (p.y < 0) p.y = H; if (p.y > H) p.y = 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(56,189,248,${p.a})`;
|
||||
ctx.fill();
|
||||
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const q = particles[j];
|
||||
const dx = p.x - q.x, dy = p.y - q.y;
|
||||
const d = Math.sqrt(dx * dx + dy * dy);
|
||||
if (d < MAX_DIST) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y);
|
||||
ctx.strokeStyle = `rgba(56,189,248,${(1 - d / MAX_DIST) * 0.15})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
animId = requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
resize(); init(); draw();
|
||||
window.addEventListener('resize', () => { resize(); });
|
||||
|
||||
// Pause when hidden
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) { cancelAnimationFrame(animId); } else { draw(); }
|
||||
});
|
||||
})();
|
||||
|
||||
/* ─── Typewriter ──────────────────────────────────────────────────────── */
|
||||
(function () {
|
||||
const el = document.getElementById('typewriter');
|
||||
if (!el) return;
|
||||
const words = JSON.parse(el.dataset.words || '[]');
|
||||
if (!words.length) return;
|
||||
let wi = 0, ci = 0, deleting = false;
|
||||
|
||||
function tick() {
|
||||
const word = words[wi];
|
||||
if (!deleting) {
|
||||
el.textContent = word.slice(0, ++ci);
|
||||
if (ci === word.length) { setTimeout(tick, 1800); deleting = true; return; }
|
||||
setTimeout(tick, 80);
|
||||
} else {
|
||||
el.textContent = word.slice(0, --ci);
|
||||
if (ci === 0) { deleting = false; wi = (wi + 1) % words.length; setTimeout(tick, 300); return; }
|
||||
setTimeout(tick, 40);
|
||||
}
|
||||
}
|
||||
tick();
|
||||
})();
|
||||
|
||||
/* ─── Animated counters ───────────────────────────────────────────────── */
|
||||
(function () {
|
||||
const io = new IntersectionObserver(entries => {
|
||||
entries.forEach(e => {
|
||||
if (!e.isIntersecting) return;
|
||||
const el = e.target;
|
||||
const target = parseInt(el.dataset.target || '0', 10);
|
||||
const duration = 1200;
|
||||
const start = performance.now();
|
||||
function step(now) {
|
||||
const t = Math.min((now - start) / duration, 1);
|
||||
const ease = 1 - Math.pow(1 - t, 3);
|
||||
el.textContent = el.dataset.prefix + Math.round(target * ease) + (el.dataset.suffix || '');
|
||||
if (t < 1) requestAnimationFrame(step);
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
io.unobserve(el);
|
||||
if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); }
|
||||
});
|
||||
}, { threshold: 0.5 });
|
||||
document.querySelectorAll('.counter').forEach(el => io.observe(el));
|
||||
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
|
||||
els.forEach(el => io.observe(el));
|
||||
})();
|
||||
|
||||
/* ─── Expertise bars (animate on scroll) ─────────────────────────────── */
|
||||
(function () {
|
||||
const io = new IntersectionObserver(entries => {
|
||||
entries.forEach(e => {
|
||||
if (!e.isIntersecting) return;
|
||||
const fill = e.target.querySelector('.bar-fill');
|
||||
if (fill) { fill.style.width = fill.dataset.w; }
|
||||
io.unobserve(e.target);
|
||||
});
|
||||
}, { threshold: 0.3 });
|
||||
document.querySelectorAll('.bar-track').forEach(el => io.observe(el));
|
||||
})();
|
||||
|
||||
/* ─── Portfolio modal ─────────────────────────────────────────────────── */
|
||||
(function () {
|
||||
const modal = document.getElementById('portfolio-modal');
|
||||
const overlay = document.getElementById('modal-overlay');
|
||||
if (!modal) return;
|
||||
|
||||
let images = [], idx = 0;
|
||||
|
||||
const imgEl = document.getElementById('modal-img');
|
||||
const titleEl = document.getElementById('modal-title');
|
||||
const bodyEl = document.getElementById('modal-body');
|
||||
const prevBtn = document.getElementById('modal-prev');
|
||||
const nextBtn = document.getElementById('modal-next');
|
||||
const closeBtn = document.getElementById('modal-close');
|
||||
|
||||
function showModal(card) {
|
||||
images = JSON.parse(card.dataset.gallery || '[]');
|
||||
idx = 0;
|
||||
if (titleEl) titleEl.textContent = card.dataset.title || '';
|
||||
if (bodyEl) bodyEl.innerHTML = card.dataset.summary || '';
|
||||
updateImg();
|
||||
modal.classList.remove('hidden');
|
||||
modal.style.opacity = '0';
|
||||
requestAnimationFrame(() => { modal.style.opacity = '1'; });
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function hideModal() {
|
||||
modal.style.opacity = '0';
|
||||
setTimeout(() => { modal.classList.add('hidden'); document.body.style.overflow = ''; }, 250);
|
||||
}
|
||||
|
||||
function updateImg() {
|
||||
if (!imgEl) return;
|
||||
imgEl.src = images[idx] || '';
|
||||
if (prevBtn) prevBtn.disabled = idx === 0;
|
||||
if (nextBtn) nextBtn.disabled = idx === images.length - 1;
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-portfolio-card]').forEach(card => {
|
||||
card.addEventListener('click', () => showModal(card));
|
||||
card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); showModal(card); } });
|
||||
});
|
||||
|
||||
if (closeBtn) closeBtn.addEventListener('click', hideModal);
|
||||
if (overlay) overlay.addEventListener('click', hideModal);
|
||||
if (prevBtn) prevBtn.addEventListener('click', () => { if (idx > 0) { idx--; updateImg(); } });
|
||||
if (nextBtn) nextBtn.addEventListener('click', () => { if (idx < images.length - 1) { idx++; updateImg(); } });
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (modal.classList.contains('hidden')) return;
|
||||
if (e.key === 'Escape') hideModal();
|
||||
if (e.key === 'ArrowLeft') { if (idx > 0) { idx--; updateImg(); } }
|
||||
if (e.key === 'ArrowRight') { if (idx < images.length - 1) { idx++; updateImg(); } }
|
||||
});
|
||||
})();
|
||||
|
||||
/* ─── Contact form (AJAX) ─────────────────────────────────────────────── */
|
||||
/* ─── Contact form (AJAX, JSON → /contact) ───────────────────────────── */
|
||||
(function () {
|
||||
const form = document.getElementById('contact-form');
|
||||
if (!form) return;
|
||||
const status = document.getElementById('contact-status');
|
||||
|
||||
form.addEventListener('submit', async e => {
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
const btn = form.querySelector('[type="submit"]');
|
||||
btn.disabled = true;
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
const setStatus = (msg, cls) => { if (status) { status.textContent = msg; status.className = 'mt-1 text-sm ' + cls; } };
|
||||
|
||||
try {
|
||||
const res = await fetch('/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (res.ok) {
|
||||
if (status) { status.textContent = form.dataset.successMsg || 'Sent!'; status.className = 'mt-3 text-sm text-emerald-400'; }
|
||||
setStatus(form.dataset.successMsg || 'Sent.', 'text-emerald-600');
|
||||
form.reset();
|
||||
} else {
|
||||
if (status) { status.textContent = form.dataset.errorMsg || 'Something went wrong.'; status.className = 'mt-3 text-sm text-red-400'; }
|
||||
setStatus(form.dataset.errorMsg || 'Something went wrong.', 'text-red-600');
|
||||
}
|
||||
} catch {
|
||||
if (status) { status.textContent = form.dataset.errorMsg || 'Network error.'; status.className = 'mt-3 text-sm text-red-400'; }
|
||||
setStatus(form.dataset.errorMsg || 'Network error.', 'text-red-600');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="Soroush Asadi">
|
||||
<rect width="32" height="32" rx="7" fill="#18181b"/>
|
||||
<text x="16" y="21.5" text-anchor="middle" font-family="Syne, system-ui, sans-serif" font-size="13.5" font-weight="700" letter-spacing="-0.6" fill="#fafafa">SA</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
Reference in New Issue
Block a user