Redesign public site: minimal light editorial theme
deploy / deploy (push) Successful in 1m20s

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:
soroush.asadi
2026-06-21 05:16:06 +03:30
parent a55c75b928
commit 97bd2a12df
7 changed files with 433 additions and 810 deletions
+15 -16
View File
@@ -1,31 +1,30 @@
@page "/blog" @page "/blog"
@model SoroushAsadi.Pages.Blog.BlogIndexModel @model SoroushAsadi.Pages.Blog.BlogIndexModel
@{ @{
ViewData["Title"] = Model.IsFa ? "بلاگ سروش اسعدی" : "Blog Soroush Asadi"; ViewData["Title"] = Model.IsFa ? "بلاگ - سروش اسعدی" : "Blog - Soroush Asadi";
var fa = Model.IsFa; var fa = Model.IsFa;
} }
<div class="min-h-screen pt-28 pb-20 px-5 sm:px-8"> <div class="px-5 pt-28 pb-24 sm:px-8 sm:pt-32">
<div class="mx-auto max-w-7xl"> <div class="mx-auto max-w-4xl">
<div class="section-header mb-14"> <div class="sec-head">
<div class="eyebrow mb-4"><span class="chip">@(fa ? "بلاگ" : "Journal")</span></div> <h1 class="@(fa ? "font-fa" : "")" style="font-size:clamp(2rem,4vw,2.75rem)">
<h1 class="font-display text-4xl font-extrabold text-white @(fa ? "font-fa" : "")">
@(fa ? "یادداشت‌های مهندسی" : "Engineering notes") @(fa ? "یادداشت‌های مهندسی" : "Engineering notes")
</h1> </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>
<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) @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"> <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">
<span class="label-mono text-electric mb-3 block">@post.Category</span> <div class="flex items-baseline justify-between sm:flex-col sm:gap-1">
<h2 class="font-display font-semibold leading-snug text-white group-hover:text-electric transition-colors @(fa ? "font-fa" : "")" <span class="kicker">@post.Category</span>
style="font-size:clamp(1rem,1.4vw,1.15rem)">@post.Title</h2> <span class="text-[.78rem] text-zinc-400">@post.ReadTime @(fa ? "دقیقه" : "min")</span>
<p class="mt-3 text-[.88rem] leading-relaxed text-slate-400 line-clamp-3">@post.Excerpt</p> </div>
<div class="mt-4 flex items-center justify-between"> <div>
<span class="label-mono">@post.ReadTime @(fa ? "دقیقه" : "min") @(fa ? "مطالعه" : "read")</span> <h2 class="text-[1.1rem] font-semibold transition-colors group-hover:text-accent @(fa ? "font-fa" : "")">@post.Title</h2>
<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> <p class="mt-1.5 text-[.9rem] leading-relaxed text-zinc-600">@post.Excerpt</p>
</div> </div>
</a> </a>
} }
+12 -28
View File
@@ -1,48 +1,32 @@
@page "/blog/{slug}" @page "/blog/{slug}"
@model SoroushAsadi.Pages.Blog.PostModel @model SoroushAsadi.Pages.Blog.PostModel
@{ @{
ViewData["Title"] = Model.Title + " Soroush Asadi"; ViewData["Title"] = Model.Title + " - Soroush Asadi";
var fa = Model.IsFa; var fa = Model.IsFa;
} }
<div class="min-h-screen pt-28 pb-20 px-5 sm:px-8"> <div class="px-5 pt-28 pb-24 sm:px-8 sm:pt-32">
<div class="mx-auto max-w-3xl"> <div class="mx-auto max-w-2xl">
<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" : "")"> <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="2" class="@(fa ? "" : "rotate-180")"><path d="M5 12H19"/><path d="M13 6L19 12L13 18"/></svg> <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") @(fa ? "بازگشت به بلاگ" : "Back to blog")
</a> </a>
@if (Model.PostNotFound) @if (Model.PostNotFound)
{ {
<p class="text-slate-400">@(fa ? "مقاله پیدا نشد." : "Post not found.")</p> <p class="text-zinc-600">@(fa ? "مقاله پیدا نشد." : "Post not found.")</p>
} }
else else
{ {
<div class="mb-6"> <header class="mb-8">
<span class="label-mono text-electric">@Model.Category</span> <span class="kicker">@Model.Category</span>
<h1 class="mt-3 font-display text-3xl font-extrabold leading-tight text-white @(fa ? "font-fa" : "")">@Model.Title</h1> <h1 class="mt-3 @(fa ? "font-fa" : "")" style="font-size:clamp(1.8rem,4vw,2.5rem)">@Model.Title</h1>
<p class="mt-2 label-mono text-slate-500">@Model.ReadTime @(fa ? "دقیقه مطالعه" : "min read")</p> <p class="mt-3 text-sm text-zinc-400">@Model.ReadTime @(fa ? "دقیقه مطالعه" : "min read")</p>
</div> </header>
<article class="prose-custom glass p-8"> <article class="prose-custom">
@Html.Raw(Model.BodyHtml) @Html.Raw(Model.BodyHtml)
</article> </article>
} }
</div> </div>
</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>
}
+212 -390
View File
@@ -5,239 +5,117 @@
var locale = Model.Locale; var locale = Model.Locale;
} }
<!-- ─── HERO ─────────────────────────────────────────────────────────── --> <!-- ─── HERO (editorial, centered) ───────────────────────────────────── -->
<section id="top" class="relative isolate overflow-hidden min-h-[100svh] pt-28 pb-20 sm:pt-32"> <section id="top" class="px-5 pt-28 pb-24 sm:px-8 sm:pt-32">
<!-- Particle canvas --> <div class="mx-auto max-w-3xl text-center">
<div class="pointer-events-none absolute inset-0 -z-10"> <p class="kicker reveal">@(fa ? "مهندس هوش مصنوعی، مشاور، معمار راهکار" : "AI Engineer, Consultant, Solution Architect")</p>
<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>
<!-- Aurora background --> <h1 class="reveal mt-6 @(fa ? "font-fa" : "")" style="font-size:clamp(2.6rem,7vw,4.75rem);transition-delay:.05s">
<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">
@(fa ? "سروش اسعدی" : "Soroush Asadi") @(fa ? "سروش اسعدی" : "Soroush Asadi")
</h1> </h1>
<!-- Headline --> <p class="lede reveal mx-auto mt-6 text-balance" style="font-size:clamp(1.05rem,2vw,1.3rem);transition-delay:.1s">
<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">
@(fa @(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> </p>
<!-- CTAs --> <div class="reveal mt-9 flex flex-wrap items-center justify-center gap-3" style="transition-delay:.15s">
<div class="mt-9 flex flex-wrap items-center justify-center gap-3 reveal" style="transition-delay:.5s"> <a href="#contact" class="btn">@(fa ? "رزرو جلسه" : "Book a call")</a>
<a href="#contact" class="btn-primary"> <a href="#portfolio" class="btn-ghost">@(fa ? "نمونه‌کارها" : "View work")</a>
@(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> </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> </div>
</section> </section>
<!-- ─── SERVICES ─────────────────────────────────────────────────────── --> <!-- ─── SERVICES (text-block grid) ───────────────────────────────────── -->
<section id="services" class="relative px-5 py-28 sm:px-8"> <section id="services" class="px-5 py-24 sm:px-8 sm:py-28">
<div class="mx-auto max-w-7xl"> <div class="mx-auto max-w-6xl">
<div class="section-header"> <div class="sec-head">
<div class="eyebrow"><span class="chip">@(fa ? "خدمات" : "Services")</span></div> <h2>@(fa ? "شش حوزه‌ی تخصص" : "Six areas of practice")</h2>
<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>
<p>@(fa ? "از اولین جلسه‌ی راهبرد تا استقرار تولید — یک شریک مهندسی برای کل چرخه‌ی عمر هوش مصنوعی شما." : "From the first strategy session to production rollout — one engineering partner for the full AI lifecycle.")</p>
</div> </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[]{ var services = fa ? new[]{
("strategy","راهبرد و نقشه راه هوش مصنوعی","ارزیابی بلوغ سازمانی، شناسایی موارد کاربری با بیشترین بازده، و طراحی نقشه راه ۱۲۱۸ ماهه با KPIهای روشن.","electric",new[]{"Discovery","ROI Mapping","Roadmap"}), ("strategy","راهبرد و نقشه راه هوش مصنوعی","ارزیابی بلوغ سازمانی، شناسایی موارد کاربری با بیشترین بازده، و طراحی نقشه راه ۱۲ تا ۱۸ ماهه با KPIهای روشن.",new[]{"Discovery","ROI Mapping","Roadmap"}),
("automation","اتوماسیون هوش مصنوعی","ساخت عامل‌های خودکار و گردش‌کارهای n8n که فرایندهای دستی را به سامانه‌های قابل ممیزی تبدیل می‌کنند.","violet",new[]{"n8n","Agents","Workflows"}), ("automation","اتوماسیون هوش مصنوعی","ساخت عامل‌های خودکار و گردش‌کارهای n8n که فرایندهای دستی را به سامانه‌های قابل ممیزی تبدیل می‌کنند.",new[]{"n8n","Agents","Workflows"}),
("llm-rag","مهندسی LLM و RAG","طراحی pipeline‌های RAG با پایگاه‌های برداری، evaluation framework، و سرویس‌دهی با تأخیر زیر ۵۰ میلی‌ثانیه.","magenta",new[]{"RAG","Vector DB","Eval"}), ("llm-rag","مهندسی LLM و RAG","طراحی pipeline‌های RAG با پایگاه‌های برداری، evaluation framework، و سرویس‌دهی با تأخیر زیر ۵۰ میلی‌ثانیه.",new[]{"RAG","Vector DB","Eval"}),
("architecture","معماری راهکار","طراحی سامانه‌های توزیع‌شده روی Kubernetes با میکروسرویس‌ها، event streaming، و الگوهای پایداری در مقیاس بالا.","emerald",new[]{"K8s","Microservices","Event-Driven"}), ("architecture","معماری راهکار","طراحی سامانه‌های توزیع‌شده روی Kubernetes با میکروسرویس‌ها، event streaming، و الگوهای پایداری در مقیاس بالا.",new[]{"K8s","Microservices","Event-Driven"}),
("mobile","اپلیکیشن‌های موبایل هوش مصنوعی","برنامه‌های Flutter، Swift و Kotlin با on-device inference، استریم LLM و تجربه‌ی کاربری بومی.","electric",new[]{"Flutter","Swift","Kotlin"}), ("mobile","اپلیکیشن‌های موبایل هوش مصنوعی","برنامه‌های Flutter، Swift و Kotlin با on-device inference، استریم LLM و تجربه‌ی کاربری بومی.",new[]{"Flutter","Swift","Kotlin"}),
("google-stack","تخصص استک گوگل","استقرار روی Vertex AI، GKE و Gemini با بهینه‌سازی هزینه و الگوهای امنیتی سطح enterprise.","cyan",new[]{"Vertex AI","GKE","Gemini"}), ("google-stack","تخصص استک گوگل","استقرار روی Vertex AI، GKE و Gemini با بهینه‌سازی هزینه و الگوهای امنیتی سطح enterprise.",new[]{"Vertex AI","GKE","Gemini"}),
} : new[]{ } : new[]{
("strategy","AI Strategy & Roadmap","Maturity assessment, highest-ROI use-case discovery, and a 1218 month roadmap with measurable KPIs.","electric",new[]{"Discovery","ROI Mapping","Roadmap"}), ("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.","violet",new[]{"n8n","Agents","Workflows"}), ("automation","AI Automation","Autonomous agents and n8n workflows that turn manual processes into auditable, observable systems.",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"}), ("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.","emerald",new[]{"K8s","Microservices","Event-Driven"}), ("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.","electric",new[]{"Flutter","Swift","Kotlin"}), ("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.","cyan",new[]{"Vertex AI","GKE","Gemini"}), ("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 { <article class="reveal border-t border-zinc-200 pt-6">
"violet" => ("group-hover:border-violet/50", "group-hover:shadow-glow-violet", "text-violet", "border-violet/30 bg-violet/5 text-violet/90"), <span class="text-zinc-400" aria-hidden="true">@Html.Raw(ServiceIcon(id))</span>
"magenta" => ("group-hover:border-magenta/50", "group-hover:shadow-glow-magenta", "text-magenta", "border-magenta/30 bg-magenta/5 text-magenta/90"), <h3 class="mt-5 text-lg font-semibold @(fa ? "font-fa" : "")">@title</h3>
"emerald" => ("group-hover:border-emerald/50", "group-hover:shadow-glow-emerald", "text-emerald", "border-emerald/30 bg-emerald/5 text-emerald/90"), <p class="mt-2.5 text-[.95rem] leading-relaxed text-zinc-600">@desc</p>
"cyan" => ("group-hover:border-cyan/50", "group-hover:shadow-glow-electric","text-cyan", "border-cyan/30 bg-cyan/5 text-cyan/90"), <div class="mt-4 flex flex-wrap gap-1.5">
_ => ("group-hover:border-electric/50","group-hover:shadow-glow-electric","text-electric","border-electric/30 bg-electric/5 text-electric/90"), @foreach (var tag in tags) { <span class="chip">@tag</span> }
};
<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>
</div> </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> </article>
si++;
} }
</div> </div>
</div> </div>
</section> </section>
<!-- ─── DATA FLOW ────────────────────────────────────────────────────── --> <!-- ─── PIPELINE (horizontal stepper) ────────────────────────────────── -->
<section id="dataflow" class="relative overflow-hidden px-5 py-28 sm:px-8"> <section id="dataflow" class="px-5 py-24 sm:px-8 sm:py-28">
<div class="mx-auto max-w-5xl"> <div class="mx-auto max-w-6xl">
<div class="section-header"> <div class="sec-head">
<div class="eyebrow"><span class="chip">@(fa ? "پایپ‌لاین" : "Pipeline")</span></div> <h2>@(fa ? "از سند خام تا پاسخ قابل اتکا" : "From raw document to a trustworthy answer")</h2>
<h2>@(fa ? "از سند خام تا پاسخ قابل اتکا" : "From raw document to 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>
<p>@(fa ? "مسیری که هر پرسش در یک سامانه‌ی RAG تولیدی طی می‌کند — هر مرحله قابل اندازه‌گیری، قابل ممیزی و بهینه‌شده برای تأخیر." : "The path every query takes through a production RAG system — each stage measurable, auditable, and tuned for latency.")</p>
</div> </div>
<!-- Flow diagram -->
<div class="reveal mt-10 overflow-x-auto"> <ol class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-3 lg:grid-cols-5">
<div class="flex min-w-max items-center gap-0 mx-auto w-fit"> @{
@{ var nodes = fa ? new[]{
var nodes = fa ? new[]{ ("دریافت","نرمال‌سازی، قطعه‌بندی و پاک‌سازی اسناد منبع"),
("ingest","دریافت","نرمال‌سازی، قطعه‌بندی و پاک‌سازی اسناد منبع","electric"), ("برداری‌سازی","تولید embedding و نمایه‌سازی در پایگاه برداری"),
("embed","برداری‌سازی","تولید embedding و نمایه‌سازی در پایگاه برداری","violet"), ("بازیابی","جستجوی ترکیبی معنایی و کلیدواژه‌ای"),
("retrieve","بازیابی","جستجوی ترکیبی معنایی و کلیدواژه‌ای","cyan"), ("بازرتبه‌بندی","مرتب‌سازی مجدد نامزدها با cross-encoder"),
("rerank","بازرتبه‌بندی","مرتب‌سازی مجدد نامزدها با cross-encoder","magenta"), ("تولید","پاسخ مستند با ارجاع به منبع"),
("generate","تولید","پاسخ مستند با ارجاع به منبع","emerald"), } : new[]{
} : new[]{ ("Ingest","Normalize, chunk, and clean source documents"),
("ingest","Ingest","Normalize, chunk, and clean source documents","electric"), ("Embed","Generate embeddings and index in the vector store"),
("embed","Embed","Generate embeddings and index in vector store","violet"), ("Retrieve","Hybrid semantic and keyword search"),
("retrieve","Retrieve","Hybrid semantic + keyword search","cyan"), ("Rerank","Re-order candidates with a cross-encoder"),
("rerank","Rerank","Re-order candidates with a cross-encoder","magenta"), ("Generate","Grounded answer with source citations"),
("generate","Generate","Grounded answer with source citations","emerald"), };
}; int stepN = 0;
var colorMap2 = new Dictionary<string,(string border, string text, string bg)>{ }
["electric"] = ("border-electric/40","text-electric","bg-electric/10"), @foreach (var (nlabel, ndesc) in nodes)
["violet"] = ("border-violet/40", "text-violet", "bg-violet/10"), {
["cyan"] = ("border-cyan/40", "text-cyan", "bg-cyan/10"), stepN++;
["magenta"] = ("border-magenta/40", "text-magenta", "bg-magenta/10"), <li class="reveal border-t border-zinc-200 pt-4" style="transition-delay:@((stepN-1) * 40)ms">
["emerald"] = ("border-emerald/40", "text-emerald", "bg-emerald/10"), <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>
@for (int ni = 0; ni < nodes.Length; ni++) </li>
{ }
var (nid, nlabel, ndesc, naccent) = nodes[ni]; </ol>
var (nborder, ntext, nbg) = colorMap2[naccent]; <p class="mt-8 text-sm text-zinc-500">@(fa ? "تأخیر سرتاسری زیر ۵۰ میلی‌ثانیه؛ هر مرحله مشاهده‌پذیر." : "Sub-50ms end-to-end, every stage observable.")</p>
<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>
}
}
</div>
</div>
<p class="mt-8 text-center label-mono text-slate-500 reveal">@(fa ? "تأخیر سرتاسری زیر ۵۰ میلی‌ثانیه · هر مرحله مشاهده‌پذیر" : "Sub-50ms end-to-end · every stage observable")</p>
</div> </div>
</section> </section>
<!-- ─── STACK ─────────────────────────────────────────────────────────── --> <!-- ─── STACK (grouped tag clusters) ─────────────────────────────────── -->
<section id="stack" class="relative px-5 py-28 sm:px-8"> <section id="stack" class="px-5 py-24 sm:px-8 sm:py-28">
<div class="mx-auto max-w-7xl"> <div class="mx-auto max-w-6xl">
<div class="section-header"> <div class="sec-head">
<div class="eyebrow"><span class="chip">@(fa ? "استک" : "Stack")</span></div> <h2>@(fa ? "ابزار روزمره" : "Daily tooling")</h2>
<h2>@(fa ? "ابزارهای روزانه" : "Daily tooling")</h2> <p class="lede">@(fa ? "هر چه می‌سازم بر این پایه‌ها استوار است؛ انتخاب‌شده برای دوام، نه چرخه‌های هیجان." : "Everything I ship sits on this foundation, chosen for longevity, not hype cycles.")</p>
<p>@(fa ? "هر چه ساخته می‌شود از این پایه‌ها بیرون می‌آید — انتخاب‌شده برای عمر طولانی، نه ترند روز." : "Everything I ship sits on this foundation — chosen for longevity, not hype cycles.")</p>
</div> </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[]{ var cats = fa ? new[]{
("زبان‌ها", new[]{"Python","TypeScript","Go","Rust","SQL"}), ("زبان‌ها", new[]{"Python","TypeScript","Go","Rust","SQL"}),
@@ -245,206 +123,152 @@
("زیرساخت", new[]{"Kubernetes","Terraform","Postgres","Redis","Kafka","NATS"}), ("زیرساخت", new[]{"Kubernetes","Terraform","Postgres","Redis","Kafka","NATS"}),
("هوش مصنوعی", new[]{"Vertex AI","Gemini","OpenAI","Anthropic","LangGraph","Pinecone","pgvector"}), ("هوش مصنوعی", new[]{"Vertex AI","Gemini","OpenAI","Anthropic","LangGraph","Pinecone","pgvector"}),
} : new[]{ } : new[]{
("Languages", new[]{"Python","TypeScript","Go","Rust","SQL"}), ("Languages", new[]{"Python","TypeScript","Go","Rust","SQL"}),
("Mobile", new[]{"Flutter","Swift / SwiftUI","Kotlin","React Native"}), ("Mobile", new[]{"Flutter","Swift / SwiftUI","Kotlin","React Native"}),
("Infrastructure",new[]{"Kubernetes","Terraform","Postgres","Redis","Kafka","NATS"}), ("Infrastructure",new[]{"Kubernetes","Terraform","Postgres","Redis","Kafka","NATS"}),
("AI / ML", new[]{"Vertex AI","Gemini","OpenAI","Anthropic","LangGraph","Pinecone","pgvector"}), ("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) @foreach (var (catLabel, items) in cats)
{ {
<div class="glass p-6"> <div class="reveal border-t border-zinc-200 pt-5">
<h3 class="font-display font-semibold text-white mb-4 @catColors[ci2]">@catLabel</h3> <h3 class="mb-4 text-sm font-semibold @(fa ? "font-fa" : "")">@catLabel</h3>
<ul class="space-y-2"> <div class="flex flex-wrap gap-1.5">
@foreach (var item in items) @foreach (var item in items) { <span class="chip">@item</span> }
{ </div>
<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> </div>
ci2++;
} }
</div> </div>
</div> </div>
</section> </section>
<!-- ─── EXPERTISE ────────────────────────────────────────────────────── --> <!-- ─── EXPERTISE (definition list) ──────────────────────────────────── -->
<section id="expertise" class="relative px-5 py-28 sm:px-8"> <section id="expertise" class="px-5 py-24 sm:px-8 sm:py-28">
<div class="mx-auto max-w-3xl"> <div class="mx-auto max-w-4xl">
<div class="section-header"> <div class="sec-head">
<div class="eyebrow"><span class="chip">@(fa ? "تخصص" : "Expertise")</span></div> <h2>@(fa ? "آنچه در آن عمیق می‌شوم" : "What I go deep on")</h2>
<h2>@(fa ? "اعدادی که اهمیت دارند" : "The numbers that matter")</h2> <p class="lede">@(fa ? "سامانه‌هایی که میلیون‌ها رویداد در روز را دوام می‌آورند. این‌ها حوزه‌هایی‌اند که برایشان بهینه می‌کنم." : "Systems that survive millions of events per day. These are the areas I optimize for.")</p>
<p>@(fa ? "سامانه‌هایی که در میلیون‌ها رویداد در روز پایدار می‌مانند — این‌ها معیارهایی هستند که اندازه می‌گیریم." : "Systems that survive millions of events per day — these are the metrics I optimize for.")</p>
</div> </div>
<div class="space-y-6 reveal">
<dl>
@{ @{
var bars = fa ? new[]{ var areas = fa ? new[]{
("مهندسی LLM و RAG", 95), ("مهندسی LLM و RAG","پایپ‌لاین‌های بازیابی، ارزیابی و تولید مستند در محیط تولید."),
("معماری ابری و Kubernetes", 92), ("معماری ابری و Kubernetes","سرویس‌های توزیع‌شده، مقیاس خودکار و پایداری در مقیاس بالا."),
("سیستم‌های عامل‌محور و اتوماسیون", 90), ("سیستم‌های عامل‌محور و اتوماسیون","گردش‌کارهای خودکار قابل ممیزی با n8n و LangGraph."),
("استک گوگل کلود (Vertex / GKE)", 88), ("استک گوگل کلود (Vertex / GKE)","Vertex AI، GKE و Gemini با انضباط هزینه."),
("موبایل بومی و cross-platform", 82), ("موبایل بومی و cross-platform","Flutter، Swift و Kotlin با استنتاج روی دستگاه."),
} : new[]{ } : new[]{
("LLM & RAG engineering", 95), ("LLM and RAG engineering","Retrieval pipelines, evals, and grounded generation in production."),
("Cloud architecture & Kubernetes", 92), ("Cloud architecture and Kubernetes","Distributed services, autoscaling, and resilience at scale."),
("Agentic systems & automation", 90), ("Agentic systems and automation","Auditable autonomous workflows with n8n and LangGraph."),
("Google Cloud stack (Vertex / GKE)", 88), ("Google Cloud stack (Vertex / GKE)","Vertex AI, GKE, and Gemini with real cost discipline."),
("Native + cross-platform mobile", 82), ("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="reveal grid grid-cols-1 gap-1 border-t border-zinc-200 py-5 sm:grid-cols-[1fr_1.5fr] sm:gap-8">
<div class="mb-2 flex items-center justify-between"> <dt class="text-base font-semibold @(fa ? "font-fa" : "")">@alabel</dt>
<span class="text-[.9rem] text-slate-300">@blabel</span> <dd class="text-[.95rem] leading-relaxed text-zinc-600">@adesc</dd>
<span class="label-mono text-slate-400">@bval%</span>
</div>
<div class="bar-track">
<div class="bar-fill @barColors[bi]" data-w="@bval%" style="width:0%"></div>
</div>
</div> </div>
bi++;
} }
</div> </dl>
</div> </div>
</section> </section>
<!-- ─── PORTFOLIO ────────────────────────────────────────────────────── --> <!-- ─── PORTFOLIO (card grid, typographic covers) ────────────────────── -->
<section id="portfolio" class="relative px-5 py-28 sm:px-8"> <section id="portfolio" class="px-5 py-24 sm:px-8 sm:py-28">
<div class="mx-auto max-w-7xl"> <div class="mx-auto max-w-6xl">
<div class="section-header"> <div class="sec-head">
<div class="eyebrow"><span class="chip">@(fa ? "نمونه‌کارها" : "Selected work")</span></div> <h2>@(fa ? "نمونه‌کارهای منتخب" : "Selected work")</h2>
<h2>@(fa ? "سامانه‌هایی که در تولید کار می‌کنند" : "Systems that run in production")</h2> <p class="lede">@(fa ? "گزیده‌ای از پروژه‌های واقعی در حوزه‌ی هوش مصنوعی، داده و موبایل." : "A selection of real engagements across AI, data, and mobile.")</p>
<p>@(fa ? "گزیده‌ای از پروژه‌های واقعی. روی هر کارت بزنید تا جزئیات معماری را ببینید." : "A selection of real engagements. Tap any card for the gallery and architecture details.")</p>
</div> </div>
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
@{ @{
var projects = fa ? new[]{ 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"}), ("atlas-rag","اطلس - پلتفرم RAG سازمانی","بانک ردیف‌اول","۲۰۲۵","دستیار دانش روی بیش از ۴ میلیون سند داخلی؛ بازیابی ترکیبی با pgvector و reranker.",new[]{"RAG","pgvector","Vertex AI"},new[]{("۴M+","سند نمایه‌شده"),("۳۸ms","تأخیر p95"),("۹۲٪","دقت پاسخ")}),
("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"}), ("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 برای تحلیل قفسه و جریان مشتری.","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"}), ("vertex-vision","Vertex Vision - استنتاج بینایی بلادرنگ","زنجیره خرده‌فروشی","۲۰۲۴","استنتاج بینایی بلادرنگ روی GKE با Triton و Vertex AI برای تحلیل قفسه و جریان مشتری.",new[]{"Vertex AI","GKE","Triton"},new[]{("۱.۲B","استنتاج ماهانه"),("۳۰۰+","فروشگاه"),("۶۰٪","کاهش هزینه")}),
("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"}), ("mirage-mobile","Mirage - مجموعه هوش مصنوعی on-device","محصول مصرفی","۲۰۲۴","اپلیکیشن Flutter با استنتاج کاملاً آفلاین با Gemini Nano و LiteRT.",new[]{"Flutter","Gemini Nano","LiteRT"},new[]{("۰","وابستگی شبکه"),("<80ms","پاسخ"),("۴.۸★","امتیاز کاربران")}),
("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"}), ("flux-stream","Flux - مش داده رویدادمحور","پلتفرم لجستیک","۲۰۲۳","ستون استریمینگ روی Kafka و NATS روی Kubernetes؛ ۴۰+ میکروسرویس با الگوهای پایداری.",new[]{"Kafka","NATS","Go"},new[]{("۴۰+","میکروسرویس"),("۲M/s","رویداد بر ثانیه"),("۹۹.۹٪","uptime")}),
("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"}), ("oracle-forecast","Oracle - موتور پیش‌بینی تقاضا","زنجیره تامین","۲۰۲۳","پایپ‌لاین پیش‌بینی سری زمانی روی BigQuery و dbt با بازآموزی خودکار.",new[]{"BigQuery","dbt","MLOps"},new[]{("۲۳٪","کاهش ضایعات"),("۸۹٪","دقت پیش‌بینی"),("روزانه","بازآموزی")}),
} : 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"}), ("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","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"}), ("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","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"}), ("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","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"}), ("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","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"}), ("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","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"}), ("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 { var initial = char.ToUpperInvariant(pid[0]);
"violet" => ("border-violet/30", "text-violet"), <article class="card card-link reveal overflow-hidden">
"cyan" => ("border-cyan/30", "text-cyan"), <div class="flex aspect-[16/9] items-center justify-center bg-zinc-100" aria-hidden="true">
"magenta" => ("border-magenta/30", "text-magenta"), <span class="font-display text-5xl font-bold text-zinc-300">@initial</span>
"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" />
</div> </div>
<div class="mb-3 flex flex-wrap gap-1.5"> <div class="p-5">
@foreach (var tag in ptags) <div class="mb-3 flex flex-wrap gap-1.5">
{ @foreach (var tag in ptags) { <span class="chip">@tag</span> }
<span class="rounded-full border @pborder px-2 py-0.5 font-mono text-[.62rem] uppercase tracking-wider @ptext/80">@tag</span> </div>
} <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>
<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> </div>
<h3 class="font-display font-semibold text-white @(fa ? "font-fa" : "")" style="font-size:clamp(1rem,1.5vw,1.2rem)">@ptitle</h3> </article>
<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">
@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>
</div>
} }
</div> </div>
</div> </div>
</section> </section>
<!-- Portfolio modal --> <!-- ─── BLOG (editorial list) ────────────────────────────────────────── -->
<div id="portfolio-modal" class="fixed inset-0 z-[100] hidden" role="dialog" aria-modal="true"> <section id="blog" class="px-5 py-24 sm:px-8 sm:py-28">
<div id="modal-overlay" class="absolute inset-0 bg-black/80 backdrop-blur-sm"></div> <div class="mx-auto max-w-4xl">
<div class="relative z-10 flex h-full items-center justify-center p-4"> <div class="sec-head">
<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>
<h2>@(fa ? "یادداشت‌های مهندسی" : "Engineering notes")</h2> <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>
<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[]{ 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), ("agentic-n8n-patterns","Automation","الگوهای عامل‌محور با n8n برای سازمان","چگونه n8n را با LangGraph ترکیب کنیم تا گردش‌کارهای قابل ممیزی بسازیم.",11),
("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه ضدالگو که در ۸۰٪ پروژه‌های Vertex می‌بینم، و چگونه ۶۰٪ هزینه را کاهش دادیم.",6), ("vertex-cost-control","Google Stack","کنترل هزینه روی Vertex AI در مقیاس بالا","سه ضدالگو که در ۸۰٪ پروژه‌های Vertex می‌بینم، و چگونه ۶۰٪ هزینه را کاهش دادیم.",6),
("k8s-llm-inference","Infra","استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ میلی‌ثانیه","الگوی استقرار با KEDA، GPU sharing، و request hedging برای سرویس‌دهی پایدار.",14), ("k8s-llm-inference","Infra","استنتاج LLM روی Kubernetes با تأخیر زیر ۵۰ میلی‌ثانیه","الگوی استقرار با KEDA، GPU sharing، و request hedging برای سرویس‌دهی پایدار.",14),
("flutter-on-device-ai","Mobile","هوش مصنوعی on-device در Flutter","استفاده از Gemini Nano و LiteRT برای استنتاج آفلاین در اپلیکیشن‌های موبایل.",9), ("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[]{ } : 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), ("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), ("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), ("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), ("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) @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"> <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">
<span class="label-mono text-electric mb-3 block">@cat</span> <div class="flex items-baseline justify-between sm:flex-col sm:gap-1">
<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> <span class="kicker">@cat</span>
<p class="mt-3 text-[.88rem] leading-relaxed text-slate-400 line-clamp-3">@excerpt</p> <span class="text-[.78rem] text-zinc-400">@readTime @(fa ? "دقیقه" : "min")</span>
<div class="mt-4 flex items-center justify-between"> </div>
<span class="label-mono">@readTime @(fa ? "دقیقه" : "min") @(fa ? "ادامه" : "read")</span> <div>
<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> <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> </div>
</a> </a>
} }
@@ -452,37 +276,35 @@
</div> </div>
</section> </section>
<!-- ─── CONTACT ──────────────────────────────────────────────────────── --> <!-- ─── CONTACT ──────────────────────────────────────────────────────── -->
<section id="contact" class="relative px-5 py-28 sm:px-8"> <section id="contact" class="px-5 py-24 sm:px-8 sm:py-28">
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-2xl">
<div class="section-header"> <div class="sec-head">
<div class="eyebrow"><span class="chip">@(fa ? "تماس" : "Contact")</span></div> <h2>@(fa ? "رزرو یک جلسه‌ی ۳۰ دقیقه‌ای" : "Book a 30-minute call")</h2>
<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>
<p>@(fa ? "بدون هزینه، بدون تعهد. موارد کاربردی، محدودیت‌ها و گام بعدی را با هم بررسی می‌کنیم." : "No cost, no commitment. We map the use case, the constraints, and the next step together.")</p>
</div> </div>
<form id="contact-form" <form id="contact-form" class="card space-y-5 p-6 sm:p-8"
class="glass p-8 space-y-5" data-success-msg="@(fa ? "پیام ارسال شد. معمولاً ظرف ۲۴ ساعت کاری پاسخ می‌دهم." : "Sent. Typical reply within 24 working hours.")"
data-success-msg="@(fa ? "پیام ارسال شد. معمولاً ظرف ۲۴ ساعت کاری پاسخ می‌دهم." : "Sent! Typical reply within 24 working hours.")"
data-error-msg="@(fa ? "خطایی رخ داد. لطفاً دوباره امتحان کنید." : "Something went wrong. Please try again.")"> data-error-msg="@(fa ? "خطایی رخ داد. لطفاً دوباره امتحان کنید." : "Something went wrong. Please try again.")">
<input type="hidden" name="locale" value="@locale" /> <input type="hidden" name="locale" value="@locale" />
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div> <div>
<label class="label-mono mb-2 block" for="name">@(fa ? "نام" : "Name")</label> <label class="flabel" 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" /> <input id="name" name="name" type="text" required placeholder="@(fa ? "نام و نام خانوادگی" : "Full name")" class="field" />
</div> </div>
<div> <div>
<label class="label-mono mb-2 block" for="company">@(fa ? "سازمان" : "Company")</label> <label class="flabel" 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" /> <input id="company" name="company" type="text" placeholder="@(fa ? "نام سازمان" : "Organization")" class="field" />
</div> </div>
</div> </div>
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div> <div>
<label class="label-mono mb-2 block" for="service">@(fa ? "خدمت" : "Service")</label> <label class="flabel" 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"> <select id="service" name="service" required class="field">
<option value="" disabled selected></option> <option value="" disabled selected>@(fa ? "انتخاب کنید" : "Select…")</option>
@if (fa) @if (fa)
{ {
<option value="strategy">راهبرد و نقشه راه</option> <option value="strategy">راهبرد و نقشه راه</option>
@@ -494,9 +316,9 @@
} }
else else
{ {
<option value="strategy">AI Strategy & Roadmap</option> <option value="strategy">AI Strategy and Roadmap</option>
<option value="automation">AI Automation</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="architecture">Solution Architecture</option>
<option value="mobile">Mobile AI Apps</option> <option value="mobile">Mobile AI Apps</option>
<option value="google-stack">Google Stack</option> <option value="google-stack">Google Stack</option>
@@ -504,47 +326,47 @@
</select> </select>
</div> </div>
<div> <div>
<label class="label-mono mb-2 block" for="budget">@(fa ? "بودجه (تقریبی)" : "Budget (rough)")</label> <label class="flabel" 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"> <select id="budget" name="budget" required class="field">
<option value="" disabled selected></option> <option value="" disabled selected>@(fa ? "انتخاب کنید" : "Select…")</option>
<option>Under $10k</option> <option>Under $10k</option>
<option>$10k$50k</option> <option>$10k - $50k</option>
<option>$50k$200k</option> <option>$50k - $200k</option>
<option>$200k+</option> <option>$200k+</option>
</select> </select>
</div> </div>
</div> </div>
<div> <div>
<label class="label-mono mb-2 block" for="message">@(fa ? "پیام" : "Message")</label> <label class="flabel" 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> <textarea id="message" name="message" required rows="4" placeholder="@(fa ? "هدف، بازه زمانی، موانع فعلی…" : "Goal, timeline, current blockers…")" class="field resize-none"></textarea>
</div> </div>
<button type="submit" class="btn-primary w-full justify-center"> <button type="submit" class="btn w-full">@(fa ? "ارسال درخواست" : "Send request")</button>
@(fa ? "ارسال درخواست" : "Send request") <p id="contact-status" class="mt-1 text-sm text-zinc-500">@(fa ? "معمولاً ظرف ۲۴ ساعت کاری پاسخ می‌دهم." : "Typical reply within 24 working hours.")</p>
</button>
<p id="contact-status" class="text-center text-sm text-slate-500">@(fa ? "معمولاً ظرف ۲۴ ساعت کاری پاسخ می‌دهم." : "Typical reply within 24 working hours.")</p>
</form> </form>
</div> </div>
</section> </section>
<!-- ─── FOOTER ───────────────────────────────────────────────────────── --> <!-- ─── FOOTER ───────────────────────────────────────────────────────── -->
<footer class="border-t border-white/5 px-5 py-10 sm:px-8"> <footer class="border-t border-zinc-200 px-5 py-10 sm:px-8">
<div class="mx-auto flex max-w-7xl flex-col items-center gap-3 text-center"> <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">
<img src="/logo-mark.svg" alt="" width="24" height="24" /> <div class="flex items-center gap-2.5">
<p class="label-mono">@(fa ? "طراحی‌شده در تهران · ساخته‌شده برای سازمان‌ها" : "Designed in Tehran · Built for the enterprise")</p> <img src="/logo-mark.svg" alt="" width="22" height="22" />
<p class="text-[.78rem] text-slate-600">© 2026 Soroush Asadi. @(fa ? "تمام حقوق محفوظ است." : "All rights reserved.")</p> <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> </div>
</footer> </footer>
@functions { @functions {
static string ServiceIcon(string id) => id switch { 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>""", "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="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>""", "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="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>""", "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="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>""", "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="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>""", "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="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>""", "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="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="10"/></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
View File
@@ -4,8 +4,8 @@
var dir = isRtl ? "rtl" : "ltr"; var dir = isRtl ? "rtl" : "ltr";
var lang = locale == "fa" ? "fa" : "en"; var lang = locale == "fa" ? "fa" : "en";
var title = (string?)ViewData["Title"] ?? (locale == "fa" var title = (string?)ViewData["Title"] ?? (locale == "fa"
? "سروش اسعدی مهندس هوش مصنوعی، مشاور، معمار راهکار" ? "سروش اسعدی - مهندس هوش مصنوعی، مشاور، معمار راهکار"
: "Soroush Asadi AI Engineer, Consultant, Solution Architect"); : "Soroush Asadi - AI Engineer, Consultant, Solution Architect");
} }
<!doctype html> <!doctype html>
<html lang="@lang" dir="@dir"> <html lang="@lang" dir="@dir">
@@ -14,89 +14,53 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>@title</title> <title>@title</title>
<meta name="description" content="@(locale == "fa" <meta name="description" content="@(locale == "fa"
? "طراحی و پیاده‌سازی سامانه‌های هوش مصنوعی در مقیاس سازمانی راهبرد، LLM و RAG، اتوماسیون عامل‌محور، زیرساخت ابری و استک گوگل." ? "طراحی و پیاده‌سازی سامانه‌های هوش مصنوعی در مقیاس سازمانی. راهبرد، LLM و RAG، اتوماسیون عامل‌محور، زیرساخت ابری و استک گوگل."
: "Designing and deploying enterprise-grade AI systems — strategy, LLM & RAG, agentic automation, cloud infrastructure, and Google Stack.")" /> : "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> <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:'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:'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> </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 src="https://cdn.tailwindcss.com"></script>
<script> <script>
tailwind.config = { tailwind.config = {
theme: { theme: {
extend: { extend: {
colors: { colors: { accent: '#2563eb', accentink: '#1d4ed8' },
base: { DEFAULT:'#020510', 800:'#050a1a', 700:'#0a1224', 600:'#0f1b33' },
electric:'#38bdf8',
violet: '#818cf8',
magenta: '#e879f9',
emerald: '#34d399',
cyan: '#22d3ee',
},
fontFamily: { fontFamily: {
sans: ['Syne','Vazirmatn','VazirmatnLat','system-ui','sans-serif'], display: ['Syne', 'system-ui', 'sans-serif'],
display: ['Syne','Vazirmatn','sans-serif'], fa: ['Vazirmatn', 'system-ui', '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)',
}, },
} }
} }
} }
</script> </script>
<!-- Site CSS -->
<link rel="stylesheet" href="/css/site.css" /> <link rel="stylesheet" href="/css/site.css" />
<link rel="icon" href="/logo-mark.svg" type="image/svg+xml" /> <link rel="icon" href="/logo-mark.svg" type="image/svg+xml" />
</head> </head>
<body class="bg-base text-slate-200 antialiased"> <body class="site antialiased">
<!-- Custom cursor (desktop only) --> <!-- Sentinel for the navbar border (observed by IntersectionObserver) -->
<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="nav-sentinel" 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>
<!-- Navbar --> <!-- Navbar -->
<header id="navbar" class="fixed inset-x-0 top-0 z-50 transition-all duration-300"> <header id="navbar" class="fixed inset-x-0 top-0 z-50">
<div class="mx-auto flex max-w-7xl items-center justify-between px-5 py-4 sm:px-8"> <div class="mx-auto flex max-w-6xl items-center justify-between px-5 py-3.5 sm:px-8">
<!-- Logo --> <!-- Logo -->
<a href="/#top" class="flex items-center gap-2.5" aria-label="Home"> <a href="/#top" class="flex items-center gap-2.5" aria-label="@(locale == "fa" ? "خانه" : "Home")">
<img src="/logo-mark.svg" alt="" width="28" height="28" class="h-7 w-7" /> <img src="/logo-mark.svg" alt="" width="26" height="26" class="h-[26px] w-[26px]" />
<span class="font-display font-bold text-white @(isRtl ? "font-fa" : "")"> <span class="font-display text-[15px] font-bold text-zinc-900 @(isRtl ? "font-fa" : "")">
@(locale == "fa" ? "سروش اسعدی" : "Soroush Asadi") @(locale == "fa" ? "سروش اسعدی" : "Soroush Asadi")
</span> </span>
</a> </a>
<!-- Desktop nav --> <!-- 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") @if (locale == "fa")
{ {
<a href="/#services" class="nav-link">خدمات</a> <a href="/#services" class="nav-link">خدمات</a>
@@ -104,7 +68,6 @@
<a href="/#expertise" class="nav-link">تخصص</a> <a href="/#expertise" class="nav-link">تخصص</a>
<a href="/#portfolio" class="nav-link">نمونه‌کارها</a> <a href="/#portfolio" class="nav-link">نمونه‌کارها</a>
<a href="/#blog" class="nav-link">بلاگ</a> <a href="/#blog" class="nav-link">بلاگ</a>
<a href="/#contact" class="nav-link">تماس</a>
} }
else else
{ {
@@ -113,31 +76,26 @@
<a href="/#expertise" class="nav-link">Expertise</a> <a href="/#expertise" class="nav-link">Expertise</a>
<a href="/#portfolio" class="nav-link">Portfolio</a> <a href="/#portfolio" class="nav-link">Portfolio</a>
<a href="/#blog" class="nav-link">Blog</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"> <a href="/#contact" class="btn text-sm">@(locale == "fa" ? "رزرو جلسه" : "Book a call")</a>
@(locale == "fa" ? "رزرو جلسه" : "Book a call")
</a>
<!-- Locale toggle --> <!-- Locale toggle -->
<form method="post" action="/locale"> <form method="post" action="/locale">
<input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" /> <input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" />
<input type="hidden" name="returnUrl" value="@Context.Request.Path@Context.Request.QueryString" /> <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"> <button type="submit" class="nav-link text-xs tracking-wide">@(locale == "fa" ? "EN" : "FA")</button>
@(locale == "fa" ? "EN" : "FA")
</button>
</form> </form>
</nav> </nav>
<!-- Mobile menu button --> <!-- Mobile menu button -->
<button id="menu-btn" class="flex flex-col gap-1.5 p-2 md:hidden" aria-label="Menu"> <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-slate-300 transition-all"></span> <span class="block h-0.5 w-5 bg-zinc-800"></span>
<span class="block h-0.5 w-5 bg-slate-300 transition-all"></span> <span class="block h-0.5 w-5 bg-zinc-800"></span>
<span class="block h-0.5 w-5 bg-slate-300 transition-all"></span> <span class="block h-0.5 w-5 bg-zinc-800"></span>
</button> </button>
</div> </div>
<!-- Mobile drawer --> <!-- 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"> <nav class="flex flex-col gap-1 px-5 py-4">
@if (locale == "fa") @if (locale == "fa")
{ {
@@ -160,9 +118,7 @@
<form method="post" action="/locale" class="mt-2"> <form method="post" action="/locale" class="mt-2">
<input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" /> <input type="hidden" name="locale" value="@(locale == "fa" ? "en" : "fa")" />
<input type="hidden" name="returnUrl" value="@Context.Request.Path@Context.Request.QueryString" /> <input type="hidden" name="returnUrl" value="@Context.Request.Path@Context.Request.QueryString" />
<button type="submit" class="label-mono text-slate-400"> <button type="submit" class="nav-link text-xs tracking-wide">@(locale == "fa" ? "Switch to English" : "تغییر به فارسی")</button>
@(locale == "fa" ? "Switch to English" : "تغییر به فارسی")
</button>
</form> </form>
</nav> </nav>
</div> </div>
@@ -172,7 +128,6 @@
@RenderBody() @RenderBody()
</main> </main>
<!-- Scripts -->
<script src="/js/app.js" defer></script> <script src="/js/app.js" defer></script>
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</body> </body>
+127 -89
View File
@@ -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 { :root {
--bg: #020510; --bg: #fafafa;
--electric: #38bdf8; --surface: #ffffff;
--violet: #818cf8; --text: #18181b; /* zinc-900 */
--magenta: #e879f9; --text-2: #52525b; /* zinc-600 */
--emerald: #34d399; --text-3: #a1a1aa; /* zinc-400 */
--cyan: #22d3ee; --line: #e4e4e7; /* zinc-200 */
--radius: 14px; --line-strong: #d4d4d8; /* zinc-300 */
color-scheme: dark; --accent: #2563eb; /* blue-600 - the single accent */
--accent-ink: #1d4ed8; /* blue-700 */
--accent-weak: #eff4ff;
--radius: 8px;
} }
html, body { * { box-sizing: border-box; }
background: var(--bg); html { scroll-behavior: smooth; background: var(--bg); }
font-feature-settings: 'ss01','cv11';
-webkit-font-smoothing: antialiased; ::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 { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: #050a1a; } ::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: linear-gradient(180deg,#38bdf8,#818cf8); border-radius:999px; border:2px solid #050a1a; } ::-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 { .site h1, .site h2, .site h3, .site h4 {
background: linear-gradient(180deg,rgba(255,255,255,.04) 0%,rgba(255,255,255,.015) 100%); font-family: 'Syne', system-ui, sans-serif;
border: 1px solid rgba(56,189,248,.14); font-weight: 700; color: var(--text);
backdrop-filter: blur(14px); letter-spacing: -0.02em; line-height: 1.12;
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); [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 { /* ─── Navigation ─────────────────────────────────────────────────────── */
display: inline-flex; align-items: center; gap: .5rem; .nav-link { font-size: .9rem; color: var(--text-2); transition: color .18s ease; }
padding: .35rem .75rem; border-radius: 999px; .nav-link:hover { color: var(--text); }
border: 1px solid rgba(52,211,153,.25); background: rgba(52,211,153,.06); #navbar { transition: background .25s ease, border-color .25s ease; border-bottom: 1px solid transparent; }
color: #a7f3d0; #navbar.scrolled { background: rgba(250,250,250,.82); backdrop-filter: blur(12px); border-bottom-color: var(--line); }
font-family: 'SpaceMono',ui-monospace,monospace;
font-size: .72rem; letter-spacing: .04em; text-transform: uppercase;
}
.label-mono { /* ─── Buttons (one shape: 8px radius, no pills, no glow) ──────────────── */
font-family: 'SpaceMono',ui-monospace,monospace; .btn, .btn-primary {
font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: #94a3b8; 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;
} }
.btn:hover, .btn-primary:hover { background: var(--accent-ink); border-color: var(--accent-ink); }
.gradient-text { .btn:active, .btn-primary:active { transform: translateY(1px); }
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-ghost { .btn-ghost {
display: inline-flex; align-items: center; gap: .6rem; display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
padding: .8rem 1.35rem; border-radius: 999px; padding: .7rem 1.15rem; border-radius: var(--radius);
font-weight: 500; color: #e2e8f0; font-weight: 500; font-size: .92rem;
border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.02); color: var(--text); background: transparent; border: 1px solid var(--line-strong);
transition: border-color .25s ease, background .25s ease, transform .25s ease; 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 { /* ─── Type helpers ───────────────────────────────────────────────────── */
font-size: .875rem; color: #94a3b8; .kicker {
transition: color .2s ease; text-decoration: none; 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 ─────────────────────────────────────────────── */ /* ─── Neutral tag chip (one shape) ───────────────────────────────────── */
.section-header { text-align: center; margin-bottom: 3.5rem; } .chip {
.section-header .eyebrow { margin-bottom: 1rem; } display: inline-flex; align-items: center;
.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; } border: 1px solid var(--line); border-radius: 6px;
[dir='rtl'] .section-header h2 { font-family:'Vazirmatn',sans-serif; } padding: .2rem .55rem; font-size: .72rem; color: var(--text-2);
.section-header p { max-width:42rem; margin:0 auto; color:#94a3b8; font-size:1.0625rem; line-height:1.7; } background: #fff; white-space: nowrap;
}
/* ─── Service cards ──────────────────────────────────────────────── */ /* ─── Surface card (portfolio, contact) ──────────────────────────────── */
.service-card { transition: border-color .3s, box-shadow .3s, transform .3s; } .card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.service-card:hover { transform: translateY(-2px); } .card-link { transition: border-color .18s ease, transform .18s ease; }
.card-link:hover { border-color: var(--line-strong); transform: translateY(-2px); }
/* ─── Expertise bars ─────────────────────────────────────────────── */ /* ─── Form fields ────────────────────────────────────────────────────── */
.bar-track { height: .375rem; background: rgba(255,255,255,.07); border-radius:999px; overflow:hidden; } .flabel { display: block; font-size: .85rem; font-weight: 500; color: var(--text-2); margin-bottom: .4rem; }
.bar-fill { height: 100%; border-radius:999px; transform-origin:left; } .field {
[dir='rtl'] .bar-fill { transform-origin:right; } 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 ──────────────────────────────────────────────── */ /* ─── Scroll reveal (MOTION_INTENSITY 3: subtle, transform+opacity only) ─ */
.reveal { opacity: 0; transform: translateY(24px); transition: opacity .7s cubic-bezier(.22,1,.36,1), transform .7s cubic-bezier(.22,1,.36,1); } .reveal { opacity: 0; transform: translateY(14px); transition: opacity .6s ease, transform .6s ease; }
.reveal.visible { opacity: 1; transform: none; } .reveal.visible { opacity: 1; transform: none; }
/* ─── Portfolio modal ────────────────────────────────────────────── */ /* ─── Blog prose ─────────────────────────────────────────────────────── */
#portfolio-modal { transition: opacity .25s ease; } .prose-custom { color: var(--text-2); line-height: 1.8; font-size: 1.02rem; }
#portfolio-modal.hidden { pointer-events: none; } .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 { .admin-nav-link {
display: block; padding: .5rem .75rem; border-radius: .5rem; display: block; padding: .5rem .75rem; border-radius: .5rem;
color: #94a3b8; font-size: .875rem; text-decoration: none; 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:hover { background: rgba(255,255,255,.05); color: #e2e8f0; }
.admin-nav-link.active { background: rgba(56,189,248,.1); color: #38bdf8; } .admin-nav-link.active { background: rgba(56,189,248,.1); color: #38bdf8; }
/* ─── Reduced motion ─────────────────────────────────────────────── */ /* ─── Reduced motion ─────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
*,*::before,*::after { *, *::before, *::after {
animation-duration: .001ms !important; animation-iteration-count: 1 !important; animation-duration: .001ms !important; animation-iteration-count: 1 !important;
transition-duration: .001ms !important; scroll-behavior: auto !important; transition-duration: .001ms !important; scroll-behavior: auto !important;
} }
.reveal { opacity: 1 !important; transform: none !important; }
} }
+36 -215
View File
@@ -1,257 +1,78 @@
/* ─── Custom cursor ───────────────────────────────────────────────────── */ /* ════════════════════════════════════════════════════════════════════════
(function () { Minimal interactions. MOTION_INTENSITY 3: hover + a single subtle reveal.
const ring = document.getElementById('cursor'); No custom cursor, no particle canvas, no typewriter, no scroll listeners.
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)');
});
})();
/* ─── Navbar scroll shadow ────────────────────────────────────────────── */ /* ─── Navbar border on scroll (IntersectionObserver, not a scroll listener) */
(function () { (function () {
const nav = document.getElementById('navbar'); const nav = document.getElementById('navbar');
if (!nav) return; const sentinel = document.getElementById('nav-sentinel');
const update = () => nav.classList.toggle('scrolled', window.scrollY > 30); if (!nav || !sentinel) return;
window.addEventListener('scroll', update, { passive: true }); const io = new IntersectionObserver(
update(); ([entry]) => nav.classList.toggle('scrolled', !entry.isIntersecting),
{ threshold: 0 }
);
io.observe(sentinel);
})(); })();
/* ─── Mobile menu toggle ─────────────────────────────────────────────── */ /* ─── Mobile menu toggle ─────────────────────────────────────────────── */
(function () { (function () {
const btn = document.getElementById('menu-btn'); const btn = document.getElementById('menu-btn');
const menu = document.getElementById('mobile-menu'); const menu = document.getElementById('mobile-menu');
if (!btn || !menu) return; if (!btn || !menu) return;
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const open = menu.classList.toggle('hidden'); const hidden = menu.classList.toggle('hidden');
btn.setAttribute('aria-expanded', String(!open)); btn.setAttribute('aria-expanded', String(!hidden));
}); });
menu.querySelectorAll('a').forEach(a => a.addEventListener('click', () => menu.classList.add('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 () { (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) => { 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 => { entries.forEach(e => {
if (!e.isIntersecting) return; if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); }
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);
}); });
}, { threshold: 0.5 }); }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
document.querySelectorAll('.counter').forEach(el => io.observe(el)); els.forEach(el => io.observe(el));
})(); })();
/* ─── Expertise bars (animate on scroll) ─────────────────────────────── */ /* ─── Contact form (AJAX, JSON → /contact) ───────────────────────────── */
(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) ─────────────────────────────────────────────── */
(function () { (function () {
const form = document.getElementById('contact-form'); const form = document.getElementById('contact-form');
if (!form) return; if (!form) return;
const status = document.getElementById('contact-status'); const status = document.getElementById('contact-status');
form.addEventListener('submit', async e => { form.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const data = Object.fromEntries(new FormData(form)); const data = Object.fromEntries(new FormData(form));
const btn = form.querySelector('[type="submit"]'); 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 { try {
const res = await fetch('/contact', { const res = await fetch('/contact', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) body: JSON.stringify(data),
}); });
if (res.ok) { 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(); form.reset();
} else { } 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 { } 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 { } finally {
btn.disabled = false; if (btn) btn.disabled = false;
} }
}); });
})(); })();
+4
View File
@@ -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