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>
}
+195 -373
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"), ("دریافت","نرمال‌سازی، قطعه‌بندی و پاک‌سازی اسناد منبع"),
("embed","برداری‌سازی","تولید embedding و نمایه‌سازی در پایگاه برداری","violet"), ("برداری‌سازی","تولید embedding و نمایه‌سازی در پایگاه برداری"),
("retrieve","بازیابی","جستجوی ترکیبی معنایی و کلیدواژه‌ای","cyan"), ("بازیابی","جستجوی ترکیبی معنایی و کلیدواژه‌ای"),
("rerank","بازرتبه‌بندی","مرتب‌سازی مجدد نامزدها با cross-encoder","magenta"), ("بازرتبه‌بندی","مرتب‌سازی مجدد نامزدها با cross-encoder"),
("generate","تولید","پاسخ مستند با ارجاع به منبع","emerald"), ("تولید","پاسخ مستند با ارجاع به منبع"),
} : new[]{ } : new[]{
("ingest","Ingest","Normalize, chunk, and clean source documents","electric"), ("Ingest","Normalize, chunk, and clean source documents"),
("embed","Embed","Generate embeddings and index in vector store","violet"), ("Embed","Generate embeddings and index in the vector store"),
("retrieve","Retrieve","Hybrid semantic + keyword search","cyan"), ("Retrieve","Hybrid semantic and keyword search"),
("rerank","Rerank","Re-order candidates with a cross-encoder","magenta"), ("Rerank","Re-order candidates with a cross-encoder"),
("generate","Generate","Grounded answer with source citations","emerald"), ("Generate","Grounded answer with source citations"),
};
var colorMap2 = new Dictionary<string,(string border, string text, string bg)>{
["electric"] = ("border-electric/40","text-electric","bg-electric/10"),
["violet"] = ("border-violet/40", "text-violet", "bg-violet/10"),
["cyan"] = ("border-cyan/40", "text-cyan", "bg-cyan/10"),
["magenta"] = ("border-magenta/40", "text-magenta", "bg-magenta/10"),
["emerald"] = ("border-emerald/40", "text-emerald", "bg-emerald/10"),
}; };
int stepN = 0;
} }
@for (int ni = 0; ni < nodes.Length; ni++) @foreach (var (nlabel, ndesc) in nodes)
{ {
var (nid, nlabel, ndesc, naccent) = nodes[ni]; stepN++;
var (nborder, ntext, nbg) = colorMap2[naccent]; <li class="reveal border-t border-zinc-200 pt-4" style="transition-delay:@((stepN-1) * 40)ms">
<div class="flex flex-col items-center"> <span class="font-display text-sm text-zinc-400">@stepN.ToString("D2")</span>
<div class="glass @nborder @nbg flex flex-col items-center gap-2 rounded-2xl border p-5 text-center w-40"> <h3 class="mt-2 text-base font-semibold @(fa ? "font-fa" : "")">@nlabel</h3>
<span class="label-mono @ntext">@nlabel</span> <p class="mt-1.5 text-[.85rem] leading-relaxed text-zinc-600">@ndesc</p>
<p class="text-[.72rem] leading-snug text-slate-400">@ndesc</p> </li>
</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>
} }
} </ol>
</div> <p class="mt-8 text-sm text-zinc-500">@(fa ? "تأخیر سرتاسری زیر ۵۰ میلی‌ثانیه؛ هر مرحله مشاهده‌پذیر." : "Sub-50ms end-to-end, every stage observable.")</p>
</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"}),
@@ -250,201 +128,147 @@
("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>
<div class="bar-track">
<div class="bar-fill @barColors[bi]" data-w="@bval%" style="width:0%"></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="p-5">
<div class="mb-3 flex flex-wrap gap-1.5"> <div class="mb-3 flex flex-wrap gap-1.5">
@foreach (var tag in ptags) @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> </div>
<h3 class="font-display font-semibold text-white @(fa ? "font-fa" : "")" style="font-size:clamp(1rem,1.5vw,1.2rem)">@ptitle</h3> <h3 class="text-[1.05rem] font-semibold @(fa ? "font-fa" : "")">@ptitle</h3>
<p class="mt-1 text-[.82rem] text-slate-400">@pclient · @pyear</p> <p class="mt-1 text-[.8rem] text-zinc-500">@pclient · @pyear</p>
<div class="mt-4 grid grid-cols-3 gap-3 border-t border-white/5 pt-4"> <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) @foreach (var (mv, ml) in pmetrics)
{ {
<div class="text-center"> <div>
<div class="font-display font-bold @ptext text-lg">@mv</div> <div class="font-display text-base font-bold text-zinc-900">@mv</div>
<div class="text-[.68rem] text-slate-500">@ml</div> <div class="mt-0.5 text-[.68rem] leading-tight text-zinc-500">@ml</div>
</div> </div>
} }
</div> </div>
</div> </div>
</article>
} }
</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"> <h2>@(fa ? "یادداشت‌های مهندسی" : "Engineering notes")</h2>
<div class="mb-4 flex items-start justify-between gap-4"> <p class="lede">@(fa ? "یافته‌ها از پروژه‌های واقعی. نه ترجمه‌ی مقاله، نه فهرست هیجان." : "Findings from real engagements. Not translated articles, not hype lists.")</p>
<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> </div>
<!-- ─── BLOG ──────────────────────────────────────────────────────────── --> <div class="border-b border-zinc-200">
<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>
<p>@(fa ? "یافته‌ها از پروژه‌های واقعی — نه ترجمه‌ی مقاله، نه فهرست hype." : "Findings from real engagements — not translated articles, not hype lists.")</p>
</div>
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
@{ @{
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>
+134 -96
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;
} }
/* ─── Navigation ─────────────────────────────────────────────────────── */
.nav-link { font-size: .9rem; color: var(--text-2); transition: color .18s ease; }
.nav-link:hover { color: var(--text); }
#navbar { transition: background .25s ease, border-color .25s ease; border-bottom: 1px solid transparent; }
#navbar.scrolled { background: rgba(250,250,250,.82); backdrop-filter: blur(12px); border-bottom-color: var(--line); }
/* ─── Buttons (one shape: 8px radius, no pills, no glow) ──────────────── */
.btn, .btn-primary {
display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
padding: .7rem 1.15rem; border-radius: var(--radius);
font-weight: 600; font-size: .92rem;
background: var(--accent); color: #fff; border: 1px solid var(--accent);
transition: background .18s ease, transform .12s ease;
}
.btn:hover, .btn-primary:hover { background: var(--accent-ink); border-color: var(--accent-ink); }
.btn:active, .btn-primary:active { transform: translateY(1px); }
.btn-ghost {
display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
padding: .7rem 1.15rem; border-radius: var(--radius);
font-weight: 500; font-size: .92rem;
color: var(--text); background: transparent; border: 1px solid var(--line-strong);
transition: border-color .18s ease, background .18s ease, transform .12s ease;
}
.btn-ghost:hover { border-color: var(--text); background: #fff; }
.btn-ghost:active { transform: translateY(1px); }
/* ─── Type helpers ───────────────────────────────────────────────────── */
.kicker {
font-size: .76rem; letter-spacing: .14em; text-transform: uppercase;
color: var(--text-3); font-weight: 500;
}
[dir='rtl'] .kicker { letter-spacing: .06em; }
.lede { color: var(--text-2); font-size: 1.05rem; line-height: 1.7; max-width: 44rem; }
.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; }
/* ─── Neutral tag chip (one shape) ───────────────────────────────────── */
.chip { .chip {
display: inline-flex; align-items: center; gap: .5rem; display: inline-flex; align-items: center;
padding: .35rem .75rem; border-radius: 999px; border: 1px solid var(--line); border-radius: 6px;
border: 1px solid rgba(52,211,153,.25); background: rgba(52,211,153,.06); padding: .2rem .55rem; font-size: .72rem; color: var(--text-2);
color: #a7f3d0; background: #fff; white-space: nowrap;
font-family: 'SpaceMono',ui-monospace,monospace;
font-size: .72rem; letter-spacing: .04em; text-transform: uppercase;
} }
/* ─── Surface card (portfolio, contact) ──────────────────────────────── */
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.card-link { transition: border-color .18s ease, transform .18s ease; }
.card-link:hover { border-color: var(--line-strong); transform: translateY(-2px); }
/* ─── Form fields ────────────────────────────────────────────────────── */
.flabel { display: block; font-size: .85rem; font-weight: 500; color: var(--text-2); margin-bottom: .4rem; }
.field {
width: 100%; font-family: inherit; font-size: .92rem; color: var(--text);
background: #fff; border: 1px solid var(--line-strong); border-radius: var(--radius);
padding: .7rem .85rem; transition: border-color .18s ease, box-shadow .18s ease;
}
.field::placeholder { color: var(--text-3); }
.field:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
/* ─── Scroll reveal (MOTION_INTENSITY 3: subtle, transform+opacity only) ─ */
.reveal { opacity: 0; transform: translateY(14px); transition: opacity .6s ease, transform .6s ease; }
.reveal.visible { opacity: 1; transform: none; }
/* ─── Blog prose ─────────────────────────────────────────────────────── */
.prose-custom { color: var(--text-2); line-height: 1.8; font-size: 1.02rem; }
.prose-custom h2, .prose-custom h3 { color: var(--text); font-family: 'Syne', system-ui, sans-serif; margin: 2rem 0 .6rem; }
[dir='rtl'] .prose-custom h2, [dir='rtl'] .prose-custom h3 { font-family: 'Vazirmatn', system-ui, sans-serif; }
.prose-custom p { margin-bottom: 1.2rem; }
.prose-custom a { color: var(--accent); text-decoration: underline; text-underline-offset: .2em; }
.prose-custom strong { color: var(--text); }
.prose-custom code { font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace; background: #f4f4f5; border: 1px solid var(--line); border-radius: 5px; padding: .12em .4em; font-size: .88em; color: #3f3f46; }
.prose-custom pre { background: #f4f4f5; border: 1px solid var(--line); border-radius: 8px; padding: 1.1rem; overflow-x: auto; margin: 1.4rem 0; }
.prose-custom pre code { background: none; border: 0; padding: 0; color: #27272a; }
.prose-custom ul, .prose-custom ol { padding-inline-start: 1.4rem; margin-bottom: 1.2rem; }
.prose-custom li { margin-bottom: .4rem; }
.prose-custom blockquote { border-inline-start: 3px solid var(--accent); padding-inline-start: 1rem; color: var(--text-2); margin: 1.4rem 0; }
/* ════════════════════════════════════════════════════════════════════════
Admin (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 { .label-mono {
font-family: 'SpaceMono', ui-monospace, monospace; font-family: 'SpaceMono', ui-monospace, monospace;
font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: #94a3b8; font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: #94a3b8;
} }
.gradient-text {
background: linear-gradient(135deg,#38bdf8 0%,#818cf8 45%,#e879f9 100%);
-webkit-background-clip: text; background-clip: text; color: transparent;
background-size: 200% 200%; animation: gradient-pan 8s ease-in-out infinite;
}
@keyframes gradient-pan { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
.btn-primary {
display: inline-flex; align-items: center; gap: .6rem;
padding: .85rem 1.4rem; border-radius: 999px;
font-weight: 600; color: #020510;
background: linear-gradient(135deg,#38bdf8 0%,#818cf8 60%,#e879f9 100%);
background-size: 200% 200%;
transition: transform .25s ease, box-shadow .25s ease, background-position .6s ease;
box-shadow: 0 12px 40px -12px rgba(56,189,248,.55);
}
.btn-primary:hover { transform: translateY(-1px); background-position: 100% 0; box-shadow: 0 18px 50px -10px rgba(232,121,249,.55); }
.btn-ghost {
display: inline-flex; align-items: center; gap: .6rem;
padding: .8rem 1.35rem; border-radius: 999px;
font-weight: 500; color: #e2e8f0;
border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.02);
transition: border-color .25s ease, background .25s ease, transform .25s ease;
}
.btn-ghost:hover { border-color: rgba(56,189,248,.6); background: rgba(56,189,248,.06); transform: translateY(-1px); }
.nav-link {
font-size: .875rem; color: #94a3b8;
transition: color .2s ease; text-decoration: none;
}
.nav-link:hover { color: #e2e8f0; }
#navbar.scrolled { background: rgba(2,5,16,.85); backdrop-filter: blur(20px); border-bottom: 1px solid rgba(255,255,255,.05); }
/* ─── Section header ─────────────────────────────────────────────── */
.section-header { text-align: center; margin-bottom: 3.5rem; }
.section-header .eyebrow { margin-bottom: 1rem; }
.section-header h2 { font-family:'Syne',sans-serif; font-size:clamp(1.8rem,3.5vw,2.75rem); font-weight:800; color:#fff; margin-bottom:1rem; }
[dir='rtl'] .section-header h2 { font-family:'Vazirmatn',sans-serif; }
.section-header p { max-width:42rem; margin:0 auto; color:#94a3b8; font-size:1.0625rem; line-height:1.7; }
/* ─── Service cards ──────────────────────────────────────────────── */
.service-card { transition: border-color .3s, box-shadow .3s, transform .3s; }
.service-card:hover { transform: translateY(-2px); }
/* ─── Expertise bars ─────────────────────────────────────────────── */
.bar-track { height: .375rem; background: rgba(255,255,255,.07); border-radius:999px; overflow:hidden; }
.bar-fill { height: 100%; border-radius:999px; transform-origin:left; }
[dir='rtl'] .bar-fill { transform-origin:right; }
/* ─── Scroll-reveal ──────────────────────────────────────────────── */
.reveal { opacity: 0; transform: translateY(24px); transition: opacity .7s cubic-bezier(.22,1,.36,1), transform .7s cubic-bezier(.22,1,.36,1); }
.reveal.visible { opacity: 1; transform: none; }
/* ─── Portfolio modal ────────────────────────────────────────────── */
#portfolio-modal { transition: opacity .25s ease; }
#portfolio-modal.hidden { pointer-events: none; }
/* ─── Admin nav ──────────────────────────────────────────────────── */
.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