Add Docker deployment for Hugging Face Spaces

This commit is contained in:
alireza 2026-05-26 16:04:01 +03:30
parent 8c5d2a9739
commit 848f03374e
22 changed files with 1514 additions and 509 deletions

40
.dockerignore Normal file
View File

@ -0,0 +1,40 @@
# Version control
.git
.gitignore
.gitattributes
# Build output (rebuilt inside the image)
dist
dist-ssr
build
.cache
# Dependencies (reinstalled in the image)
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor / OS
.vscode
.idea
.DS_Store
Thumbs.db
*.swp
*.swo
# Env (never bake secrets into the image)
.env
.env.*
!.env.example
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# Misc
*.log
coverage
.eslintcache

40
Dockerfile Normal file
View File

@ -0,0 +1,40 @@
# syntax=docker/dockerfile:1.7
# =====================================================================
# Stage 1 — Build the Vite/React app
# =====================================================================
FROM node:22-alpine AS builder
WORKDIR /app
# Install only what's locked, deterministically.
# --legacy-peer-deps avoids ERESOLVE failures on bleeding-edge peer ranges
# (React 19 / Vite 8 / TS 6 / ESLint 10).
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund --legacy-peer-deps
# Build the static bundle
COPY . .
RUN npm run build
# =====================================================================
# Stage 2 — Serve with non-root nginx (Hugging Face Spaces compatible)
# =====================================================================
# nginx-unprivileged runs as uid 101 by default — required by HF Spaces,
# which forbids running as root.
FROM nginxinc/nginx-unprivileged:alpine AS runner
# SPA-aware server config that listens on 7860 (HF Spaces default port)
COPY --chown=nginx:nginx nginx.conf /etc/nginx/conf.d/default.conf
# Static assets produced by `vite build`
COPY --chown=nginx:nginx --from=builder /app/dist /usr/share/nginx/html
EXPOSE 7860
# Healthcheck (works both locally and on HF Spaces)
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1:7860/ >/dev/null 2>&1 || exit 1
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,7 +1,38 @@
---
title: Andishkade Foolad
emoji: 🏭
colorFrom: indigo
colorTo: gray
sdk: docker
app_port: 7860
pinned: false
---
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
## Run locally with Docker
```bash
# Build and start (foreground)
docker compose up --build
# Or detached
docker compose up -d --build
# Stop
docker compose down
```
The app will be available at <http://localhost:7860>.
## Deploy to Hugging Face Spaces
1. Create a new Space → **SDK: Docker****Blank**.
2. Push this repo (including `Dockerfile`, `nginx.conf`, and the README frontmatter above).
3. Spaces reads `app_port: 7860` from the frontmatter and routes traffic to the container automatically — no further config needed.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)

21
docker-compose.yml Normal file
View File

@ -0,0 +1,21 @@
# Spec-less Compose file (Docker Compose v2+). The top-level `version:` key
# is intentionally omitted — it is deprecated and emits a warning on modern
# Compose versions.
services:
web:
build:
context: .
dockerfile: Dockerfile
image: andishkade-foolad:latest
container_name: andishkade-foolad
ports:
# host:container — HF Spaces also exposes 7860 publicly
- "7860:7860"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:7860/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

55
nginx.conf Normal file
View File

@ -0,0 +1,55 @@
server {
listen 7860;
listen [::]:7860;
server_name _;
root /usr/share/nginx/html;
index index.html;
# gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/xml+rss
application/wasm
image/svg+xml
font/ttf
font/otf
font/woff
font/woff2;
# Long-lived cache for hashed Vite assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Never cache index.html (so deploys take effect immediately)
location = /index.html {
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
try_files $uri =404;
}
# SPA history-fallback
location / {
try_files $uri $uri/ /index.html;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}

View File

@ -96,7 +96,7 @@ export function Footer() {
gridTemplateColumns: '2fr 1px 1fr 1px 1fr 1px 1fr',
gap: 0,
}}
className="max-md:grid-cols-1 max-md:gap-10"
className="mq-stack max-md:grid-cols-1 max-md:gap-10"
>
{/* Brand */}
<div style={{ paddingLeft: 0, paddingRight: 48 }} className="max-md:pr-0">

View File

@ -101,8 +101,17 @@ export default function Header() {
const [mobileOpen, setMobileOpen] = useState(false)
const [isMobile, setIsMobile] = useState(false)
const [searchOpen, setSearchOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
/* scroll listener — toggles glass + collapses meta strip */
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8)
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
/* viewport listener */
useEffect(() => {
const mq = window.matchMedia('(max-width: 1023px)')
@ -129,17 +138,25 @@ export default function Header() {
position: 'sticky',
top: 0,
zIndex: 50,
background: T.paper,
background: scrolled ? 'rgba(244,239,231,0.72)' : T.paper,
backdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
WebkitBackdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
borderBottom: scrolled ? '1px solid rgba(26,23,18,0.08)' : '1px solid transparent',
direction: 'rtl',
transition: 'background 220ms ease, backdrop-filter 220ms ease, border-color 220ms ease',
}}
>
{/*
TOP META STRIP slim, dark
TOP META STRIP slim, dark, collapses on scroll
*/}
<div
style={{
background: T.ink,
color: 'rgba(244,239,231,0.7)',
height: scrolled ? 0 : 32,
opacity: scrolled ? 0 : 1,
overflow: 'hidden',
transition: 'height 220ms ease, opacity 180ms ease',
}}
className="max-md:hidden"
>
@ -207,7 +224,8 @@ export default function Header() {
*/}
<div
style={{
borderBottom: `2px solid ${T.ink}`,
borderBottom: scrolled ? 'none' : `2px solid ${T.ink}`,
transition: 'border-color 220ms ease',
}}
>
<div

View File

@ -21,6 +21,11 @@ export interface CardData {
title: string
category: string
content: ReactNode
/** Optional thumbnail body fields (MUI-style card layout) */
summary?: string
author?: string
date?: string
pages?: number
}
interface CarouselContextType {
@ -44,6 +49,7 @@ export function Carousel({
}) {
const carouselRef = useRef<HTMLDivElement>(null)
const [currentIndex, setCurrentIndex] = useState(0)
const [paused, setPaused] = useState(false)
useEffect(() => {
if (carouselRef.current) {
@ -51,6 +57,23 @@ export function Carousel({
}
}, [initialScroll])
/* Auto-advance: scroll one card-width every 4s. Pauses on hover. */
useEffect(() => {
if (paused) return
const el = carouselRef.current
if (!el) return
const id = window.setInterval(() => {
if (!el) return
const step = window.innerWidth < 768 ? 280 : 360 // card width + gap
const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 4
el.scrollTo({
left: atEnd ? 0 : el.scrollLeft + step,
behavior: 'smooth',
})
}, 4000)
return () => window.clearInterval(id)
}, [paused])
function handleCardClose(index: number) {
if (!carouselRef.current) return
const cardWidth = window.innerWidth < 768 ? 230 : 350
@ -58,23 +81,34 @@ export function Carousel({
setCurrentIndex(index)
}
function scrollByStep(direction: -1 | 1) {
if (!carouselRef.current) return
const step = window.innerWidth < 768 ? 280 : 360
carouselRef.current.scrollBy({ left: direction * step, behavior: 'smooth' })
}
return (
<CarouselContext.Provider value={{ onCardClose: handleCardClose, currentIndex }}>
<div className="relative w-full">
<div
className="relative w-full"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* Scrollable track */}
<div
ref={carouselRef}
className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 md:py-14 [scrollbar-width:none]"
style={{ direction: 'ltr' }}
onTouchStart={() => setPaused(true)}
onTouchEnd={() => setPaused(false)}
>
<div className="flex flex-row justify-start gap-4 px-4 md:px-10">
<div className="flex flex-row justify-start gap-5 px-6 md:px-12 md:pr-24">
{items.map((item, index) => (
<motion.div
key={'card' + index}
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, delay: 0.1 * index, ease: 'easeOut' }}
className="last:pl-[10%] md:last:pl-[20%]"
transition={{ duration: 0.45, delay: 0.08 * index, ease: 'easeOut' }}
>
{item}
</motion.div>
@ -82,6 +116,103 @@ export function Carousel({
</div>
</div>
{/* ── Glass pill controls — prev/next + auto state ── */}
<div
className="absolute bottom-3 right-6 md:right-12 z-20 flex items-center"
style={{
background: 'rgba(244,239,231,0.5)',
backdropFilter: 'blur(14px) saturate(160%)',
WebkitBackdropFilter: 'blur(14px) saturate(160%)',
border: '1px solid rgba(26,23,18,0.08)',
borderRadius: 9999,
padding: '4px',
boxShadow: '0 4px 16px rgba(26,23,18,0.08)',
gap: 2,
}}
>
{/* Prev */}
<button
aria-label="کارت قبلی"
onClick={() => scrollByStep(-1)}
style={{
width: 36,
height: 36,
borderRadius: '50%',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: 'var(--ink)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background 140ms',
}}
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(26,23,18,0.06)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 19l-7-7 7-7" />
</svg>
</button>
{/* Auto state pill */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '0 12px',
height: 36,
fontSize: 9,
fontWeight: 700,
letterSpacing: '2px',
color: 'var(--ink-4)',
fontFamily: 'ui-monospace, monospace',
direction: 'ltr',
whiteSpace: 'nowrap',
userSelect: 'none',
}}
className="max-md:hidden"
>
<span
style={{
width: 6,
height: 6,
borderRadius: '50%',
background: paused ? 'var(--ink-5)' : 'var(--red)',
boxShadow: paused ? 'none' : '0 0 8px rgba(155,28,28,0.55)',
transition: 'background 160ms, box-shadow 160ms',
}}
/>
{paused ? 'PAUSED' : 'AUTO'}
</div>
{/* Next */}
<button
aria-label="کارت بعدی"
onClick={() => scrollByStep(1)}
style={{
width: 36,
height: 36,
borderRadius: '50%',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: 'var(--ink)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background 140ms',
}}
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(26,23,18,0.06)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</CarouselContext.Provider>
)
@ -188,47 +319,144 @@ export function Card({
)}
</AnimatePresence>
{/* ── Card thumbnail ── */}
{/* ── Card thumbnail — MUI ActionAreaCard pattern, editorial palette ── */}
<motion.button
onClick={handleOpen}
className={cn(
'rounded-3xl overflow-hidden flex flex-col items-start justify-end relative z-10 cursor-pointer',
'h-72 w-48 md:h-[420px] md:w-80',
'overflow-hidden flex flex-col relative cursor-pointer shrink-0 text-right',
'w-[260px] h-[440px] md:w-[340px] md:h-[480px]',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--red)]',
)}
style={{ direction: 'rtl' }}
whileHover={{ scale: 1.015 }}
style={{
direction: 'rtl',
fontFamily: 'Vazir, sans-serif',
background: 'var(--paper)',
border: '1px solid var(--rule-thin)',
borderRadius: 8,
boxShadow: '0 1px 2px rgba(26,23,18,0.04)',
transition: 'box-shadow 200ms, border-color 200ms',
}}
whileHover={{ y: -3 }}
transition={{ duration: 0.2 }}
onMouseEnter={(e) => {
e.currentTarget.style.boxShadow = '0 12px 32px rgba(26,23,18,0.12)'
e.currentTarget.style.borderColor = 'var(--ink-6)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.boxShadow = '0 1px 2px rgba(26,23,18,0.04)'
e.currentTarget.style.borderColor = 'var(--rule-thin)'
}}
>
{/* Strong bottom-to-top gradient — fully opaque at bottom so title pops */}
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/75 via-[45%] to-transparent z-20 pointer-events-none" />
{/* Top centered category badge — prominent, eye-catching */}
<div className="absolute top-6 left-1/2 -translate-x-1/2 z-30">
{/* Media — image strip */}
<div className="relative w-full" style={{ height: 180, overflow: 'hidden', flexShrink: 0 }}>
<BlurImage
src={card.src}
alt={card.title}
className="object-cover w-full h-full"
/>
{/* Category badge — overlaid bottom-right of image (RTL: visual start) */}
<div className="absolute bottom-3 right-3">
<span
className="inline-block text-white px-4 py-1.5 text-[12px] font-bold tracking-[2px] whitespace-nowrap"
className="inline-block text-white px-3 py-1 text-[10px] font-bold tracking-[2px] whitespace-nowrap"
style={{ background: 'var(--red)' }}
>
{card.category}
</span>
</div>
{/* Bottom: title — bright white, readable */}
<div className="relative z-30 px-7 pb-6 pt-10 w-full text-right overflow-hidden">
<p
className="text-white text-[16px] font-extrabold leading-snug line-clamp-3"
style={{ textShadow: '0 2px 8px rgba(0,0,0,0.6)' }}
>
{card.title}
</p>
</div>
{/* Background image */}
<BlurImage
src={card.src}
alt={card.title}
className="object-cover absolute inset-0 w-full h-full z-10"
/>
{/* Content — title + summary + meta */}
<div className="flex flex-col flex-1 px-6 py-5 max-md:px-5 max-md:py-4" style={{ minHeight: 0 }}>
{/* Title + summary group — vertically centered in remaining space */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 10 }}>
{/* Title */}
<h3
style={{
fontSize: 16,
fontWeight: 800,
color: 'var(--ink)',
letterSpacing: '-0.3px',
lineHeight: 1.4,
overflowWrap: 'normal',
wordBreak: 'normal',
paddingLeft: 8,
paddingRight: 8,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{card.title}
</h3>
{/* Summary */}
{card.summary && (
<p
style={{
fontSize: 13,
color: 'var(--ink-4)',
lineHeight: 1.65,
fontWeight: 400,
overflowWrap: 'normal',
wordBreak: 'normal',
paddingLeft: 8,
paddingRight: 8,
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
margin: 0,
}}
>
{card.summary}
</p>
)}
</div>
{/* Meta footer — tight below summary */}
{(card.author || card.date || card.pages) && (
<div
style={{
marginTop: 16,
paddingTop: 12,
borderTop: '1px solid var(--rule-thin)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 }}>
{card.author && (
<span style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink-2)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{card.author}
</span>
)}
{card.date && (
<span style={{ fontSize: 10, color: 'var(--ink-5)', fontWeight: 500 }}>
{card.date}
</span>
)}
</div>
{card.pages && (
<span
style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '0.5px',
color: 'var(--red)',
fontVariantNumeric: 'tabular-nums',
whiteSpace: 'nowrap',
paddingRight: 2,
}}
>
{card.pages.toLocaleString('fa-IR')} ص
</span>
)}
</div>
)}
</div>
</motion.button>
</>
)

View File

@ -0,0 +1,52 @@
import type { CSSProperties, ReactNode } from 'react'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
function cn(...inputs: Parameters<typeof clsx>) {
return twMerge(clsx(inputs))
}
interface MarqueeProps {
children: ReactNode
reverse?: boolean
pauseOnHover?: boolean
className?: string
style?: CSSProperties
}
/**
* Horizontal infinite marquee. Children duplicated 2x for seamless loop.
* Speed via CSS var --duration. Gap via CSS var --gap (default 1.5rem).
*/
export function Marquee({
children,
reverse = false,
pauseOnHover = false,
className,
style,
}: MarqueeProps) {
return (
<div
className={cn(
'group flex overflow-hidden',
'[--gap:1.5rem] [gap:var(--gap)]',
className,
)}
style={{ direction: 'ltr', ...style }}
>
{[0, 1].map((i) => (
<div
key={i}
aria-hidden={i === 1 ? 'true' : undefined}
className={cn(
'flex shrink-0 [gap:var(--gap)]',
reverse ? 'animate-marquee-reverse' : 'animate-marquee',
pauseOnHover && 'group-hover:[animation-play-state:paused]',
)}
>
{children}
</div>
))}
</div>
)
}

View File

@ -246,6 +246,44 @@ body {
.animate-scroll-risk { animation: none; }
}
/*
MOBILE STACK OVERRIDE
Inline `style={{ display:'grid', gridTemplateColumns: '...' }}`
always beats Tailwind `max-md:grid-cols-1`. This utility forces
any tagged grid container down to single/two columns below 768px,
regardless of the inline declaration.
*/
@media (max-width: 767px) {
.mq-stack {
grid-template-columns: 1fr !important;
gap: 1rem !important;
}
.mq-stack-tight {
grid-template-columns: 1fr !important;
}
.mq-stack-2 {
grid-template-columns: repeat(2, 1fr) !important;
}
}
/*
MARQUEE (horizontal infinite scroll)
*/
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(calc(-100% - var(--gap, 1rem))); }
}
@keyframes marquee-reverse {
from { transform: translateX(calc(-100% - var(--gap, 1rem))); }
to { transform: translateX(0); }
}
.animate-marquee { animation: marquee var(--duration, 40s) linear infinite; }
.animate-marquee-reverse { animation: marquee-reverse var(--duration, 40s) linear infinite; }
@media (prefers-reduced-motion: reduce) {
.animate-marquee,
.animate-marquee-reverse { animation: none; }
}
/*
PRINT
*/

View File

@ -7,73 +7,100 @@ import EventsSection from './sections/EventsSection'
import RiskSection from './sections/RiskSection'
import TeamSection from './sections/TeamSection'
import PlansSection from './sections/PlansSection'
import PartnersSection from './sections/PartnersSection'
import NewsletterSection from './sections/NewsletterSection'
/* Section chapter marker
A thin full-bleed dark bar that stamps the start of every
major section creates instant, unmistakable boundaries.
*/
function SectionLabel({ n, title }: { n: string; title: string }) {
/* Minimal chapter mark — thin strip, matches next section's bg, no duplicate title */
function SectionLabel({
n,
bg = 'paper',
}: {
n: string
/** matches next section bg so there's no color seam */
bg?: 'paper' | 'paper-2'
title?: string // ignored — editorial h2 inside each section owns the title
}) {
return (
<div
style={{
background: 'var(--ink)',
borderTop: '1px solid rgba(255,255,255,0.08)',
position: 'relative',
background: `var(--${bg})`,
borderTop: '1px solid var(--rule-thin)',
}}
>
{/* Red accent bar on the right edge (RTL → visual start) */}
<div style={{
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
width: 4,
background: 'var(--red)',
}} />
<div
className="max-w-7xl mx-auto px-12 max-md:px-5"
style={{
display: 'flex',
alignItems: 'center',
gap: 24,
justifyContent: 'space-between',
gap: 16,
paddingTop: 32,
paddingBottom: 8,
}}
className="max-w-7xl mx-auto w-full px-12 py-10 max-md:px-5 max-md:py-7 max-md:gap-4"
>
<span style={{
fontSize: 36,
{/* Left (visual end in RTL): chapter index */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span
style={{
fontSize: 48,
fontWeight: 900,
color: 'var(--red)',
letterSpacing: '-1.5px',
lineHeight: 1,
letterSpacing: '-2px',
lineHeight: 0.85,
direction: 'ltr',
fontVariantNumeric: 'tabular-nums',
flexShrink: 0,
}}
className="max-md:text-[26px]"
className="max-md:text-[32px]"
>
{n}
</span>
<div style={{
width: 40,
height: 2,
background: 'rgba(255,255,255,0.25)',
flexShrink: 0,
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 2,
lineHeight: 1.2,
}}
className="max-md:w-6"
/>
<span style={{
fontSize: 17,
fontWeight: 800,
letterSpacing: '-0.3px',
color: 'var(--paper)',
}}
className="max-md:text-[14px]"
>
{title}
<span
style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '2.5px',
color: 'var(--ink-5)',
direction: 'ltr',
fontFamily: 'ui-monospace, monospace',
}}
>
CHAPTER
</span>
<span
style={{
fontSize: 11,
fontWeight: 600,
letterSpacing: '0.5px',
color: 'var(--ink-4)',
}}
>
بخش {n}
</span>
</div>
</div>
{/* Right (visual start RTL): thin rule fills */}
<div
style={{
flex: 1,
height: 1,
background: 'var(--rule-thin)',
maxWidth: 280,
}}
className="max-md:hidden"
/>
</div>
</div>
)
@ -90,33 +117,37 @@ export default function Home() {
<SnapshotBar />
{/* ── 01 · اسکنر جهانی · bg: paper ─────────────── */}
<SectionLabel n="01" title="اسکنر جهانی · رصد ۲۴ ساعته" />
<SectionLabel n="01" bg="paper" />
<GlobalScannerSection />
{/* ── 02 · تحلیل‌ها · bg: paper-2 ──────────────── */}
<SectionLabel n="02" title="تحلیل‌های اخیر" />
<SectionLabel n="02" bg="paper-2" />
<ReportsCarouselSection />
{/* ── 03 · اخبار · bg: paper ────────────────────── */}
<SectionLabel n="03" title="آخرین اخبار بازار" />
<SectionLabel n="03" bg="paper" />
<LatestNewsBentoSection />
{/* ── 04 · رویدادها · bg: paper-2 ──────────────── */}
<SectionLabel n="04" title="رویدادهای پیش‌رو" />
<SectionLabel n="04" bg="paper-2" />
<EventsSection />
{/* ── 05 · ریسک · bg: paper ─────────────────────── */}
<SectionLabel n="05" title="پایش ریسک · لحظه‌ای" />
<SectionLabel n="05" bg="paper" />
<RiskSection />
{/* ── 06 · تیم · bg: paper-2 ────────────────────── */}
<SectionLabel n="06" title="تیم تحریریه" />
<SectionLabel n="06" bg="paper-2" />
<TeamSection />
{/* ── 07 · اشتراک · bg: paper ───────────────────── */}
<SectionLabel n="07" title="اشتراک و دسترسی" />
<SectionLabel n="07" bg="paper" />
<PlansSection />
{/* ── 08 · همراهان · bg: paper-2 ────────────────── */}
<SectionLabel n="08" bg="paper-2" />
<PartnersSection />
{/* ── خبرنامه (last, no label) ───────────────────── */}
<NewsletterSection />
</div>

View File

@ -27,11 +27,38 @@ export default function EventsSection() {
const displayed = events.slice(0, 4)
return (
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)' }}>
<div className="max-w-7xl mx-auto px-12 py-20 max-md:px-5 max-md:py-14">
{/* Sleek top row */}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 24 }}>
<Link to="/events" style={{ fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--ink-4)', textDecoration: 'none' }}>تقویم کامل </Link>
<section style={{ background: 'var(--paper-2)' }}>
<div className="max-w-7xl mx-auto px-12 py-28 max-md:px-5 max-md:py-20">
{/* ── Editorial header ── */}
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'end',
gap: 32,
paddingBottom: 28,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 48,
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18 }}>
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
تقویم رویداد · ۱۴۰۳۱۴۰۴
</span>
</div>
<h2 style={{ fontSize: 'clamp(28px, 3.6vw, 48px)', fontWeight: 900, letterSpacing: '-1.2px', color: 'var(--ink)', lineHeight: 1.1, marginBottom: 12 }}>
رویدادهای پیشرو
</h2>
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'var(--ink-4)', maxWidth: 560 }}>
همایشها، نمایشگاهها و نشستهای تخصصی صنعت فولاد در ماههای آینده برای حضور یا حمایت رسانهای.
</p>
</div>
<Link to="/events" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap', paddingBottom: 4 }}>
تقویم کامل
</Link>
</div>
{/* 4-col grid */}

View File

@ -52,7 +52,6 @@ export default function GlobalScannerSection() {
<section
style={{
background: 'var(--paper)',
borderBottom: '2px solid var(--ink)',
overflow: 'hidden',
}}
>

View File

@ -4,10 +4,6 @@ import { reports } from '@/data/reports'
const featured = reports.find(r => r.featured)!
/* Dramatic steel mill / industrial image */
const BG_IMAGE =
'https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?q=85&w=2400&auto=format&fit=crop'
const STATS = [
{ n: '۴', label: 'سناریو محتمل' },
{ n: '۱۵', label: 'کشور رقیب' },
@ -23,142 +19,213 @@ export default function HeroSection() {
<section
style={{
position: 'relative',
height: 'min(88vh, 780px)',
minHeight: 540,
overflow: 'hidden',
background: 'var(--paper)',
borderBottom: '3px solid var(--ink)',
overflow: 'hidden',
}}
>
{/* ── Background image ── */}
<img
src={BG_IMAGE}
alt=""
{/* Subtle grid texture */}
<div
aria-hidden="true"
style={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center 40%',
backgroundImage: `
linear-gradient(to right, rgba(26,23,18,0.04) 1px, transparent 1px),
linear-gradient(to bottom, rgba(26,23,18,0.04) 1px, transparent 1px)
`,
backgroundSize: '64px 64px',
pointerEvents: 'none',
}}
/>
{/* ── Gradient overlay — dark vignette from bottom-right ── */}
<div style={{
position: 'absolute',
inset: 0,
background: [
'linear-gradient(to top, rgba(10,8,6,0.96) 0%, rgba(10,8,6,0.5) 45%, transparent 70%)',
'linear-gradient(to left, rgba(10,8,6,0.7) 0%, rgba(10,8,6,0.15) 55%, transparent 80%)',
].join(', '),
}} />
{/* ── Top-right badge ── */}
<div style={{
position: 'absolute',
top: 32,
left: 48,
display: 'flex',
gap: 10,
alignItems: 'center',
}}
className="max-md:left-5 max-md:top-5"
<div
className="relative max-w-7xl mx-auto px-12 max-md:px-5"
style={{ paddingTop: 56, paddingBottom: 64 }}
>
{/* ─── Top meta row ─────────────────────────── */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingBottom: 24,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 48,
}}
className="max-md:flex-wrap max-md:gap-3 max-md:mb-8"
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<div style={{
background: 'var(--red)',
color: 'white',
fontSize: 9,
fontWeight: 700,
letterSpacing: '2.5px',
padding: '5px 12px',
textTransform: 'uppercase',
padding: '5px 14px',
}}>
گزارش ویژه
گزارش ویژه · شماره ۰۱
</div>
<span style={{ fontSize: 10, color: 'rgba(255,255,255,0.45)', letterSpacing: '1px' }}>
بهمن ۱۴۰۳
<span style={{
fontSize: 10,
color: 'var(--ink-5)',
letterSpacing: '1.5px',
textTransform: 'uppercase',
fontWeight: 600,
}}>
{featured.publishDate} · {featured.category}
</span>
</div>
{/* ── Main content — anchored bottom ── */}
<span
style={{
fontSize: 10,
color: 'var(--ink-5)',
fontWeight: 600,
letterSpacing: '1.5px',
fontFamily: 'ui-monospace, monospace',
direction: 'ltr',
}}
className="max-md:hidden"
>
ISSN 2783-XXXX · VOL. XII
</span>
</div>
{/* ─── Main grid — 8/12 + 4/12 ─────────────── */}
<div
style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'flex-end',
display: 'grid',
gridTemplateColumns: '1fr 320px',
gap: 56,
alignItems: 'start',
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-10"
>
<div
className="max-w-7xl mx-auto w-full px-14 pb-[52px] grid grid-cols-1 md:grid-cols-[1fr_240px] items-end gap-12 max-md:px-5 max-md:pb-8 max-md:gap-6"
>
{/* ── Left: headline + meta ── */}
{/* ═══ Left col ═══ */}
<div>
{/* Overline */}
<motion.div
initial={{ opacity: 0, y: 10 }}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 20 }}
transition={{ duration: 0.4 }}
style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}
>
<div style={{ width: 24, height: 2, background: 'var(--red)', flexShrink: 0 }} />
<div style={{ width: 40, height: 2, background: 'var(--red)' }} />
<span style={{
fontSize: 10,
fontSize: 11,
fontWeight: 700,
color: 'rgba(255,255,255,0.5)',
letterSpacing: '3px',
textTransform: 'uppercase',
color: 'var(--ink-4)',
}}>
گزارش راهبردی · تحلیل سناریو
تحلیل سناریو · راهبردی
</span>
</motion.div>
{/* Headline */}
{/* Massive editorial headline */}
<motion.h1
initial={{ opacity: 0, y: 20 }}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
transition={{ duration: 0.55, delay: 0.08 }}
style={{
fontSize: 'clamp(32px, 5vw, 64px)',
fontSize: 'clamp(34px, 5.2vw, 76px)',
fontWeight: 900,
lineHeight: 1.2,
color: 'white',
marginBottom: 24,
maxWidth: 700,
lineHeight: 1.05,
letterSpacing: '-1.5px',
color: 'var(--ink)',
marginBottom: 28,
}}
>
{featured.title.split(':').map((part, i) => (
<span key={i}>
{i === 0 ? part + ':' : (
<span style={{ color: 'rgba(255,255,255,0.45)' }}>{part}</span>
)}
{i === 0 && <br />}
{(() => {
const [main, sub] = featured.title.split(':')
return (
<>
<span style={{ display: 'block' }}>{main}</span>
{sub && (
<span style={{
display: 'block',
color: 'var(--red)',
fontWeight: 900,
marginTop: 8,
}}>
{sub.trim()}
</span>
))}
)}
</>
)
})()}
</motion.h1>
{/* Summary — one line */}
{/* Lede */}
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.25 }}
transition={{ duration: 0.5, delay: 0.18 }}
style={{
fontSize: 14,
lineHeight: 1.8,
fontSize: 'clamp(15px, 1.2vw, 18px)',
lineHeight: 1.7,
fontWeight: 400,
color: 'rgba(255,255,255,0.6)',
maxWidth: 580,
marginBottom: 32,
color: 'var(--ink-3)',
maxWidth: 620,
marginBottom: 36,
borderRight: '2px solid var(--red)',
paddingRight: 16,
}}
>
مدلسازی کمی بر اساس دادههای تجارت ۱۵ کشور رقیب از بدبینانه تا خوشبینانه،
با توصیههای سیاستی مستقیم برای وزارت صمت و سازمان بنادر.
</motion.p>
{/* Divider */}
<div style={{ width: '100%', height: 1, background: 'rgba(255,255,255,0.12)', marginBottom: 24 }} />
{/* Stats inline row */}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.25 }}
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 0,
borderTop: '1px solid var(--rule-thin)',
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 36,
}}
className="mq-stack-2 max-md:grid-cols-2"
>
{STATS.map((s, i) => (
<div
key={i}
style={{
padding: '16px 12px 16px 0',
borderRight: i < STATS.length - 1 ? '1px solid var(--rule-thin)' : 'none',
}}
className={i < 2 ? 'max-md:border-b max-md:border-[var(--rule-thin)]' : ''}
>
<div style={{
fontSize: 'clamp(24px, 2.8vw, 36px)',
fontWeight: 900,
letterSpacing: '-1px',
color: 'var(--ink)',
lineHeight: 1,
fontVariantNumeric: 'tabular-nums',
}}>
{s.n}
</div>
<div style={{
fontSize: 10,
color: 'var(--ink-5)',
fontWeight: 600,
letterSpacing: '0.5px',
marginTop: 6,
}}>
{s.label}
</div>
</div>
))}
</motion.div>
{/* Author + Price + CTA in one bar */}
{/* Author + CTAs */}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
@ -170,78 +237,64 @@ export default function HeroSection() {
flexWrap: 'wrap',
}}
>
{/* Author */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 36, height: 36,
background: 'var(--red)',
color: 'white',
width: 40, height: 40,
background: 'var(--ink)',
color: 'var(--paper)',
fontSize: 14, fontWeight: 900,
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
borderRadius: '50%',
}}>
{featured.authorInitial}
</div>
<div>
<div style={{ fontSize: 12, fontWeight: 700, color: 'white', lineHeight: 1.2 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.2 }}>
{featured.author}
</div>
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', lineHeight: 1.3 }}>
<div style={{ fontSize: 10, color: 'var(--ink-5)', lineHeight: 1.3, marginTop: 2 }}>
{featured.authorRole}
</div>
</div>
</div>
{/* Separator */}
<div className="max-sm:hidden" style={{ width: 1, height: 32, background: 'rgba(255,255,255,0.15)' }} />
<div className="max-sm:hidden" style={{ width: 1, height: 36, background: 'var(--rule-thin)' }} />
{/* Price */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 5 }}>
<span style={{
fontSize: 28,
fontWeight: 900,
color: 'white',
lineHeight: 1,
}}>
{featured.price.toLocaleString('fa-IR')}
</span>
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.4)' }}>تومان</span>
</div>
{/* Buttons */}
<div className="flex gap-2.5 max-sm:flex-col max-sm:w-full">
<button
onMouseEnter={() => setBuyHov(true)}
onMouseLeave={() => setBuyHov(false)}
style={{
background: buyHov ? 'var(--paper-2)' : 'var(--paper)',
color: 'var(--ink)',
background: buyHov ? 'var(--red)' : 'var(--ink)',
color: 'var(--paper)',
border: 'none',
padding: '11px 26px',
padding: '12px 28px',
fontSize: 12,
fontWeight: 700,
fontFamily: 'Vazir, sans-serif',
cursor: 'pointer',
transition: 'background 160ms',
whiteSpace: 'nowrap',
letterSpacing: '0.3px',
}}
className="max-sm:w-full"
>
خرید گزارش
خرید گزارش · {featured.price.toLocaleString('fa-IR')} تومان
</button>
<button
onMouseEnter={() => setFreeHov(true)}
onMouseLeave={() => setFreeHov(false)}
style={{
background: freeHov ? 'rgba(255,255,255,0.1)' : 'transparent',
color: 'white',
border: '1px solid rgba(255,255,255,0.3)',
padding: '11px 26px',
background: freeHov ? 'var(--ink)' : 'transparent',
color: freeHov ? 'var(--paper)' : 'var(--ink)',
border: '1px solid var(--ink)',
padding: '12px 24px',
fontSize: 12,
fontWeight: 400,
fontWeight: 600,
fontFamily: 'Vazir, sans-serif',
cursor: 'pointer',
transition: 'background 160ms',
transition: 'background 160ms, color 160ms',
whiteSpace: 'nowrap',
}}
className="max-sm:w-full"
@ -252,60 +305,118 @@ export default function HeroSection() {
</motion.div>
</div>
{/* ── Right: floating stats panel ── */}
{/* ═══ Right col: dossier mini-cover — frosted glass over ink ═══ */}
<motion.div
initial={{ opacity: 0, x: -16 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.45 }}
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.55, delay: 0.4 }}
style={{
background: 'rgba(255,255,255,0.06)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
border: '1px solid rgba(255,255,255,0.1)',
padding: '20px',
marginBottom: 2,
position: 'relative',
background: 'rgba(26,23,18,0.78)',
backdropFilter: 'blur(24px) saturate(170%)',
WebkitBackdropFilter: 'blur(24px) saturate(170%)',
border: '1px solid rgba(244,239,231,0.08)',
color: 'var(--paper)',
padding: '36px 30px',
boxShadow: '0 24px 60px rgba(26,23,18,0.22), inset 0 1px 0 rgba(244,239,231,0.05)',
transform: 'rotate(0.6deg)',
}}
className="max-md:hidden"
className="max-md:rotate-0"
>
<div style={{
position: 'absolute',
top: 0, right: 0, left: 0,
height: 4,
background: 'var(--red)',
}} />
<div style={{
position: 'absolute',
top: 18,
left: 22,
fontSize: 9,
fontWeight: 700,
letterSpacing: '2px',
textTransform: 'uppercase',
color: 'rgba(255,255,255,0.35)',
marginBottom: 16,
paddingBottom: 10,
borderBottom: '1px solid rgba(255,255,255,0.1)',
color: 'rgba(244,239,231,0.4)',
direction: 'ltr',
fontFamily: 'ui-monospace, monospace',
}}>
در این گزارش
01 / 2025
</div>
{STATS.map((s, i) => (
<div
key={i}
<div style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '3px',
color: 'rgba(244,239,231,0.4)',
textTransform: 'uppercase',
marginBottom: 24,
marginTop: 8,
}}>
DOSSIER
</div>
<div style={{
fontSize: 120,
fontWeight: 900,
lineHeight: 0.85,
letterSpacing: '-6px',
color: 'var(--red)',
direction: 'ltr',
fontVariantNumeric: 'tabular-nums',
marginBottom: 12,
}}>
۰۴
</div>
<div style={{
fontSize: 13,
fontWeight: 700,
color: 'rgba(244,239,231,0.85)',
lineHeight: 1.4,
marginBottom: 4,
}}>
سناریو راهبردی
</div>
<div style={{
fontSize: 11,
color: 'rgba(244,239,231,0.5)',
lineHeight: 1.5,
marginBottom: 28,
}}>
برای آینده فولاد ایران در بازار جهانی ۱۴۰۴
</div>
<ul style={{
listStyle: 'none',
padding: 0,
margin: 0,
borderTop: '1px solid rgba(244,239,231,0.1)',
paddingTop: 16,
}}>
{[
{ k: 'صفحات', v: `${featured.pages.toLocaleString('fa-IR')} صفحه` },
{ k: 'نوع', v: 'گزارش راهبردی' },
{ k: 'انتشار', v: featured.publishDate },
{ k: 'مخاطب', v: 'سیاست‌گذار · مدیر ارشد' },
].map((row) => (
<li
key={row.k}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'baseline',
padding: '10px 0',
borderBottom: i < STATS.length - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
alignItems: 'center',
padding: '8px 0',
fontSize: 11,
}}
>
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.5)', fontWeight: 300 }}>
{s.label}
</span>
<span style={{
fontSize: 22,
fontWeight: 900,
letterSpacing: '-0.5px',
color: 'white',
lineHeight: 1,
}}>
{s.n}
</span>
</div>
<span style={{ color: 'rgba(244,239,231,0.5)' }}>{row.k}</span>
<span style={{ color: 'rgba(244,239,231,0.9)', fontWeight: 600 }}>{row.v}</span>
</li>
))}
</ul>
</motion.div>
</div>
</div>
</section>

View File

@ -51,7 +51,7 @@ function Accent({ color = 'var(--red)' }: { color?: string }) {
/* ─── Section ────────────────────────────────────────────── */
export default function LatestNewsBentoSection() {
return (
<section style={{ background: 'var(--paper)', borderBottom: '2px solid var(--ink)' }}>
<section style={{ background: 'var(--paper)' }}>
{/* ── Header ── */}
<div className="max-w-7xl mx-auto px-12 pt-8 pb-4 max-md:px-5 max-md:pt-6 flex justify-between items-center text-xs text-[var(--ink-4)]">

View File

@ -17,7 +17,7 @@ export default function NewsletterSection() {
{/* Outer grid — strip inline padding, use Tailwind */}
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1px 1fr', minHeight: 180 }}
className="max-w-7xl mx-auto w-full px-12 max-md:px-5 max-md:grid-cols-1"
className="mq-stack max-w-7xl mx-auto w-full px-12 max-md:px-5 max-md:grid-cols-1"
>
{/* Left: description */}
<div

View File

@ -0,0 +1,250 @@
import { Marquee } from '@/components/ui/marquee'
interface Partner {
short: string
name: string
sector: string
}
const partners: Partner[] = [
{ short: 'MSC', name: 'فولاد مبارکه اصفهان', sector: 'تولید · اصفهان' },
{ short: 'KSC', name: 'فولاد خوزستان', sector: 'تولید · اهواز' },
{ short: 'ESC', name: 'ذوب آهن اصفهان', sector: 'تولید · اصفهان' },
{ short: 'HSC', name: 'فولاد هرمزگان', sector: 'تولید · بندرعباس' },
{ short: 'KJK', name: 'فولاد کاوه جنوب کیش', sector: 'تولید · کیش' },
{ short: 'CMG', name: 'چادرملو', sector: 'معدن · یزد' },
{ short: 'GEG', name: 'گل گهر سیرجان', sector: 'معدن · سیرجان' },
{ short: 'IMD', name: 'ایمیدرو', sector: 'هلدینگ دولتی' },
{ short: 'MIM', name: 'وزارت صمت', sector: 'سیاست‌گذاری' },
{ short: 'ICC', name: 'اتاق بازرگانی ایران', sector: 'بخش خصوصی' },
{ short: 'BIM', name: 'بانک صنعت و معدن', sector: 'تأمین مالی' },
{ short: 'NDF', name: 'صندوق توسعه ملی', sector: 'سرمایه‌گذاری' },
]
const half = Math.ceil(partners.length / 2)
const rowA = partners.slice(0, half)
const rowB = partners.slice(half)
function PartnerCard({ p }: { p: Partner }) {
return (
<div
style={{
width: 260,
flexShrink: 0,
background: 'var(--paper)',
border: '1px solid var(--rule-thin)',
padding: '18px 20px',
display: 'flex',
alignItems: 'center',
gap: 14,
direction: 'rtl',
fontFamily: 'Vazir, sans-serif',
transition: 'border-color 160ms, background 160ms',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--ink)'
e.currentTarget.style.background = 'var(--paper-2)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--rule-thin)'
e.currentTarget.style.background = 'var(--paper)'
}}
>
<div
style={{
width: 44,
height: 44,
background: 'var(--ink)',
color: 'var(--paper)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
fontWeight: 900,
letterSpacing: '0.5px',
direction: 'ltr',
fontFamily: 'ui-monospace, monospace',
flexShrink: 0,
borderTop: '2px solid var(--red)',
}}
>
{p.short}
</div>
<div style={{ minWidth: 0, flex: 1 }}>
<div
style={{
fontSize: 13,
fontWeight: 800,
color: 'var(--ink)',
letterSpacing: '-0.2px',
lineHeight: 1.3,
marginBottom: 3,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{p.name}
</div>
<div
style={{
fontSize: 10,
color: 'var(--ink-5)',
fontWeight: 500,
letterSpacing: '0.3px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{p.sector}
</div>
</div>
</div>
)
}
export default function PartnersSection() {
return (
<section
style={{
background: 'var(--paper-2)',
borderBottom: '2px solid var(--ink)',
position: 'relative',
overflow: 'hidden',
}}
>
<div className="max-w-7xl mx-auto px-12 pt-24 pb-20 max-md:px-5 max-md:pt-16 max-md:pb-14">
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'end',
gap: 32,
paddingBottom: 28,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 48,
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
>
<div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 18,
}}
>
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
<span
style={{
fontSize: 11,
fontWeight: 700,
letterSpacing: '3px',
textTransform: 'uppercase',
color: 'var(--ink-4)',
}}
>
همراهان و مشتریان
</span>
</div>
<h2
style={{
fontSize: 'clamp(28px, 3.6vw, 48px)',
fontWeight: 900,
letterSpacing: '-1.2px',
color: 'var(--ink)',
lineHeight: 1.1,
marginBottom: 12,
}}
>
کسانی که با ما کار میکنند
</h2>
<p
style={{
fontSize: 14,
lineHeight: 1.7,
color: 'var(--ink-4)',
maxWidth: 560,
}}
>
تولیدکنندگان فولاد، شرکتهای معدنی، نهادهای سیاستگذار و
مؤسسات مالی که از گزارشهای راهبردی ما استفاده میکنند.
</p>
</div>
<span
style={{
fontSize: 11,
fontWeight: 700,
letterSpacing: '2px',
color: 'var(--ink-5)',
fontFamily: 'ui-monospace, monospace',
direction: 'ltr',
whiteSpace: 'nowrap',
paddingBottom: 4,
}}
className="max-md:hidden"
>
{partners.length.toLocaleString('fa-IR')} · NETWORK
</span>
</div>
<div
style={{
position: 'relative',
padding: '8px 0',
}}
>
<Marquee
pauseOnHover
className="py-2"
style={{ ['--duration' as never]: '38s' }}
>
{rowA.map((p) => (
<PartnerCard key={p.short} p={p} />
))}
</Marquee>
<Marquee
reverse
pauseOnHover
className="py-2 mt-3"
style={{ ['--duration' as never]: '46s' }}
>
{rowB.map((p) => (
<PartnerCard key={p.short} p={p} />
))}
</Marquee>
<div
aria-hidden="true"
style={{
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
width: '18%',
background: 'linear-gradient(to left, var(--paper-2), transparent)',
pointerEvents: 'none',
}}
/>
<div
aria-hidden="true"
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
width: '18%',
background: 'linear-gradient(to right, var(--paper-2), transparent)',
pointerEvents: 'none',
}}
/>
</div>
</div>
</section>
)
}

View File

@ -215,7 +215,7 @@ function EnterpriseCard() {
gap: '32px',
alignItems: 'start',
}}
className="max-md:grid-cols-1 max-md:gap-6 max-md:p-6"
className="mq-stack max-md:grid-cols-1 max-md:gap-6 max-md:p-6"
>
{/* Left: description */}
<div>
@ -259,11 +259,14 @@ function EnterpriseCard() {
</div>
{/* Right: feature grid */}
<div style={{
<div
className="mq-stack"
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '8px 18px',
}}>
}}
>
{enterpriseFeatures.map(f => (
<div key={f} style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
<Check featured={false} />
@ -282,12 +285,11 @@ export default function PlansSection() {
return (
<section style={{
background: 'var(--paper)',
borderBottom: '2px solid var(--ink)',
}}>
<div className="max-w-6xl mx-auto px-12 py-16 max-md:px-5 max-md:py-12">
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: 40 }}>
<div style={{ textAlign: 'center', marginBottom: 64 }}>
<div style={{
fontSize: 10, fontWeight: 700, letterSpacing: '2.5px',
textTransform: 'uppercase', color: 'var(--red)',
@ -323,7 +325,7 @@ export default function PlansSection() {
marginBottom: 20,
alignItems: 'stretch',
}}
className="max-md:grid-cols-1 max-md:gap-4"
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
>
{plans.map((plan, idx) => (
<PlanCard

View File

@ -155,7 +155,7 @@ function ReportCardContent({ report }: { report: Report }) {
)}
{/* Meta grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 20 }}>
<div className="mq-stack" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 20 }}>
{[
{ label: 'نویسنده', value: report.author },
{ label: 'صفحات', value: `${report.pages} صفحه` },
@ -222,12 +222,16 @@ function ReportCardContent({ report }: { report: Report }) {
/* ─── Section ────────────────────────────────────────── */
export default function ReportsCarouselSection() {
const carouselReports = reports.filter(r => !r.featured).slice(0, 10)
const carouselReports = reports.filter(r => !r.featured)
const cards: CardData[] = carouselReports.map(report => ({
src: reportImage[report.id] ?? fallbackImage,
title: report.title,
category: typeLabel[report.type] ?? report.type,
summary: report.summary,
author: report.author,
date: report.publishDate,
pages: report.pages,
content: <ReportCardContent report={report} />,
}))
@ -236,27 +240,89 @@ export default function ReportsCarouselSection() {
))
return (
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)', paddingBottom: 40 }}>
{/* Legend & Link Row */}
<div className="max-w-7xl mx-auto px-12 pt-10 max-md:px-5 flex items-center justify-between flex-wrap gap-4">
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
{Object.entries(typeLabel).map(([key, label]) => (
<div key={key} style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: typeColor[key] }} />
<span style={{ fontSize: 10, color: 'var(--ink-4)', fontWeight: 600 }}>{label}</span>
</div>
))}
</div>
<Link to="/reports" style={{
fontSize: 10, fontWeight: 700, letterSpacing: '1.5px',
textTransform: 'uppercase', color: 'var(--ink-4)',
textDecoration: 'none',
<section style={{ background: 'var(--paper-2)', paddingBottom: 96 }}>
{/* ── Editorial header ── */}
<div className="max-w-7xl mx-auto px-12 pt-24 max-md:px-5 max-md:pt-16">
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'end',
gap: 32,
paddingBottom: 28,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 32,
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
>
<div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 18,
}}>
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
<span style={{
fontSize: 11,
fontWeight: 700,
letterSpacing: '3px',
textTransform: 'uppercase',
color: 'var(--ink-4)',
}}>
آرشیو پژوهش · ۱۴۰۳
</span>
</div>
<h2 style={{
fontSize: 'clamp(28px, 3.6vw, 48px)',
fontWeight: 900,
letterSpacing: '-1.2px',
color: 'var(--ink)',
lineHeight: 1.1,
marginBottom: 12,
}}>
تحلیلهای اخیر
</h2>
<p style={{
fontSize: 14,
lineHeight: 1.7,
color: 'var(--ink-4)',
maxWidth: 560,
}}>
مجموعهای از گزارشهای راهبردی، تحلیلهای فصلی و توصیههای سیاستی
منتشرشده در ماههای اخیر.
</p>
</div>
<Link
to="/reports"
style={{
fontSize: 11,
fontWeight: 700,
letterSpacing: '1.5px',
textTransform: 'uppercase',
color: 'var(--red)',
textDecoration: 'none',
whiteSpace: 'nowrap',
paddingBottom: 4,
}}
>
همه گزارشها
</Link>
</div>
<div className="max-w-7xl mx-auto w-full px-12 max-md:px-5 mt-6">
{/* Legend row */}
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}>
{Object.entries(typeLabel).map(([key, label]) => (
<div key={key} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 7, height: 7, borderRadius: '50%', background: typeColor[key] }} />
<span style={{ fontSize: 11, color: 'var(--ink-4)', fontWeight: 600 }}>{label}</span>
</div>
))}
</div>
</div>
<div className="max-w-7xl mx-auto w-full px-12 max-md:px-5 mt-10">
<Carousel items={cardElements} />
</div>
</section>

View File

@ -90,10 +90,43 @@ export default function RiskSection() {
}, [])
return (
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper)' }}>
{/* ── Legend, active count, and link row ── */}
<div className="max-w-7xl mx-auto px-12 pt-8 pb-2 max-md:px-5 max-md:pt-6 flex justify-between items-center flex-wrap gap-4 text-xs">
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}>
<section style={{ background: 'var(--paper)' }}>
{/* ── Editorial header ── */}
<div className="max-w-7xl mx-auto px-12 pt-24 max-md:px-5 max-md:pt-16">
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'end',
gap: 32,
paddingBottom: 28,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 28,
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18 }}>
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
پایش لحظهای · {riskItems.length.toLocaleString('fa-IR')} سیگنال
</span>
</div>
<h2 style={{ fontSize: 'clamp(28px, 3.6vw, 48px)', fontWeight: 900, letterSpacing: '-1.2px', color: 'var(--ink)', lineHeight: 1.1, marginBottom: 12 }}>
نقشه ریسک صنعت
</h2>
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'var(--ink-4)', maxWidth: 580 }}>
ریسکهای ژئوپلیتیک، مقرراتی و بازار که در ماههای اخیر پایش شدهاند
با سطحبندی از بحرانی تا فرصت.
</p>
</div>
<Link to="/risks" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap', paddingBottom: 4 }}>
همه ریسکها
</Link>
</div>
{/* Legend row */}
<div style={{ display: 'flex', gap: 22, flexWrap: 'wrap', marginBottom: 8 }}>
{[
{ color: '#7f1d1d', label: 'بحرانی' },
{ color: '#9b1c1c', label: 'بالا' },
@ -101,25 +134,12 @@ export default function RiskSection() {
{ color: '#166534', label: 'پایین' },
{ color: '#1e40af', label: 'فرصت' },
].map(l => (
<div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: l.color }} />
<span style={{ fontSize: 10, color: 'var(--ink-4)', fontWeight: 600 }}>{l.label}</span>
<span style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 600 }}>{l.label}</span>
</div>
))}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ color: 'var(--ink-5)', fontWeight: 600 }}>{riskItems.length} سیگنال فعال</span>
<Link to="/risks" style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '1.5px',
textTransform: 'uppercase',
color: 'var(--ink-4)',
textDecoration: 'none',
}}>
همه ریسکها
</Link>
</div>
</div>
{/* ── Auto-scroll ticker ── */}

View File

@ -16,7 +16,7 @@ export default function SnapshotBar() {
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
}}
className="max-w-7xl mx-auto w-full px-12 max-md:px-6 max-md:grid-cols-2"
className="mq-stack-2 max-w-7xl mx-auto w-full px-12 max-md:px-6 max-md:grid-cols-2"
>
{prices.map((price, idx) => {
const isLast = idx === prices.length - 1
@ -49,12 +49,12 @@ export default function SnapshotBar() {
{/* Label */}
<div
style={{
fontSize: '9px',
fontSize: 11,
textTransform: 'uppercase',
letterSpacing: '2px',
color: 'var(--ink-5)',
marginBottom: '6px',
fontWeight: 600,
marginBottom: 8,
fontWeight: 700,
}}
>
{price.name}

View File

@ -2,7 +2,7 @@ import { useState } from 'react'
import { Link } from 'react-router-dom'
import { team } from '@/data/team'
function MemberCard({ member }: { member: (typeof team)[0] }) {
function ProfileCard({ member }: { member: (typeof team)[0] }) {
const [hovered, setHovered] = useState(false)
return (
@ -10,136 +10,112 @@ function MemberCard({ member }: { member: (typeof team)[0] }) {
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
background: hovered ? 'var(--paper-2)' : 'var(--paper)',
background: hovered ? 'var(--paper)' : 'transparent',
border: '1px solid var(--rule-thin)',
padding: '24px 18px 18px',
display: 'flex',
flexDirection: 'column',
transition: 'background 150ms ease',
alignItems: 'center',
textAlign: 'center',
transition: 'background 160ms, transform 160ms',
transform: hovered ? 'translateY(-2px)' : 'none',
cursor: 'pointer',
position: 'relative',
}}
className="p-6 max-md:p-5 cursor-pointer border border-[var(--rule-thin)]"
>
{/* Top row: avatar + name/role + reports stat */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
{/* Reports count badge — top-end */}
<div
style={{
position: 'absolute',
top: 10,
left: 10,
background: 'var(--ink)',
color: 'var(--paper)',
fontSize: 9,
fontWeight: 700,
letterSpacing: '0.5px',
padding: '3px 7px',
fontVariantNumeric: 'tabular-nums',
display: 'flex',
alignItems: 'center',
gap: 3,
}}
>
<span>{member.reportCount.toLocaleString('fa-IR')}</span>
<span style={{ opacity: 0.55 }}>گزارش</span>
</div>
{/* Avatar */}
<div
style={{
width: 44,
height: 44,
width: 72,
height: 72,
background: 'var(--ink)',
color: 'var(--paper)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 16,
fontSize: 26,
fontWeight: 900,
borderRadius: '50%',
flexShrink: 0,
marginBottom: 12,
border: '3px solid var(--paper-2)',
boxShadow: '0 4px 12px rgba(26,23,18,0.08)',
}}
>
{member.initial}
</div>
{/* Name + role */}
<div style={{ flex: 1, minWidth: 0 }}>
{/* Name */}
<h3
style={{
fontSize: 14,
fontWeight: 800,
color: 'var(--ink)',
lineHeight: 1.3,
marginBottom: 2,
letterSpacing: '-0.2px',
lineHeight: 1.3,
marginBottom: 4,
}}
>
{member.name}
</h3>
{/* Role */}
<span
style={{
fontSize: 10,
fontWeight: 700,
color: 'var(--red)',
letterSpacing: '1px',
letterSpacing: '1.2px',
textTransform: 'uppercase',
marginBottom: 12,
}}
>
{member.role}
</span>
</div>
{/* Report count — top left (RTL → visual end) */}
<div style={{ textAlign: 'left', flexShrink: 0 }}>
{/* Top expertise chip */}
{member.expertise[0] && (
<div
style={{
fontSize: 18,
fontWeight: 900,
color: 'var(--ink)',
lineHeight: 1,
fontVariantNumeric: 'tabular-nums',
}}
>
{member.reportCount.toLocaleString('fa-IR')}
</div>
<div
style={{
fontSize: 9,
color: 'var(--ink-5)',
marginTop: 3,
fontWeight: 500,
}}
>
گزارش
</div>
</div>
</div>
{/* Bio */}
<p
style={{
fontSize: 12,
color: 'var(--ink-3)',
lineHeight: 1.7,
fontWeight: 400,
marginTop: 14,
marginBottom: 14,
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{member.bio}
</p>
{/* Tags — max 3 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 12 }}>
{member.expertise.slice(0, 3).map(tag => (
<span
key={tag}
style={{
fontSize: 10,
padding: '2px 8px',
color: 'var(--ink-4)',
padding: '3px 9px',
background: 'var(--paper-2)',
color: 'var(--ink-3)',
border: '1px solid var(--rule-thin)',
fontWeight: 500,
whiteSpace: 'nowrap',
marginBottom: 14,
}}
>
{tag}
</span>
))}
{member.expertise[0]}
</div>
)}
{/* Footer: email */}
{/* Email footer */}
{member.email && (
<div
style={{
paddingTop: 10,
borderTop: '1px solid var(--rule-thin)',
marginTop: 'auto',
}}
>
<a
href={`mailto:${member.email}`}
onClick={e => e.stopPropagation()}
style={{
fontSize: 10,
color: 'var(--ink-5)',
@ -147,22 +123,23 @@ function MemberCard({ member }: { member: (typeof team)[0] }) {
fontWeight: 500,
direction: 'ltr',
fontFamily: 'ui-monospace, monospace',
paddingTop: 10,
borderTop: '1px solid var(--rule-thin)',
width: '100%',
textAlign: 'center',
}}
>
{member.email}
</a>
</div>
)}
</div>
)
}
export default function TeamSection() {
const displayedMembers = team.slice(0, 4)
return (
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)' }}>
<div className="max-w-6xl mx-auto px-12 py-16 max-md:px-5 max-md:py-12">
<section style={{ background: 'var(--paper-2)' }}>
<div className="max-w-6xl mx-auto px-12 py-24 max-md:px-5 max-md:py-16">
{/* Editorial sub-bar */}
<div
style={{
@ -170,20 +147,19 @@ export default function TeamSection() {
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: '1px solid var(--rule-thin)',
paddingBottom: 14,
marginBottom: 24,
paddingBottom: 16,
marginBottom: 40,
}}
className="flex-wrap gap-3"
>
<span
style={{
fontSize: 12,
fontWeight: 400,
color: 'var(--ink-4)',
fontStyle: 'italic',
}}
>
اعضای هیئت تحریریه و متخصصان راهبردی
هیئت تحریریه و متخصصان راهبردی
</span>
<Link
to="/team"
@ -199,17 +175,17 @@ export default function TeamSection() {
</Link>
</div>
{/* 2x2 grid on desktop, single column on mobile */}
{/* Profile grid — 4 col desktop, 2 col tablet, 1 col mobile */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: 12,
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 20,
}}
className="max-md:grid-cols-1"
className="mq-stack-2 max-lg:grid-cols-2 max-sm:grid-cols-1"
>
{displayedMembers.map(member => (
<MemberCard key={member.id} member={member} />
{team.slice(0, 4).map(member => (
<ProfileCard key={member.id} member={member} />
))}
</div>
</div>