Add Docker deployment for Hugging Face Spaces
This commit is contained in:
parent
8c5d2a9739
commit
848f03374e
|
|
@ -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
|
||||||
|
|
@ -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;"]
|
||||||
31
README.md
31
README.md
|
|
@ -1,7 +1,38 @@
|
||||||
|
---
|
||||||
|
title: Andishkade Foolad
|
||||||
|
emoji: 🏭
|
||||||
|
colorFrom: indigo
|
||||||
|
colorTo: gray
|
||||||
|
sdk: docker
|
||||||
|
app_port: 7860
|
||||||
|
pinned: false
|
||||||
|
---
|
||||||
|
|
||||||
# React + TypeScript + Vite
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
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:
|
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)
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -96,7 +96,7 @@ export function Footer() {
|
||||||
gridTemplateColumns: '2fr 1px 1fr 1px 1fr 1px 1fr',
|
gridTemplateColumns: '2fr 1px 1fr 1px 1fr 1px 1fr',
|
||||||
gap: 0,
|
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 */}
|
{/* Brand */}
|
||||||
<div style={{ paddingLeft: 0, paddingRight: 48 }} className="max-md:pr-0">
|
<div style={{ paddingLeft: 0, paddingRight: 48 }} className="max-md:pr-0">
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,17 @@ export default function Header() {
|
||||||
const [mobileOpen, setMobileOpen] = useState(false)
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [searchOpen, setSearchOpen] = useState(false)
|
const [searchOpen, setSearchOpen] = useState(false)
|
||||||
|
const [scrolled, setScrolled] = useState(false)
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
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 */
|
/* viewport listener */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mq = window.matchMedia('(max-width: 1023px)')
|
const mq = window.matchMedia('(max-width: 1023px)')
|
||||||
|
|
@ -129,17 +138,25 @@ export default function Header() {
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 50,
|
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',
|
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
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: T.ink,
|
background: T.ink,
|
||||||
color: 'rgba(244,239,231,0.7)',
|
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"
|
className="max-md:hidden"
|
||||||
>
|
>
|
||||||
|
|
@ -207,7 +224,8 @@ export default function Header() {
|
||||||
═══════════════════════════════════════════ */}
|
═══════════════════════════════════════════ */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
borderBottom: `2px solid ${T.ink}`,
|
borderBottom: scrolled ? 'none' : `2px solid ${T.ink}`,
|
||||||
|
transition: 'border-color 220ms ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ export interface CardData {
|
||||||
title: string
|
title: string
|
||||||
category: string
|
category: string
|
||||||
content: ReactNode
|
content: ReactNode
|
||||||
|
/** Optional thumbnail body fields (MUI-style card layout) */
|
||||||
|
summary?: string
|
||||||
|
author?: string
|
||||||
|
date?: string
|
||||||
|
pages?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CarouselContextType {
|
interface CarouselContextType {
|
||||||
|
|
@ -44,6 +49,7 @@ export function Carousel({
|
||||||
}) {
|
}) {
|
||||||
const carouselRef = useRef<HTMLDivElement>(null)
|
const carouselRef = useRef<HTMLDivElement>(null)
|
||||||
const [currentIndex, setCurrentIndex] = useState(0)
|
const [currentIndex, setCurrentIndex] = useState(0)
|
||||||
|
const [paused, setPaused] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (carouselRef.current) {
|
if (carouselRef.current) {
|
||||||
|
|
@ -51,6 +57,23 @@ export function Carousel({
|
||||||
}
|
}
|
||||||
}, [initialScroll])
|
}, [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) {
|
function handleCardClose(index: number) {
|
||||||
if (!carouselRef.current) return
|
if (!carouselRef.current) return
|
||||||
const cardWidth = window.innerWidth < 768 ? 230 : 350
|
const cardWidth = window.innerWidth < 768 ? 230 : 350
|
||||||
|
|
@ -58,23 +81,34 @@ export function Carousel({
|
||||||
setCurrentIndex(index)
|
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 (
|
return (
|
||||||
<CarouselContext.Provider value={{ onCardClose: handleCardClose, currentIndex }}>
|
<CarouselContext.Provider value={{ onCardClose: handleCardClose, currentIndex }}>
|
||||||
<div className="relative w-full">
|
<div
|
||||||
|
className="relative w-full"
|
||||||
|
onMouseEnter={() => setPaused(true)}
|
||||||
|
onMouseLeave={() => setPaused(false)}
|
||||||
|
>
|
||||||
{/* Scrollable track */}
|
{/* Scrollable track */}
|
||||||
<div
|
<div
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 md:py-14 [scrollbar-width:none]"
|
className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 md:py-14 [scrollbar-width:none]"
|
||||||
style={{ direction: 'ltr' }}
|
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) => (
|
{items.map((item, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={'card' + index}
|
key={'card' + index}
|
||||||
initial={{ opacity: 0, y: 24 }}
|
initial={{ opacity: 0, y: 24 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.45, delay: 0.1 * index, ease: 'easeOut' }}
|
transition={{ duration: 0.45, delay: 0.08 * index, ease: 'easeOut' }}
|
||||||
className="last:pl-[10%] md:last:pl-[20%]"
|
|
||||||
>
|
>
|
||||||
{item}
|
{item}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -82,6 +116,103 @@ export function Carousel({
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</CarouselContext.Provider>
|
</CarouselContext.Provider>
|
||||||
)
|
)
|
||||||
|
|
@ -188,47 +319,144 @@ export function Card({
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* ── Card thumbnail ── */}
|
{/* ── Card thumbnail — MUI ActionAreaCard pattern, editorial palette ── */}
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-3xl overflow-hidden flex flex-col items-start justify-end relative z-10 cursor-pointer',
|
'overflow-hidden flex flex-col relative cursor-pointer shrink-0 text-right',
|
||||||
'h-72 w-48 md:h-[420px] md:w-80',
|
'w-[260px] h-[440px] md:w-[340px] md:h-[480px]',
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--red)]',
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--red)]',
|
||||||
)}
|
)}
|
||||||
style={{ direction: 'rtl' }}
|
style={{
|
||||||
whileHover={{ scale: 1.015 }}
|
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 }}
|
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 */}
|
{/* Media — image strip */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/75 via-[45%] to-transparent z-20 pointer-events-none" />
|
<div className="relative w-full" style={{ height: 180, overflow: 'hidden', flexShrink: 0 }}>
|
||||||
|
<BlurImage
|
||||||
{/* Top centered category badge — prominent, eye-catching */}
|
src={card.src}
|
||||||
<div className="absolute top-6 left-1/2 -translate-x-1/2 z-30">
|
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
|
<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)' }}
|
style={{ background: 'var(--red)' }}
|
||||||
>
|
>
|
||||||
{card.category}
|
{card.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Background image */}
|
{/* Content — title + summary + meta */}
|
||||||
<BlurImage
|
<div className="flex flex-col flex-1 px-6 py-5 max-md:px-5 max-md:py-4" style={{ minHeight: 0 }}>
|
||||||
src={card.src}
|
{/* Title + summary group — vertically centered in remaining space */}
|
||||||
alt={card.title}
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 10 }}>
|
||||||
className="object-cover absolute inset-0 w-full h-full z-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>
|
</motion.button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -246,6 +246,44 @@ body {
|
||||||
.animate-scroll-risk { animation: none; }
|
.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
|
PRINT
|
||||||
═══════════════════════════════════════════════ */
|
═══════════════════════════════════════════════ */
|
||||||
|
|
|
||||||
|
|
@ -7,73 +7,100 @@ import EventsSection from './sections/EventsSection'
|
||||||
import RiskSection from './sections/RiskSection'
|
import RiskSection from './sections/RiskSection'
|
||||||
import TeamSection from './sections/TeamSection'
|
import TeamSection from './sections/TeamSection'
|
||||||
import PlansSection from './sections/PlansSection'
|
import PlansSection from './sections/PlansSection'
|
||||||
|
import PartnersSection from './sections/PartnersSection'
|
||||||
import NewsletterSection from './sections/NewsletterSection'
|
import NewsletterSection from './sections/NewsletterSection'
|
||||||
|
|
||||||
/* ─── Section chapter marker ─────────────────────────────
|
/* ─── Section chapter marker ─────────────────────────────
|
||||||
A thin full-bleed dark bar that stamps the start of every
|
A thin full-bleed dark bar that stamps the start of every
|
||||||
major section — creates instant, unmistakable boundaries.
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--ink)',
|
background: `var(--${bg})`,
|
||||||
borderTop: '1px solid rgba(255,255,255,0.08)',
|
borderTop: '1px solid var(--rule-thin)',
|
||||||
position: 'relative',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 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
|
<div
|
||||||
|
className="max-w-7xl mx-auto px-12 max-md:px-5"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
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={{
|
{/* Left (visual end in RTL): chapter index */}
|
||||||
fontSize: 36,
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 48,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
color: 'var(--red)',
|
color: 'var(--red)',
|
||||||
letterSpacing: '-1.5px',
|
letterSpacing: '-2px',
|
||||||
lineHeight: 1,
|
lineHeight: 0.85,
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
fontVariantNumeric: 'tabular-nums',
|
fontVariantNumeric: 'tabular-nums',
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
}}
|
||||||
className="max-md:text-[26px]"
|
className="max-md:text-[32px]"
|
||||||
>
|
>
|
||||||
{n}
|
{n}
|
||||||
</span>
|
</span>
|
||||||
|
<div
|
||||||
<div style={{
|
style={{
|
||||||
width: 40,
|
display: 'flex',
|
||||||
height: 2,
|
flexDirection: 'column',
|
||||||
background: 'rgba(255,255,255,0.25)',
|
gap: 2,
|
||||||
flexShrink: 0,
|
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>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -90,33 +117,37 @@ export default function Home() {
|
||||||
<SnapshotBar />
|
<SnapshotBar />
|
||||||
|
|
||||||
{/* ── 01 · اسکنر جهانی · bg: paper ─────────────── */}
|
{/* ── 01 · اسکنر جهانی · bg: paper ─────────────── */}
|
||||||
<SectionLabel n="01" title="اسکنر جهانی · رصد ۲۴ ساعته" />
|
<SectionLabel n="01" bg="paper" />
|
||||||
<GlobalScannerSection />
|
<GlobalScannerSection />
|
||||||
|
|
||||||
{/* ── 02 · تحلیلها · bg: paper-2 ──────────────── */}
|
{/* ── 02 · تحلیلها · bg: paper-2 ──────────────── */}
|
||||||
<SectionLabel n="02" title="تحلیلهای اخیر" />
|
<SectionLabel n="02" bg="paper-2" />
|
||||||
<ReportsCarouselSection />
|
<ReportsCarouselSection />
|
||||||
|
|
||||||
{/* ── 03 · اخبار · bg: paper ────────────────────── */}
|
{/* ── 03 · اخبار · bg: paper ────────────────────── */}
|
||||||
<SectionLabel n="03" title="آخرین اخبار بازار" />
|
<SectionLabel n="03" bg="paper" />
|
||||||
<LatestNewsBentoSection />
|
<LatestNewsBentoSection />
|
||||||
|
|
||||||
{/* ── 04 · رویدادها · bg: paper-2 ──────────────── */}
|
{/* ── 04 · رویدادها · bg: paper-2 ──────────────── */}
|
||||||
<SectionLabel n="04" title="رویدادهای پیشرو" />
|
<SectionLabel n="04" bg="paper-2" />
|
||||||
<EventsSection />
|
<EventsSection />
|
||||||
|
|
||||||
{/* ── 05 · ریسک · bg: paper ─────────────────────── */}
|
{/* ── 05 · ریسک · bg: paper ─────────────────────── */}
|
||||||
<SectionLabel n="05" title="پایش ریسک · لحظهای" />
|
<SectionLabel n="05" bg="paper" />
|
||||||
<RiskSection />
|
<RiskSection />
|
||||||
|
|
||||||
{/* ── 06 · تیم · bg: paper-2 ────────────────────── */}
|
{/* ── 06 · تیم · bg: paper-2 ────────────────────── */}
|
||||||
<SectionLabel n="06" title="تیم تحریریه" />
|
<SectionLabel n="06" bg="paper-2" />
|
||||||
<TeamSection />
|
<TeamSection />
|
||||||
|
|
||||||
{/* ── 07 · اشتراک · bg: paper ───────────────────── */}
|
{/* ── 07 · اشتراک · bg: paper ───────────────────── */}
|
||||||
<SectionLabel n="07" title="اشتراک و دسترسی" />
|
<SectionLabel n="07" bg="paper" />
|
||||||
<PlansSection />
|
<PlansSection />
|
||||||
|
|
||||||
|
{/* ── 08 · همراهان · bg: paper-2 ────────────────── */}
|
||||||
|
<SectionLabel n="08" bg="paper-2" />
|
||||||
|
<PartnersSection />
|
||||||
|
|
||||||
{/* ── خبرنامه (last, no label) ───────────────────── */}
|
{/* ── خبرنامه (last, no label) ───────────────────── */}
|
||||||
<NewsletterSection />
|
<NewsletterSection />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,38 @@ export default function EventsSection() {
|
||||||
const displayed = events.slice(0, 4)
|
const displayed = events.slice(0, 4)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)' }}>
|
<section style={{ background: 'var(--paper-2)' }}>
|
||||||
<div className="max-w-7xl mx-auto px-12 py-20 max-md:px-5 max-md:py-14">
|
<div className="max-w-7xl mx-auto px-12 py-28 max-md:px-5 max-md:py-20">
|
||||||
{/* Sleek top row */}
|
{/* ── Editorial header ── */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 24 }}>
|
<div
|
||||||
<Link to="/events" style={{ fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--ink-4)', textDecoration: 'none' }}>تقویم کامل ←</Link>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* 4-col grid */}
|
{/* 4-col grid */}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ export default function GlobalScannerSection() {
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--paper)',
|
background: 'var(--paper)',
|
||||||
borderBottom: '2px solid var(--ink)',
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,6 @@ import { reports } from '@/data/reports'
|
||||||
|
|
||||||
const featured = reports.find(r => r.featured)!
|
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 = [
|
const STATS = [
|
||||||
{ n: '۴', label: 'سناریو محتمل' },
|
{ n: '۴', label: 'سناریو محتمل' },
|
||||||
{ n: '۱۵', label: 'کشور رقیب' },
|
{ n: '۱۵', label: 'کشور رقیب' },
|
||||||
|
|
@ -23,142 +19,213 @@ export default function HeroSection() {
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
height: 'min(88vh, 780px)',
|
background: 'var(--paper)',
|
||||||
minHeight: 540,
|
|
||||||
overflow: 'hidden',
|
|
||||||
borderBottom: '3px solid var(--ink)',
|
borderBottom: '3px solid var(--ink)',
|
||||||
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* ── Background image ── */}
|
{/* Subtle grid texture */}
|
||||||
<img
|
<div
|
||||||
src={BG_IMAGE}
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
inset: 0,
|
inset: 0,
|
||||||
width: '100%',
|
backgroundImage: `
|
||||||
height: '100%',
|
linear-gradient(to right, rgba(26,23,18,0.04) 1px, transparent 1px),
|
||||||
objectFit: 'cover',
|
linear-gradient(to bottom, rgba(26,23,18,0.04) 1px, transparent 1px)
|
||||||
objectPosition: 'center 40%',
|
`,
|
||||||
|
backgroundSize: '64px 64px',
|
||||||
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ── Gradient overlay — dark vignette from bottom-right ── */}
|
<div
|
||||||
<div style={{
|
className="relative max-w-7xl mx-auto px-12 max-md:px-5"
|
||||||
position: 'absolute',
|
style={{ paddingTop: 56, paddingBottom: 64 }}
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
|
{/* ─── 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={{
|
<div style={{
|
||||||
background: 'var(--red)',
|
background: 'var(--red)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: '2.5px',
|
letterSpacing: '2.5px',
|
||||||
|
padding: '5px 12px',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
padding: '5px 14px',
|
|
||||||
}}>
|
}}>
|
||||||
گزارش ویژه
|
گزارش ویژه · شماره ۰۱
|
||||||
</div>
|
</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>
|
</span>
|
||||||
</div>
|
</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
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
display: 'grid',
|
||||||
inset: 0,
|
gridTemplateColumns: '1fr 320px',
|
||||||
display: 'flex',
|
gap: 56,
|
||||||
alignItems: 'flex-end',
|
alignItems: 'start',
|
||||||
}}
|
}}
|
||||||
|
className="mq-stack max-md:grid-cols-1 max-md:gap-10"
|
||||||
>
|
>
|
||||||
<div
|
{/* ═══ Left col ═══ */}
|
||||||
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 ── */}
|
|
||||||
<div>
|
<div>
|
||||||
{/* Overline */}
|
{/* Overline */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 8 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.4 }}
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 20 }}
|
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={{
|
<span style={{
|
||||||
fontSize: 10,
|
fontSize: 11,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: 'rgba(255,255,255,0.5)',
|
letterSpacing: '3px',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: 'var(--ink-4)',
|
||||||
}}>
|
}}>
|
||||||
گزارش راهبردی · تحلیل سناریو
|
تحلیل سناریو · راهبردی
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Headline */}
|
{/* Massive editorial headline */}
|
||||||
<motion.h1
|
<motion.h1
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.1 }}
|
transition={{ duration: 0.55, delay: 0.08 }}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 'clamp(32px, 5vw, 64px)',
|
fontSize: 'clamp(34px, 5.2vw, 76px)',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
lineHeight: 1.2,
|
lineHeight: 1.05,
|
||||||
color: 'white',
|
letterSpacing: '-1.5px',
|
||||||
marginBottom: 24,
|
color: 'var(--ink)',
|
||||||
maxWidth: 700,
|
marginBottom: 28,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{featured.title.split(':').map((part, i) => (
|
{(() => {
|
||||||
<span key={i}>
|
const [main, sub] = featured.title.split(':')
|
||||||
{i === 0 ? part + ':' : (
|
return (
|
||||||
<span style={{ color: 'rgba(255,255,255,0.45)' }}>{part}</span>
|
<>
|
||||||
)}
|
<span style={{ display: 'block' }}>{main}</span>
|
||||||
{i === 0 && <br />}
|
{sub && (
|
||||||
|
<span style={{
|
||||||
|
display: 'block',
|
||||||
|
color: 'var(--red)',
|
||||||
|
fontWeight: 900,
|
||||||
|
marginTop: 8,
|
||||||
|
}}>
|
||||||
|
{sub.trim()}
|
||||||
</span>
|
</span>
|
||||||
))}
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
|
|
||||||
{/* Summary — one line */}
|
{/* Lede */}
|
||||||
<motion.p
|
<motion.p
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.5, delay: 0.25 }}
|
transition={{ duration: 0.5, delay: 0.18 }}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 14,
|
fontSize: 'clamp(15px, 1.2vw, 18px)',
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.7,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
color: 'rgba(255,255,255,0.6)',
|
color: 'var(--ink-3)',
|
||||||
maxWidth: 580,
|
maxWidth: 620,
|
||||||
marginBottom: 32,
|
marginBottom: 36,
|
||||||
|
borderRight: '2px solid var(--red)',
|
||||||
|
paddingRight: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
مدلسازی کمی بر اساس دادههای تجارت ۱۵ کشور رقیب — از بدبینانه تا خوشبینانه،
|
مدلسازی کمی بر اساس دادههای تجارت ۱۵ کشور رقیب — از بدبینانه تا خوشبینانه،
|
||||||
با توصیههای سیاستی مستقیم برای وزارت صمت و سازمان بنادر.
|
با توصیههای سیاستی مستقیم برای وزارت صمت و سازمان بنادر.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Stats inline row */}
|
||||||
<div style={{ width: '100%', height: 1, background: 'rgba(255,255,255,0.12)', marginBottom: 24 }} />
|
<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
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 8 }}
|
initial={{ opacity: 0, y: 8 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
|
@ -170,78 +237,64 @@ export default function HeroSection() {
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Author */}
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 36, height: 36,
|
width: 40, height: 40,
|
||||||
background: 'var(--red)',
|
background: 'var(--ink)',
|
||||||
color: 'white',
|
color: 'var(--paper)',
|
||||||
fontSize: 14, fontWeight: 900,
|
fontSize: 14, fontWeight: 900,
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
borderRadius: '50%',
|
||||||
}}>
|
}}>
|
||||||
{featured.authorInitial}
|
{featured.authorInitial}
|
||||||
</div>
|
</div>
|
||||||
<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}
|
{featured.author}
|
||||||
</div>
|
</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}
|
{featured.authorRole}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Separator */}
|
<div className="max-sm:hidden" style={{ width: 1, height: 36, background: 'var(--rule-thin)' }} />
|
||||||
<div className="max-sm:hidden" style={{ width: 1, height: 32, background: 'rgba(255,255,255,0.15)' }} />
|
|
||||||
|
|
||||||
{/* 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">
|
<div className="flex gap-2.5 max-sm:flex-col max-sm:w-full">
|
||||||
<button
|
<button
|
||||||
onMouseEnter={() => setBuyHov(true)}
|
onMouseEnter={() => setBuyHov(true)}
|
||||||
onMouseLeave={() => setBuyHov(false)}
|
onMouseLeave={() => setBuyHov(false)}
|
||||||
style={{
|
style={{
|
||||||
background: buyHov ? 'var(--paper-2)' : 'var(--paper)',
|
background: buyHov ? 'var(--red)' : 'var(--ink)',
|
||||||
color: 'var(--ink)',
|
color: 'var(--paper)',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
padding: '11px 26px',
|
padding: '12px 28px',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
fontFamily: 'Vazir, sans-serif',
|
fontFamily: 'Vazir, sans-serif',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'background 160ms',
|
transition: 'background 160ms',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
letterSpacing: '0.3px',
|
||||||
}}
|
}}
|
||||||
className="max-sm:w-full"
|
className="max-sm:w-full"
|
||||||
>
|
>
|
||||||
خرید گزارش
|
خرید گزارش · {featured.price.toLocaleString('fa-IR')} تومان
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onMouseEnter={() => setFreeHov(true)}
|
onMouseEnter={() => setFreeHov(true)}
|
||||||
onMouseLeave={() => setFreeHov(false)}
|
onMouseLeave={() => setFreeHov(false)}
|
||||||
style={{
|
style={{
|
||||||
background: freeHov ? 'rgba(255,255,255,0.1)' : 'transparent',
|
background: freeHov ? 'var(--ink)' : 'transparent',
|
||||||
color: 'white',
|
color: freeHov ? 'var(--paper)' : 'var(--ink)',
|
||||||
border: '1px solid rgba(255,255,255,0.3)',
|
border: '1px solid var(--ink)',
|
||||||
padding: '11px 26px',
|
padding: '12px 24px',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 400,
|
fontWeight: 600,
|
||||||
fontFamily: 'Vazir, sans-serif',
|
fontFamily: 'Vazir, sans-serif',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'background 160ms',
|
transition: 'background 160ms, color 160ms',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
className="max-sm:w-full"
|
className="max-sm:w-full"
|
||||||
|
|
@ -252,60 +305,118 @@ export default function HeroSection() {
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Right: floating stats panel ── */}
|
{/* ═══ Right col: dossier mini-cover — frosted glass over ink ═══ */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -16 }}
|
initial={{ opacity: 0, y: 24 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 0.45 }}
|
transition={{ duration: 0.55, delay: 0.4 }}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,255,255,0.06)',
|
position: 'relative',
|
||||||
backdropFilter: 'blur(12px)',
|
background: 'rgba(26,23,18,0.78)',
|
||||||
WebkitBackdropFilter: 'blur(12px)',
|
backdropFilter: 'blur(24px) saturate(170%)',
|
||||||
border: '1px solid rgba(255,255,255,0.1)',
|
WebkitBackdropFilter: 'blur(24px) saturate(170%)',
|
||||||
padding: '20px',
|
border: '1px solid rgba(244,239,231,0.08)',
|
||||||
marginBottom: 2,
|
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={{
|
<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,
|
fontSize: 9,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: '2px',
|
letterSpacing: '2px',
|
||||||
textTransform: 'uppercase',
|
color: 'rgba(244,239,231,0.4)',
|
||||||
color: 'rgba(255,255,255,0.35)',
|
direction: 'ltr',
|
||||||
marginBottom: 16,
|
fontFamily: 'ui-monospace, monospace',
|
||||||
paddingBottom: 10,
|
|
||||||
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
|
||||||
}}>
|
}}>
|
||||||
در این گزارش
|
Nº 01 / 2025
|
||||||
</div>
|
</div>
|
||||||
{STATS.map((s, i) => (
|
|
||||||
<div
|
<div style={{
|
||||||
key={i}
|
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={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'baseline',
|
alignItems: 'center',
|
||||||
padding: '10px 0',
|
padding: '8px 0',
|
||||||
borderBottom: i < STATS.length - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
|
fontSize: 11,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.5)', fontWeight: 300 }}>
|
<span style={{ color: 'rgba(244,239,231,0.5)' }}>{row.k}</span>
|
||||||
{s.label}
|
<span style={{ color: 'rgba(244,239,231,0.9)', fontWeight: 600 }}>{row.v}</span>
|
||||||
</span>
|
</li>
|
||||||
<span style={{
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: 900,
|
|
||||||
letterSpacing: '-0.5px',
|
|
||||||
color: 'white',
|
|
||||||
lineHeight: 1,
|
|
||||||
}}>
|
|
||||||
{s.n}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
</ul>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ function Accent({ color = 'var(--red)' }: { color?: string }) {
|
||||||
/* ─── Section ────────────────────────────────────────────── */
|
/* ─── Section ────────────────────────────────────────────── */
|
||||||
export default function LatestNewsBentoSection() {
|
export default function LatestNewsBentoSection() {
|
||||||
return (
|
return (
|
||||||
<section style={{ background: 'var(--paper)', borderBottom: '2px solid var(--ink)' }}>
|
<section style={{ background: 'var(--paper)' }}>
|
||||||
|
|
||||||
{/* ── Header ── */}
|
{/* ── 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)]">
|
<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)]">
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export default function NewsletterSection() {
|
||||||
{/* Outer grid — strip inline padding, use Tailwind */}
|
{/* Outer grid — strip inline padding, use Tailwind */}
|
||||||
<div
|
<div
|
||||||
style={{ display: 'grid', gridTemplateColumns: '1fr 1px 1fr', minHeight: 180 }}
|
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 */}
|
{/* Left: description */}
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -215,7 +215,7 @@ function EnterpriseCard() {
|
||||||
gap: '32px',
|
gap: '32px',
|
||||||
alignItems: 'start',
|
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 */}
|
{/* Left: description */}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -259,11 +259,14 @@ function EnterpriseCard() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: feature grid */}
|
{/* Right: feature grid */}
|
||||||
<div style={{
|
<div
|
||||||
|
className="mq-stack"
|
||||||
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '1fr 1fr',
|
gridTemplateColumns: '1fr 1fr',
|
||||||
gap: '8px 18px',
|
gap: '8px 18px',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{enterpriseFeatures.map(f => (
|
{enterpriseFeatures.map(f => (
|
||||||
<div key={f} style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
<div key={f} style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
||||||
<Check featured={false} />
|
<Check featured={false} />
|
||||||
|
|
@ -282,12 +285,11 @@ export default function PlansSection() {
|
||||||
return (
|
return (
|
||||||
<section style={{
|
<section style={{
|
||||||
background: 'var(--paper)',
|
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">
|
<div className="max-w-6xl mx-auto px-12 py-16 max-md:px-5 max-md:py-12">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{ textAlign: 'center', marginBottom: 40 }}>
|
<div style={{ textAlign: 'center', marginBottom: 64 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 10, fontWeight: 700, letterSpacing: '2.5px',
|
fontSize: 10, fontWeight: 700, letterSpacing: '2.5px',
|
||||||
textTransform: 'uppercase', color: 'var(--red)',
|
textTransform: 'uppercase', color: 'var(--red)',
|
||||||
|
|
@ -323,7 +325,7 @@ export default function PlansSection() {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
alignItems: 'stretch',
|
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) => (
|
{plans.map((plan, idx) => (
|
||||||
<PlanCard
|
<PlanCard
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ function ReportCardContent({ report }: { report: Report }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Meta grid */}
|
{/* 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.author },
|
||||||
{ label: 'صفحات', value: `${report.pages} صفحه` },
|
{ label: 'صفحات', value: `${report.pages} صفحه` },
|
||||||
|
|
@ -222,12 +222,16 @@ function ReportCardContent({ report }: { report: Report }) {
|
||||||
|
|
||||||
/* ─── Section ────────────────────────────────────────── */
|
/* ─── Section ────────────────────────────────────────── */
|
||||||
export default function ReportsCarouselSection() {
|
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 => ({
|
const cards: CardData[] = carouselReports.map(report => ({
|
||||||
src: reportImage[report.id] ?? fallbackImage,
|
src: reportImage[report.id] ?? fallbackImage,
|
||||||
title: report.title,
|
title: report.title,
|
||||||
category: typeLabel[report.type] ?? report.type,
|
category: typeLabel[report.type] ?? report.type,
|
||||||
|
summary: report.summary,
|
||||||
|
author: report.author,
|
||||||
|
date: report.publishDate,
|
||||||
|
pages: report.pages,
|
||||||
content: <ReportCardContent report={report} />,
|
content: <ReportCardContent report={report} />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -236,27 +240,89 @@ export default function ReportsCarouselSection() {
|
||||||
))
|
))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)', paddingBottom: 40 }}>
|
<section style={{ background: 'var(--paper-2)', paddingBottom: 96 }}>
|
||||||
{/* Legend & Link Row */}
|
{/* ── Editorial header ── */}
|
||||||
<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 className="max-w-7xl mx-auto px-12 pt-24 max-md:px-5 max-md:pt-16">
|
||||||
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
<div
|
||||||
{Object.entries(typeLabel).map(([key, label]) => (
|
style={{
|
||||||
<div key={key} style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
|
display: 'grid',
|
||||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: typeColor[key] }} />
|
gridTemplateColumns: '1fr auto',
|
||||||
<span style={{ fontSize: 10, color: 'var(--ink-4)', fontWeight: 600 }}>{label}</span>
|
alignItems: 'end',
|
||||||
</div>
|
gap: 32,
|
||||||
))}
|
paddingBottom: 28,
|
||||||
</div>
|
borderBottom: '1px solid var(--rule-thin)',
|
||||||
<Link to="/reports" style={{
|
marginBottom: 32,
|
||||||
fontSize: 10, fontWeight: 700, letterSpacing: '1.5px',
|
}}
|
||||||
textTransform: 'uppercase', color: 'var(--ink-4)',
|
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
|
||||||
textDecoration: 'none',
|
>
|
||||||
|
<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>
|
</Link>
|
||||||
</div>
|
</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} />
|
<Carousel items={cardElements} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,43 @@ export default function RiskSection() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper)' }}>
|
<section style={{ background: 'var(--paper)' }}>
|
||||||
{/* ── Legend, active count, and link row ── */}
|
{/* ── Editorial header ── */}
|
||||||
<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 className="max-w-7xl mx-auto px-12 pt-24 max-md:px-5 max-md:pt-16">
|
||||||
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}>
|
<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: '#7f1d1d', label: 'بحرانی' },
|
||||||
{ color: '#9b1c1c', label: 'بالا' },
|
{ color: '#9b1c1c', label: 'بالا' },
|
||||||
|
|
@ -101,25 +134,12 @@ export default function RiskSection() {
|
||||||
{ color: '#166534', label: 'پایین' },
|
{ color: '#166534', label: 'پایین' },
|
||||||
{ color: '#1e40af', label: 'فرصت' },
|
{ color: '#1e40af', label: 'فرصت' },
|
||||||
].map(l => (
|
].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 }} />
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* ── Auto-scroll ticker ── */}
|
{/* ── Auto-scroll ticker ── */}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export default function SnapshotBar() {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
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) => {
|
{prices.map((price, idx) => {
|
||||||
const isLast = idx === prices.length - 1
|
const isLast = idx === prices.length - 1
|
||||||
|
|
@ -49,12 +49,12 @@ export default function SnapshotBar() {
|
||||||
{/* Label */}
|
{/* Label */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '9px',
|
fontSize: 11,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '2px',
|
letterSpacing: '2px',
|
||||||
color: 'var(--ink-5)',
|
color: 'var(--ink-5)',
|
||||||
marginBottom: '6px',
|
marginBottom: 8,
|
||||||
fontWeight: 600,
|
fontWeight: 700,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{price.name}
|
{price.name}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { team } from '@/data/team'
|
import { team } from '@/data/team'
|
||||||
|
|
||||||
function MemberCard({ member }: { member: (typeof team)[0] }) {
|
function ProfileCard({ member }: { member: (typeof team)[0] }) {
|
||||||
const [hovered, setHovered] = useState(false)
|
const [hovered, setHovered] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -10,136 +10,112 @@ function MemberCard({ member }: { member: (typeof team)[0] }) {
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
style={{
|
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',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
transition: 'background 150ms ease',
|
alignItems: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
transition: 'background 160ms, transform 160ms',
|
||||||
|
transform: hovered ? 'translateY(-2px)' : 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
className="p-6 max-md:p-5 cursor-pointer border border-[var(--rule-thin)]"
|
|
||||||
>
|
>
|
||||||
{/* Top row: avatar + name/role + reports stat */}
|
{/* Reports count badge — top-end */}
|
||||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
<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 */}
|
{/* Avatar */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 44,
|
width: 72,
|
||||||
height: 44,
|
height: 72,
|
||||||
background: 'var(--ink)',
|
background: 'var(--ink)',
|
||||||
color: 'var(--paper)',
|
color: 'var(--paper)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
fontSize: 16,
|
fontSize: 26,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
flexShrink: 0,
|
marginBottom: 12,
|
||||||
|
border: '3px solid var(--paper-2)',
|
||||||
|
boxShadow: '0 4px 12px rgba(26,23,18,0.08)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{member.initial}
|
{member.initial}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name + role */}
|
{/* Name */}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<h3
|
<h3
|
||||||
style={{
|
style={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
color: 'var(--ink)',
|
color: 'var(--ink)',
|
||||||
lineHeight: 1.3,
|
|
||||||
marginBottom: 2,
|
|
||||||
letterSpacing: '-0.2px',
|
letterSpacing: '-0.2px',
|
||||||
|
lineHeight: 1.3,
|
||||||
|
marginBottom: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{member.name}
|
{member.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{/* Role */}
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: 'var(--red)',
|
color: 'var(--red)',
|
||||||
letterSpacing: '1px',
|
letterSpacing: '1.2px',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
|
marginBottom: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{member.role}
|
{member.role}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Report count — top left (RTL → visual end) */}
|
{/* Top expertise chip */}
|
||||||
<div style={{ textAlign: 'left', flexShrink: 0 }}>
|
{member.expertise[0] && (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
padding: '2px 8px',
|
padding: '3px 9px',
|
||||||
color: 'var(--ink-4)',
|
background: 'var(--paper-2)',
|
||||||
|
color: 'var(--ink-3)',
|
||||||
border: '1px solid var(--rule-thin)',
|
border: '1px solid var(--rule-thin)',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
whiteSpace: 'nowrap',
|
marginBottom: 14,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tag}
|
{member.expertise[0]}
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer: email */}
|
{/* Email footer */}
|
||||||
{member.email && (
|
{member.email && (
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
paddingTop: 10,
|
|
||||||
borderTop: '1px solid var(--rule-thin)',
|
|
||||||
marginTop: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href={`mailto:${member.email}`}
|
href={`mailto:${member.email}`}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: 'var(--ink-5)',
|
color: 'var(--ink-5)',
|
||||||
|
|
@ -147,22 +123,23 @@ function MemberCard({ member }: { member: (typeof team)[0] }) {
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
fontFamily: 'ui-monospace, monospace',
|
fontFamily: 'ui-monospace, monospace',
|
||||||
|
paddingTop: 10,
|
||||||
|
borderTop: '1px solid var(--rule-thin)',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{member.email}
|
{member.email}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeamSection() {
|
export default function TeamSection() {
|
||||||
const displayedMembers = team.slice(0, 4)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)' }}>
|
<section style={{ background: 'var(--paper-2)' }}>
|
||||||
<div className="max-w-6xl mx-auto px-12 py-16 max-md:px-5 max-md:py-12">
|
<div className="max-w-6xl mx-auto px-12 py-24 max-md:px-5 max-md:py-16">
|
||||||
{/* Editorial sub-bar */}
|
{/* Editorial sub-bar */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -170,20 +147,19 @@ export default function TeamSection() {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
borderBottom: '1px solid var(--rule-thin)',
|
borderBottom: '1px solid var(--rule-thin)',
|
||||||
paddingBottom: 14,
|
paddingBottom: 16,
|
||||||
marginBottom: 24,
|
marginBottom: 40,
|
||||||
}}
|
}}
|
||||||
className="flex-wrap gap-3"
|
className="flex-wrap gap-3"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 400,
|
|
||||||
color: 'var(--ink-4)',
|
color: 'var(--ink-4)',
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
اعضای هیئت تحریریه و متخصصان راهبردی
|
هیئت تحریریه و متخصصان راهبردی
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
to="/team"
|
to="/team"
|
||||||
|
|
@ -199,17 +175,17 @@ export default function TeamSection() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2x2 grid on desktop, single column on mobile */}
|
{/* Profile grid — 4 col desktop, 2 col tablet, 1 col mobile */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||||
gap: 12,
|
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 => (
|
{team.slice(0, 4).map(member => (
|
||||||
<MemberCard key={member.id} member={member} />
|
<ProfileCard key={member.id} member={member} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue