Audit fixes: VIP self-grant, draft leak, onboarding flag, hero personalization, gated downloads
Deploy GitHub Pages / deploy (push) Canceled after 0s

This commit is contained in:
AHMED
2026-10-09 16:20:55 +03:00
parent c34611dc36
commit 5ee706ebd6
150 changed files with 24054 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import { getViewer } from '@/lib/viewer'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'عن الفريق' }
export default async function AboutPage() {
const viewer = await getViewer()
return (
<div id="site" className="site-page v2 v2-about">
<Topnav viewer={viewer} />
<main id="content">
<section className="about-hero" aria-labelledby="about-title">
<div className="wrap about-hero-grid">
<div>
<h1 id="about-title">نكتب عن تجاربنا، ونشارك ما جرّبناه عمليًا</h1>
<p className="lede">أنور بآي منصة يقودها أنور عنبتاوي مع أحمد محمد. نحوّل خبرة سنوات في البنية التحتية والذكاء الاصطناعي التطبيقي إلى أدلة عربية يمكن تنفيذها في عملك.</p>
</div>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="about-mark" src="/brand/mark-light.svg" alt="" width={436} height={382} />
</div>
</section>
<section className="about-team" aria-labelledby="team-title">
<div className="wrap">
<h2 id="team-title">الفريق</h2>
<div className="people">
<article className="person">
<p className="person-role">المؤسس، 20 عاماً في البنية التحتية</p>
<h3>أنور عنبتاوي</h3>
<p>يقود رؤية المنصة بخبرة عقدين في البنية التحتية لتقنية المعلومات. يهتم بتشغيل النماذج الكبيرة محلياً، واستكشاف النماذج المفتوحة، وبناء بيئات تجعل التجربة التقنية قابلة للتشغيل الحقيقي.</p>
<ul className="tags" aria-label="مجالات اهتمام أنور" role="list">
<li>البنية التحتية</li><li>النماذج المفتوحة</li><li>التشغيل المحلي</li><li>الأنظمة الموثوقة</li>
</ul>
<a className="text-link" href="https://www.linkedin.com/in/anwaranabtawi/" target="_blank" rel="noopener noreferrer">أنور على LinkedIn</a>
</article>
<article className="person">
<p className="person-role">الذكاء الاصطناعي التطبيقي وتكامل الأعمال</p>
<h3>أحمد محمد</h3>
<p>يركز على ضبط النماذج، وأطر الوكلاء وتنسيقها، وأنظمة <bdi dir="ltr">RAG</bdi>. يهمه أن يصبح الذكاء الاصطناعي جزءاً فعّالاً من طريقة إنجاز العمل، لا تجربة منفصلة عنه.</p>
<ul className="tags" aria-label="مجالات اهتمام أحمد" role="list">
<li>ضبط النماذج</li><li>أطر الوكلاء</li><li>تنسيق الوكلاء</li><li><bdi dir="ltr">RAG</bdi></li>
</ul>
<a className="text-link" href="https://www.linkedin.com/in/ahmed-mohamed-423583151/" target="_blank" rel="noopener noreferrer">أحمد على LinkedIn</a>
</article>
</div>
</div>
</section>
<section className="about-principles" aria-labelledby="principles-title">
<div className="wrap principles-grid">
<h2 id="principles-title">كيف نكتب كل دليل</h2>
<dl className="principles">
<div><dt>التطبيق قبل الاستعراض</dt><dd>نقيس قيمة التقنية بقدرتها على حل مشكلة واضحة أو تحسين خطوة حقيقية في العمل.</dd></div>
<div><dt>نشرح ما تحت الغطاء</dt><dd>نفضّل الأدوات المفتوحة والتشغيل الذي يمنحك فهماً وتحكماً أكبر في بياناتك ومواردك.</dd></div>
<div><dt>حدود قبل الإطلاق</dt><dd>الوكيل أو النموذج لا يكتمل دون مراقبة وحدود واضحة تمنع الفشل الصامت.</dd></div>
</dl>
</div>
</section>
<section className="about-contact">
<div className="wrap contact-strip">
<p>عندك سؤال عن دليل أو فكرة تريد تطبيقها؟</p>
<a className="btn btn-quiet" href="mailto:hello@anwarpy.ai" dir="ltr">hello@anwarpy.ai</a>
</div>
</section>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,220 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
/* 4-tab settings — markup from account-settings.html with the real Payload
endpoints wired (PATCH /api/users/me for profile + password). Prefs stay
client-side (localStorage) faithful to the original. Stripe is pending:
the cancel button renders disabled with a note. */
type Props = {
email: string
userId: number
fullName: string
role: string
}
type Status = { ok: boolean; text: string } | null
const TABS = [
{ id: 'profile', label: 'الملف الشخصي' },
{ id: 'password', label: 'كلمة المرور' },
{ id: 'prefs', label: 'التفضيلات' },
{ id: 'membership', label: 'العضوية والفوترة' },
]
export default function AccountSettingsClient({ email, userId, fullName, role }: Props) {
const router = useRouter()
const [tab, setTab] = useState('profile')
const [name, setName] = useState(fullName)
const [profileStatus, setProfileStatus] = useState<Status>(null)
const [profileBusy, setProfileBusy] = useState(false)
const [currentPw, setCurrentPw] = useState('')
const [newPw, setNewPw] = useState('')
const [confirmPw, setConfirmPw] = useState('')
const [pwStatus, setPwStatus] = useState<Status>(null)
const [pwBusy, setPwBusy] = useState(false)
const [prefs, setPrefs] = useState({ weekly: true, replies: true, updates: false })
const [prefsStatus, setPrefsStatus] = useState<Status>(null)
const [logoutBusy, setLogoutBusy] = useState(false)
useEffect(() => {
try {
const raw = localStorage.getItem('anw-prefs')
if (raw) setPrefs(p => ({ ...p, ...JSON.parse(raw) }))
} catch { /* ignore */ }
}, [])
const rulesOk = {
len: newPw.length >= 8,
num: /\d/.test(newPw),
diff: newPw.length > 0 && newPw !== currentPw,
}
async function saveProfile(e: React.FormEvent) {
e.preventDefault()
if (name.trim().length < 3) {
setProfileStatus({ ok: false, text: 'أدخل اسماً من 3 أحرف على الأقل.' })
return
}
setProfileBusy(true)
const res = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fullName: name.trim() }),
})
setProfileStatus(res.ok
? { ok: true, text: 'تم حفظ الاسم ✓' }
: { ok: false, text: 'تعذّر الحفظ — أعد المحاولة.' })
setProfileBusy(false)
router.refresh()
}
async function changePassword(e: React.FormEvent) {
e.preventDefault()
if (!currentPw) { setPwStatus({ ok: false, text: 'أدخل كلمة المرور الحالية.' }); return }
if (!(rulesOk.len && rulesOk.num)) { setPwStatus({ ok: false, text: 'كلمة المرور الجديدة لا تحقق الشروط أعلاه.' }); return }
if (newPw === currentPw) { setPwStatus({ ok: false, text: 'كلمة المرور الجديدة تطابق الحالية.' }); return }
if (newPw !== confirmPw) { setPwStatus({ ok: false, text: 'التأكيد لا يطابق كلمة المرور الجديدة.' }); return }
setPwBusy(true)
const res = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: newPw }),
})
setPwStatus(res.ok
? { ok: true, text: 'تم تحديث كلمة المرور ✓' }
: { ok: false, text: 'تعذّر التحديث — تحقق من كلمة المرور الحالية أو أعد المحاولة.' })
if (res.ok) {
setCurrentPw(''); setNewPw(''); setConfirmPw('')
}
setPwBusy(false)
}
function savePrefs() {
try { localStorage.setItem('anw-prefs', JSON.stringify(prefs)) } catch { /* ignore */ }
setPrefsStatus({ ok: true, text: 'تم حفظ التفضيلات ✓' })
}
async function logout() {
setLogoutBusy(true)
await fetch('/api/users/logout', { method: 'POST' })
router.refresh()
router.push('/')
}
const entitled = role === 'vip' || role === 'admin'
return (
<>
<nav className="settings-nav" role="tablist" aria-label="أقسام الإعدادات" data-od-id="settings-nav">
{TABS.map(t => (
<button role="tab" key={t.id} id={`tab-${t.id}`} aria-selected={tab === t.id} aria-controls={`panel-${t.id}`} data-tab={t.id} onClick={() => setTab(t.id)}>
{t.label}
</button>
))}
</nav>
<div className="panels">
<section className={`panel${tab === 'profile' ? ' active' : ''}`} id="panel-profile" role="tabpanel" aria-labelledby="tab-profile" data-od-id="panel-profile">
<h2>الملف الشخصي</h2>
<p className="panel-sub">الاسم كما يظهر في تعليقاتك وقوائم الحفظ. البريد الإلكتروني هو معرّف الدخول ولا يتغير من هنا.</p>
<form className="form-grid" id="profileForm" noValidate onSubmit={saveProfile}>
<div className="field"><label htmlFor="fullName">الاسم الكامل</label><input className="input" id="fullName" name="fullName" value={name} onChange={e => setName(e.target.value)} autoComplete="name" required /></div>
<div className="field"><label htmlFor="email">البريد الإلكتروني</label><input className="input" id="email" value={email} disabled /><span className="hint">لتغيير البريد تواصل معنا على <bdi>hello@anwarpy.ai</bdi></span></div>
<button className="btn btn-primary" type="submit" data-od-id="save-profile" disabled={profileBusy}>{profileBusy ? 'جارٍ الحفظ…' : 'حفظ التغييرات'}</button>
<p className={`status ${profileStatus?.ok ? 'ok' : profileStatus ? 'err' : ''}`} id="profileStatus" role="status">{profileStatus?.text}</p>
</form>
</section>
<section className={`panel${tab === 'password' ? ' active' : ''}`} id="panel-password" role="tabpanel" aria-labelledby="tab-password" data-od-id="panel-password">
<h2>تغيير كلمة المرور</h2>
<p className="panel-sub">بعد التغيير تبقى جلستك الحالية مفتوحة، وتُطلب كلمة المرور الجديدة عند الدخول من جهاز آخر.</p>
<form className="form-grid" id="passwordForm" noValidate onSubmit={changePassword}>
<div className="field"><label htmlFor="currentPw">كلمة المرور الحالية</label><input className="input" id="currentPw" type="password" name="currentPw" autoComplete="current-password" required value={currentPw} onChange={e => setCurrentPw(e.target.value)} /></div>
<div className="field"><label htmlFor="newPw">كلمة المرور الجديدة</label><input className="input" id="newPw" type="password" name="newPw" autoComplete="new-password" required minLength={8} aria-describedby="pwRules" value={newPw} onChange={e => setNewPw(e.target.value)} /></div>
<ul className="pw-rules" id="pwRules">
<li data-rule="len" className={rulesOk.len ? 'ok' : ''}>8 أحرف على الأقل</li>
<li data-rule="num" className={rulesOk.num ? 'ok' : ''}>تحتوي رقماً واحداً على الأقل</li>
<li data-rule="diff" className={rulesOk.diff ? 'ok' : ''}>تختلف عن كلمة المرور الحالية</li>
</ul>
<div className="field"><label htmlFor="confirmPw">تأكيد كلمة المرور</label><input className="input" id="confirmPw" type="password" name="confirmPw" autoComplete="new-password" required value={confirmPw} onChange={e => setConfirmPw(e.target.value)} /></div>
<button className="btn btn-primary" type="submit" data-od-id="save-password" disabled={pwBusy}>{pwBusy ? 'جارٍ التحديث…' : 'تحديث كلمة المرور'}</button>
<p className={`status ${pwStatus?.ok ? 'ok' : pwStatus ? 'err' : ''}`} id="passwordStatus" role="status" aria-live="polite">{pwStatus?.text}</p>
</form>
</section>
<section className={`panel${tab === 'prefs' ? ' active' : ''}`} id="panel-prefs" role="tabpanel" aria-labelledby="tab-prefs" data-od-id="panel-prefs">
<h2>التفضيلات</h2>
<p className="panel-sub">رسائل أنور بآي قليلة ومقصودة. يمكنك إيقاف أي نوع منها من دون التأثير على عضويتك.</p>
<div className="prefs">
<div className="pref-row"><div><strong>الرسالة الأسبوعية</strong><p>خلاصة ما نُشر من أدلة ومقالات، كل يوم أحد.</p></div>
<label className="switch"><input type="checkbox" id="prefWeekly" aria-label="تفعيل الرسالة الأسبوعية" checked={prefs.weekly} onChange={e => setPrefs(p => ({ ...p, weekly: e.target.checked }))} /><span className="track"></span></label></div>
<div className="pref-row"><div><strong>إشعارات التعليقات</strong><p>رسالة عند ردّ أحد الأعضاء على تعليقك.</p></div>
<label className="switch"><input type="checkbox" id="prefReplies" aria-label="تفعيل إشعارات التعليقات" checked={prefs.replies} onChange={e => setPrefs(p => ({ ...p, replies: e.target.checked }))} /><span className="track"></span></label></div>
<div className="pref-row"><div><strong>تحديثات الأدلة المحفوظة</strong><p>إشعار عندما يُحدَّث دليل حفظته للعودة إليه.</p></div>
<label className="switch"><input type="checkbox" id="prefUpdates" aria-label="تفعيل تحديثات الأدلة المحفوظة" checked={prefs.updates} onChange={e => setPrefs(p => ({ ...p, updates: e.target.checked }))} /><span className="track"></span></label></div>
</div>
<button className="btn btn-primary" type="button" id="savePrefs" style={{ marginTop: 28 }} data-od-id="save-prefs" onClick={savePrefs}>حفظ التفضيلات</button>
<p className={`status ${prefsStatus?.ok ? 'ok' : prefsStatus ? 'err' : ''}`} id="prefsStatus" role="status">{prefsStatus?.text}</p>
</section>
<section className={`panel${tab === 'membership' ? ' active' : ''}`} id="panel-membership" role="tabpanel" aria-labelledby="tab-membership" data-od-id="panel-membership">
<h2>العضوية والفوترة</h2>
<p className="panel-sub">خطتك وحالة وصولك. الإلغاء يوقف التجديد التالي ويحفظ وصولك حتى نهاية الفترة المدفوعة.</p>
<article className="membership-card" data-od-id="membership-card">
<div className="membership-head">
<div><h3 id="planName">{entitled ? 'عضوية فردية' : 'حساب مجاني'}</h3><span className="hint">الحالة الحقيقية لحسابك</span></div>
<span className={`state-chip${entitled ? '' : ' cancelled'}`} id="planState">{entitled ? 'نشطة' : role === 'admin' ? 'إداري' : 'مجاني'}</span>
</div>
<div className="plan-details">
<div><span className="price-tag"><bdi dir="ltr">$10</bdi><small>شهرياً</small></span></div>
<dl style={{ margin: 0 }}><dt>الوصول</dt><dd>{entitled ? 'كامل — كل الأدلة الحصرية وملفات العمل' : 'الأدلة المجانية ومعاينة الأدلة'}</dd></dl>
<dl style={{ margin: 0 }}><dt>طريقة الدفع</dt><dd id="payMethod">ستُربط عبر Stripe</dd></dl>
<dl style={{ margin: 0 }}><dt>الوصول بعد الإلغاء</dt><dd>حتى نهاية الفترة المدفوعة</dd></dl>
</div>
<div className="membership-actions" id="membershipActions">
{!entitled && <Link className="btn btn-primary" href="/subscription" id="resubLink">اشترك الآن</Link>}
</div>
</article>
<div className="cancel-zone" id="cancelZone" data-od-id="cancel-zone">
<h3>إلغاء الاشتراك</h3>
{entitled ? (
<>
<p>ربط الدفع (Stripe) غير مكتمل بعد — الإلغاء الذاتي سيتاح بعد ربط الدفع. حتى ذلك الحين تواصل معنا على <bdi>hello@anwarpy.ai</bdi> لأي تغيير.</p>
<button className="text-danger" type="button" id="cancelOpen" data-od-id="cancel-open" disabled>إلغاء الاشتراك</button>
<p className="hint">سيتاح بعد ربط الدفع</p>
</>
) : (
<p>لا يوجد اشتراك مدفوع لإلغائه. يمكنك الترقية في أي وقت من صفحة <Link className="text-link" href="/subscription">العضوية</Link>.</p>
)}
</div>
<div className="billing" data-od-id="billing-history">
<h3>سجل الفواتير</h3>
<p className="billing-sub">كل دفعة تُصدر فاتورة ضريبية فور اكتمالها وترسل إلى بريدك المسجّل. الفواتير متاحة للتنزيل في أي وقت.</p>
<div className="billing-table" role="table" aria-label="سجل الفواتير">
<div className="billing-row head" role="row">
<span role="columnheader">التاريخ</span><span role="columnheader">الوصف</span><span role="columnheader">المبلغ</span><span role="columnheader">الحالة</span><span role="columnheader">الفاتورة</span>
</div>
<div className="billing-row" role="row">
<span className="bdate" role="cell">—</span>
<span className="desc" role="cell">لا فواتير بعد — ربط الدفع قادم</span>
<span className="amt" role="cell">—</span>
<span role="cell"><span className="hint">—</span></span>
<span role="cell"><span className="hint">—</span></span>
</div>
</div>
</div>
</section>
</div>
<div className="form-actions" style={{ marginTop: 32 }}>
<button className="btn btn-secondary" type="button" onClick={logout} disabled={logoutBusy}>تسجيل الخروج</button>
</div>
</>
)
}
@@ -0,0 +1,51 @@
import { getPayload } from 'payload'
import { headers } from 'next/headers'
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import AccountSettingsClient from './client'
import { formatDate } from '@/lib/format'
import configPromise from '@payload-config'
import '../styles/account-settings.css'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'إعدادات الحساب' }
export default async function AccountSettingsPage() {
const payload = await getPayload({ config: configPromise })
const { user } = await payload.auth({ headers: await headers() })
return (
<div id="site" className="site-page page-account-settings">
<Topnav viewer={user} />
{user ? (
<main id="content">
<section className="page-head" data-od-id="settings-header"><div className="container">
<p className="crumb">حسابك في أنور بآي</p>
<h1 data-od-id="settings-title">إعدادات الحساب</h1>
<p className="sub"><bdi>{String(user.email)}</bdi> · عضو منذ {user.createdAt ? formatDate(String(user.createdAt)) : 'اليوم'}</p>
</div></section>
<section className="container settings-layout" data-od-id="settings-layout">
<AccountSettingsClient
email={String(user.email)}
userId={Number(user.id)}
fullName={(user.fullName as string) || ''}
role={((user.role as string) || 'free')}
/>
</section>
</main>
) : (
<main id="content" className="gate">
<section className="gate-card">
<h1>سجّل الدخول لإدارة حسابك</h1>
<p>من هنا تعدّل اسمك وكلمة المرور وتفضيلات الرسائل، وتدير عضويتك.</p>
<div className="actions">
<a className="btn btn-primary" href="/login?next=%2Faccount-settings">تسجيل الدخول</a>
<a className="btn btn-quiet" href="/signup">إنشاء حساب مجاني</a>
</div>
</section>
</main>
)}
<Footer />
</div>
)
}
@@ -0,0 +1,104 @@
'use client'
import { useEffect } from 'react'
import { guidesLabel } from '@/lib/format'
/* Library controller — search/sort/filters/likes from articles.html, wired to
the real backend: likes toggle via /api/posts/[slug]/like (401 → login),
views come from the post-page counter, sort reads data-* attributes. */
export default function ArticlesClient({ loggedIn }: { loggedIn: boolean }) {
useEffect(() => {
const controller = new AbortController()
const params = new URLSearchParams(location.search)
const allowedTopics = ['all', 'agents', 'models', 'automation', 'foundations']
const allowedAccess = ['all', 'free', 'vip']
const state: Record<string, string> = {
topic: allowedTopics.includes(params.get('topic') || '') ? params.get('topic')! : 'all',
access: allowedAccess.includes(params.get('access') || '') ? params.get('access')! : 'all',
query: '',
sort: 'latest',
}
const buttons = [...document.querySelectorAll('[data-filter-kind]')] as HTMLElement[]
const posts = [...document.querySelectorAll('.post')] as HTMLElement[]
const list = document.getElementById('postList')!
const count = document.getElementById('resultCount')
const empty = document.getElementById('emptyState')
const search = document.getElementById('postSearch') as HTMLInputElement | null
const sort = document.getElementById('postSort') as HTMLSelectElement | null
const formatNumber = (value: number) => new Intl.NumberFormat('en-US').format(value)
function normalize(value: string) {
return value.toLocaleLowerCase('ar').normalize('NFKD').replace(/[\u064B-\u065F\u0670]/g, '').replace(/[إأآ]/g, 'ا').replace(/ى/g, 'ي')
}
function paintMetrics(post: HTMLElement) {
const views = Number(post.dataset.viewsCount || 0)
const likes = Number(post.dataset.likesCount || 0)
const viewsEl = post.querySelector('[data-views]')
if (viewsEl) viewsEl.textContent = `${formatNumber(views)} قراءة`
const like = post.querySelector('[data-like]') as HTMLElement | null
if (like) {
like.setAttribute('aria-pressed', post.dataset.liked === '1' ? 'true' : 'false')
const span = like.querySelector('span')
if (span) span.textContent = formatNumber(likes)
}
}
function applyFilters() {
const ordered = [...posts].sort((a, b) => {
if (state.sort === 'views') return Number(b.dataset.viewsCount || 0) - Number(a.dataset.viewsCount || 0) || Number(b.dataset.order) - Number(a.dataset.order)
if (state.sort === 'likes') return Number(b.dataset.likesCount || 0) - Number(a.dataset.likesCount || 0) || Number(b.dataset.order) - Number(a.dataset.order)
return Number(b.dataset.order) - Number(a.dataset.order)
})
ordered.forEach(post => list.appendChild(post))
let visible = 0
posts.forEach(post => {
const topicMatch = state.topic === 'all' || (post.dataset.topic || '').split(' ').includes(state.topic)
const accessMatch = state.access === 'all' || post.dataset.access === state.access
const queryMatch = !state.query || normalize(post.textContent || '').includes(normalize(state.query))
post.hidden = !(topicMatch && accessMatch && queryMatch)
if (!post.hidden) visible += 1
})
if (count) count.textContent = visible === 0 ? 'لا نتائج' : guidesLabel(visible)
empty?.classList.toggle('show', visible === 0)
}
buttons.forEach(button => button.addEventListener('click', () => {
const kind = button.dataset.filterKind!
state[kind] = button.dataset.filterValue!
buttons.filter(item => item.dataset.filterKind === kind).forEach(item => {
item.setAttribute('aria-pressed', String(item === button))
})
button.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })
applyFilters()
}, { signal: controller.signal }))
search?.addEventListener('input', () => { state.query = search.value.trim(); applyFilters() }, { signal: controller.signal })
sort?.addEventListener('change', () => { state.sort = sort.value; applyFilters() }, { signal: controller.signal })
posts.forEach(post => {
paintMetrics(post)
const like = post.querySelector('[data-like]') as HTMLElement | null
like?.addEventListener('click', async () => {
if (!loggedIn) {
location.href = '/login?next=/articles'
return
}
const slug = like.dataset.like!
const res = await fetch(`/api/posts/${slug}/like`, { method: 'POST' })
if (!res.ok) return
const data = await res.json()
post.dataset.likesCount = String(data.likes)
post.dataset.liked = data.liked ? '1' : '0'
paintMetrics(post)
applyFilters()
}, { signal: controller.signal })
})
buttons.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.filterValue === state[button.dataset.filterKind!])))
applyFilters()
return () => controller.abort()
}, [loggedIn])
return null
}
+173
View File
@@ -0,0 +1,173 @@
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import { getPayload } from 'payload'
import { headers } from 'next/headers'
import configPromise from '@payload-config'
import Link from 'next/link'
import ArticlesClient from './articles-client'
import { TOPICS, topicsFor, INTEREST_MAP } from '@/lib/topics'
import { formatNumber, guidesLabel, minutesLabel } from '@/lib/format'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'المكتبة' }
type PostDoc = {
id: number
title: string
slug: string
dek: string
access: 'free' | 'vip'
category: string
level: string
readingMinutes: number
views: number
likes: number
publishedAt: string
cover?: { url?: string | null } | number | null
}
type Viewer = { id: number; email: string; role: string; interest: string | null; level: string | null } | null
export default async function ArticlesPage() {
const payload = await getPayload({ config: configPromise })
const [res, auth] = await Promise.all([
payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
depth: 1,
limit: 100,
sort: '-publishedAt',
select: {
title: true, slug: true, dek: true, access: true, category: true, level: true,
readingMinutes: true, views: true, likes: true, publishedAt: true, cover: true,
},
}),
payload.auth({ headers: await headers() }),
])
const posts = res.docs as unknown as PostDoc[]
const user = auth.user as { id: number; email: string; role: string; interest: string | null; level: string | null } | undefined
const viewer: Viewer = user
? { id: user.id, email: user.email, role: (user.role as string) ?? 'free', interest: (user.interest as string | null) ?? null, level: (user.level as string | null) ?? null }
: null
// Which posts the viewer liked
const likedSlugs = new Set<string>()
if (viewer) {
const { docs } = await payload.find({
collection: 'post-likes',
where: { user: { equals: viewer.id } },
depth: 0,
limit: 500,
overrideAccess: true,
})
for (const like of docs) {
const postId = typeof like.post === 'object' ? (like.post as { id: number }).id : like.post
const post = posts.find(p => p.id === postId)
if (post) likedSlugs.add(post.slug)
}
}
// Personalization: boost rows matching the viewer's interest + level (order
// is a sort weight — matching posts float to the top under «الأحدث أولاً»).
const interestTopic = viewer?.interest ? INTEREST_MAP[viewer.interest] : undefined
posts.forEach((post, i) => {
const el = post as unknown as { _order?: number; _match?: boolean }
el._order = posts.length - i
el._match = !!interestTopic && topicsFor(post).some(topic => topic === interestTopic) && post.level === viewer!.level
if (el._match) el._order += 1000
})
const freeCount = posts.filter(p => p.access === 'free').length
return (
<div id="site" className="site-page v2 v2-library">
<Topnav viewer={viewer} />
<ArticlesClient loggedIn={!!viewer} />
<main id="content">
<section className="lib-head" aria-labelledby="library-title">
<div className="wrap">
<h1 id="library-title">المكتبة</h1>
<p className="lede">تضم المكتبة {guidesLabel(posts.length)}، منها {guidesLabel(freeCount)} متاحة مجاناً بالكامل. ابحث بالموضوع أو صفِّ النتائج حسب المسار.</p>
<div className="lib-tools">
<label className="lib-search">
<span className="sr-only">ابحث في العناوين والوصف</span>
<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="8.5" cy="8.5" r="5.5" fill="none" stroke="currentColor" strokeWidth="1.6" /><path d="M13 13l4 4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /></svg>
<input id="postSearch" type="search" autoComplete="off" placeholder="ابحث: وكيل، RAG، اختيار النموذج…" />
</label>
<label className="lib-sort">
<span>الترتيب</span>
<select id="postSort"><option value="latest">الأحدث</option><option value="views">الأكثر قراءة</option><option value="likes">الأكثر إعجاباً</option></select>
</label>
</div>
<div className="lib-filters">
<div className="segmented" role="group" aria-label="المسار">
<button type="button" data-filter-kind="topic" data-filter-value="all" aria-pressed="true">كل المسارات</button>
{TOPICS.map(topic => (
<button type="button" data-filter-kind="topic" data-filter-value={topic.key} aria-pressed="false" key={topic.key}>{topic.label}</button>
))}
</div>
<div className="segmented" role="group" aria-label="نوع الوصول">
<button type="button" data-filter-kind="access" data-filter-value="all" aria-pressed="true">الكل</button>
<button type="button" data-filter-kind="access" data-filter-value="free" aria-pressed="false">مجاني</button>
<button type="button" data-filter-kind="access" data-filter-value="vip" aria-pressed="false">للأعضاء</button>
</div>
</div>
</div>
</section>
<section className="lib-results" aria-label="الأدلة">
<div className="wrap">
<p className="results-count" id="resultCount" aria-live="polite">{guidesLabel(posts.length)}</p>
<div className="lib-grid" id="postList">
{posts.map(post => {
const meta = post as unknown as { _order?: number; _match?: boolean }
const topics = topicsFor(post)
const coverUrl = typeof post.cover === 'object' ? post.cover?.url ?? null : null
return (
<article
className="post guide-card"
key={post.id}
data-topic={topics.join(' ')}
data-access={post.access}
data-level={post.level}
data-views-count={post.views ?? 0}
data-likes-count={post.likes ?? 0}
data-liked={likedSlugs.has(post.slug) ? '1' : '0'}
data-order={meta._order}
>
{coverUrl && <img className="guide-card-art" src={coverUrl} alt="" width={640} height={320} loading="lazy" />}
<div className="guide-card-body">
<p className="meta-row">
<span>{post.category}</span>
<span>{minutesLabel(post.readingMinutes)}</span>
<span>{post.level}</span>
{meta._match && <span className="for-you">يناسب مستواك</span>}
</p>
<h2 className="guide-card-title"><Link href={`/posts/${post.slug}`}>{post.title}</Link></h2>
<p className="guide-card-dek">{post.dek}</p>
<div className="guide-card-foot">
<span className={`badge ${post.access === 'vip' ? 'badge-vip' : 'badge-free'}`}>{post.access === 'free' ? 'مجاني' : 'للأعضاء'}</span>
<span className="engagement">
<span data-views>{formatNumber(post.views ?? 0)} قراءة</span>
<button className="like" type="button" data-like={post.slug} aria-pressed={likedSlugs.has(post.slug)} aria-label="أعجبني">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13.5 2.6 8.3a3.2 3.2 0 0 1 4.5-4.6L8 4.6l.9-.9a3.2 3.2 0 0 1 4.5 4.6Z" fill="none" stroke="currentColor" strokeWidth="1.4" /></svg>
<span>{formatNumber(post.likes ?? 0)}</span>
</button>
</span>
</div>
</div>
</article>
)
})}
</div>
<div className="empty" id="emptyState">
<p>لا يوجد دليل يطابق هذا البحث.</p>
<p>جرّب كلمة أقصر أو اختر «كل المسارات».</p>
</div>
</div>
</section>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,124 @@
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
export type DeckSlide = {
kicker: string
index: string
title: React.ReactNode
body: string
format: React.ReactNode
link?: { href: string; label: string }
meta?: React.ReactNode
}
const V1_SLIDES: DeckSlide[] = [
{
kicker: 'دليل حصري · المستوى المتوسط',
index: '01',
title: 'ابنِ وكيلك الأول دون فوضى',
body: 'خريطة عملية لتقسيم المهام، اختيار الأدوات، وضبط ذاكرة الوكيل قبل كتابة الكود.',
format: <><span className="play" aria-hidden="true">▶</span>فيديو · 28 دقيقة</>,
link: { href: '/posts/agent-loop-stop-condition', label: 'شاهد المعاينة ←' },
},
{
kicker: 'مقال تطبيقي · للمبتدئين',
index: '02',
title: 'كيف تختار نموذجاً لمهمتك؟',
body: 'مقارنة منهجية حسب الجودة، التكلفة، السرعة، والخصوصية — من دون ضجيج تسويقي.',
format: <>قراءة · 12 دقيقة</>,
link: { href: '/posts/article-agent-guide', label: 'ابدأ القراءة ←' },
},
{
kicker: 'مسار متقدم · للأعضاء',
index: '03',
title: <>من <bdi dir="ltr">RAG</bdi> إلى إجابة موثوقة</>,
body: 'صمّم الاسترجاع، اختبر المصادر، وافهم أين تنهار الإجابة قبل أن تصل إلى المستخدم.',
format: <><span className="play" aria-hidden="true">▶</span>سلسلة · 4 أجزاء</>,
link: { href: '/subscription', label: 'شاهد للأعضاء ←' },
},
]
const DOT_LABELS = ['الشريحة الأولى', 'الشريحة الثانية', 'الشريحة الثالثة']
/* Featured content carousel. Manual selection pauses rotation until resumed. */
export default function CatalogDeck({ slides = V1_SLIDES }: { slides?: DeckSlide[] }) {
const [activeSlide, setActiveSlide] = useState(0)
const [ready, setReady] = useState(false)
const [playing, setPlaying] = useState(true)
const [hovered, setHovered] = useState(false)
const [focused, setFocused] = useState(false)
const current = slides.length ? activeSlide % slides.length : 0
const advancing = ready && playing && !hovered && !focused && slides.length > 1
useEffect(() => {
try {
const stored = Number(localStorage.getItem('midad-slide'))
if (Number.isInteger(stored) && stored >= 0) setActiveSlide(stored)
} catch { /* Persistence is optional. */ }
const motion = window.matchMedia('(prefers-reduced-motion: reduce)')
if (motion.matches) setPlaying(false)
const onMotion = () => { if (motion.matches) setPlaying(false) }
motion.addEventListener('change', onMotion)
setReady(true)
return () => motion.removeEventListener('change', onMotion)
}, [])
useEffect(() => {
if (!ready) return
try { localStorage.setItem('midad-slide', String(current)) } catch { /* optional persistence */ }
}, [current, ready])
useEffect(() => {
if (!advancing) return
const timer = setTimeout(() => setActiveSlide(index => (index + 1) % slides.length), 6500)
return () => clearTimeout(timer)
}, [advancing, current, slides.length])
function selectSlide(index: number) {
setActiveSlide((index + slides.length) % slides.length)
setPlaying(false)
}
if (!slides.length) return null
return (
<div className="deck" data-od-id="featured-deck" role="region" aria-roledescription="عرض شرائح" aria-label="مختارات مميزة"
onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}
onFocusCapture={() => setFocused(true)}
onBlurCapture={event => { if (!event.currentTarget.contains(event.relatedTarget)) setFocused(false) }}
onKeyDown={event => {
if (event.key === 'ArrowLeft') { event.preventDefault(); selectSlide(current + 1) }
if (event.key === 'ArrowRight') { event.preventDefault(); selectSlide(current - 1) }
}}>
<div className="deck-frame" aria-live={advancing ? 'off' : 'polite'}>
{slides.map((s, i) => (
<article className={`deck-slide${i === current ? ' active' : ''}`} hidden={i !== current} aria-label={`${i + 1} من ${slides.length}`} data-slide={i} key={i}>
<div className="deck-slide-top"><span className="deck-slide-kicker">{s.kicker}</span><span className="deck-slide-index num">{s.index}</span></div>
<div className="deck-slide-body"><h2>{s.title}</h2><p>{s.body}</p></div>
<div className="deck-slide-foot">
<span className="deck-slide-format">{s.format}</span>
{s.link
? <Link className="deck-slide-link" href={s.link.href}>{s.link.label}</Link>
: <span className="meta">{s.meta}</span>}
</div>
</article>
))}
</div>
<div className="deck-controls">
<div className="deck-dots" aria-label="اختيار الشريحة">
{slides.map((_, i) => (
<button type="button" className={`deck-dot${i === current ? ' active' : ''}`} data-dot={i} aria-label={DOT_LABELS[i] ?? `الشريحة ${i + 1}`} aria-current={i === current ? 'true' : undefined} onClick={() => selectSlide(i)} key={i}></button>
))}
</div>
<div className="deck-arrows">
<button type="button" className="icon-btn" id="prevSlide" aria-label="الشريحة السابقة" disabled={slides.length < 2} onClick={() => selectSlide(current - 1)}>→</button>
<button type="button" className="icon-btn" id="nextSlide" aria-label="الشريحة التالية" disabled={slides.length < 2} onClick={() => selectSlide(current + 1)}>←</button>
{slides.length > 1 && <button type="button" className="icon-btn deck-play" aria-label={playing ? 'إيقاف العرض التلقائي' : 'تشغيل العرض التلقائي'} onClick={() => setPlaying(value => !value)}>{playing ? 'Ⅱ' : '▶'}</button>}
</div>
</div>
</div>
)
}
@@ -0,0 +1,44 @@
'use client'
import { useEffect } from 'react'
/* Filter bar for the editors' picks. Cards carry space-separated topics. */
export default function CatalogFilters() {
useEffect(() => {
const controller = new AbortController()
const filters = [...document.querySelectorAll('.filter')] as HTMLElement[]
const cards = [...document.querySelectorAll('.catalog-card')] as HTMLElement[]
const empty = document.getElementById('emptyState')
function filterCatalog(topic: string) {
let visible = 0
filters.forEach(button => {
const selected = (button as HTMLElement).dataset.filter === topic
button.classList.toggle('active', selected)
button.setAttribute('aria-pressed', String(selected))
})
cards.forEach(card => {
const show = topic === 'all' || (card.dataset.topic || '').split(' ').includes(topic)
card.hidden = !show
if (show) visible += 1
})
empty?.classList.toggle('show', visible === 0)
}
filters.forEach(button => button.addEventListener('click', () => {
button.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })
filterCatalog((button as HTMLElement).dataset.filter!)
}, { signal: controller.signal }))
return () => controller.abort()
}, [])
return (
<div className="filterbar" role="group" aria-label="تصفية المحتوى" data-od-id="catalog-filters">
<button type="button" className="filter active" data-filter="all" aria-pressed="true">الكل</button>
<button type="button" className="filter" data-filter="agents" aria-pressed="false">الوكلاء</button>
<button type="button" className="filter" data-filter="models" aria-pressed="false">النماذج</button>
<button type="button" className="filter" data-filter="automation" aria-pressed="false">الأتمتة</button>
<button type="button" className="filter" data-filter="foundations" aria-pressed="false">الأساسيات</button>
</div>
)
}
+9
View File
@@ -0,0 +1,9 @@
import { redirect } from 'next/navigation'
/* QA-07: /catalog was a stale duplicate library — /articles is canonical.
Deck/filter components in this folder are still used by the homepage. */
export const dynamic = 'force-dynamic'
export default function CatalogPage() {
redirect('/articles')
}
@@ -0,0 +1,8 @@
import type { Metadata } from 'next'
/* QA-06: page-specific title (the page itself is a client component). */
export const metadata: Metadata = { title: 'استعادة كلمة المرور' }
export default function ForgotPasswordLayout({ children }: { children: React.ReactNode }) {
return children
}
@@ -0,0 +1,62 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import '../styles/forgot-password.css'
export default function ForgotPasswordPage() {
const [email, setEmail] = useState('')
const [status, setStatus] = useState('')
const [busy, setBusy] = useState(false)
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const form = e.currentTarget
if (!form.checkValidity()) {
setStatus('أدخل بريداً إلكترونياً صحيحاً.')
form.reportValidity()
return
}
setBusy(true)
setStatus('')
try {
const response = await fetch('/api/users/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
})
if (!response.ok) throw new Error('Request failed')
setStatus('إذا كان البريد مسجلاً، فسيصلك رابط الاستعادة.')
setEmail('')
} catch {
setStatus('تعذّر إرسال الطلب. حاول مرة أخرى بعد قليل.')
} finally {
setBusy(false)
}
}
return (
<div id="site" className="site-page page-forgot-password">
<Topnav />
<main id="content" className="reset" data-od-id="password-reset">
<section className="reset-card" data-od-id="reset-card">
<p className="eyebrow">استعادة الحساب</p>
<h1 data-od-id="reset-title">استعادة كلمة المرور</h1>
<p className="lead">أدخل البريد الإلكتروني المرتبط بحسابك لتصلك رسالة إعادة تعيين كلمة المرور.</p>
<form id="resetForm" method="post" action="/api/users/forgot-password" noValidate onSubmit={onSubmit} data-od-id="reset-form">
<div className="field">
<label htmlFor="resetEmail">البريد الإلكتروني</label>
<input className="input" id="resetEmail" name="email" type="email" inputMode="email" autoComplete="email" dir="ltr" required value={email} onChange={e => setEmail(e.target.value)} />
</div>
<button className="btn btn-primary" type="submit" data-od-id="reset-submit" disabled={busy}>{busy ? 'جارٍ الإرسال…' : 'إرسال رابط الاستعادة'}</button>
<p className="status" id="resetStatus" role="status" aria-live="polite">{status}</p>
</form>
<Link className="back" href="/login">تذكرت كلمة المرور؟ سجّل الدخول</Link>
</section>
</main>
<Footer />
</div>
)
}
+8
View File
@@ -0,0 +1,8 @@
import type { Metadata } from 'next'
/* QA-06: page-specific title (the page itself is a client component). */
export const metadata: Metadata = { title: 'تسجيل الدخول' }
export default function LoginLayout({ children }: { children: React.ReactNode }) {
return children
}
+94
View File
@@ -0,0 +1,94 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import { safeNext } from '@/lib/next'
import '../styles/login.css'
export default function LoginPage() {
const router = useRouter()
const [showPassword, setShowPassword] = useState(false)
const [status, setStatus] = useState('')
const [busy, setBusy] = useState(false)
const [nextPath, setNextPath] = useState<string | null>(null)
useEffect(() => {
setNextPath(safeNext(new URLSearchParams(window.location.search).get('next')))
}, [])
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const form = e.currentTarget
if (!form.checkValidity()) {
setStatus('تحقق من البريد الإلكتروني وكلمة المرور.')
form.reportValidity()
return
}
setBusy(true)
setStatus('')
const data = new FormData(form)
try {
const res = await fetch('/api/users/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: data.get('email'), password: data.get('password') }),
})
if (res.ok) {
router.refresh()
router.push(safeNext(new URLSearchParams(window.location.search).get('next')) ?? '/account-settings')
return
}
setStatus('تعذّر تسجيل الدخول — تحقّق من البريد وكلمة المرور.')
} catch {
setStatus('تعذّر الاتصال. حاول تسجيل الدخول مرة أخرى.')
} finally {
setBusy(false)
}
}
return (
<div id="site" className="site-page page-login">
<Topnav />
<main id="content" className="auth">
<section className="auth-story">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="auth-mark" src="/brand/mark-light.svg" alt="" width={436} height={382} />
<h1>أكمل من حيث توقفت.</h1>
<p>سجّل الدخول لمتابعة قراءة الأدلة والمشاركة في النقاشات.</p>
</section>
<section className="auth-form-wrap" data-od-id="login-form-section">
<form className="auth-form" id="loginForm" method="post" action="/api/users/login" noValidate onSubmit={onSubmit} data-od-id="login-form">
<p className="eyebrow">أهلاً بعودتك</p>
<h2 data-od-id="login-title">تسجيل الدخول</h2>
<p className="lead">استخدم بريدك وكلمة المرور للوصول إلى حسابك.</p>
<div className="field">
<label htmlFor="email">البريد الإلكتروني</label>
<input className="input" id="email" name="email" type="email" inputMode="email" autoComplete="email" placeholder="name@example.com" dir="ltr" required onInvalid={e => e.currentTarget.setCustomValidity('أدخل بريداً إلكترونياً صحيحاً.')} onInput={e => e.currentTarget.setCustomValidity('')} />
</div>
<div className="field">
<label htmlFor="password">كلمة المرور</label>
<div className="input-wrap">
<input className="input" id="password" name="password" type={showPassword ? 'text' : 'password'} autoComplete="current-password" minLength={8} required onInvalid={e => e.currentTarget.setCustomValidity('أدخل كلمة المرور.')} onInput={e => e.currentTarget.setCustomValidity('')} />
<button className="password-toggle" id="passwordToggle" type="button" aria-controls="password" aria-label={showPassword ? 'إخفاء كلمة المرور' : 'إظهار كلمة المرور'} aria-pressed={showPassword} onClick={() => setShowPassword(v => !v)}>{showPassword ? 'إخفاء' : 'إظهار'}</button>
</div>
</div>
<div className="form-row">
<label className="check"><input type="checkbox" name="remember" /> تذكّرني</label>
<Link href="/forgot-password">نسيت كلمة المرور؟</Link>
</div>
<button className="btn btn-primary" type="submit" disabled={busy}>{busy ? 'جارٍ تسجيل الدخول…' : 'ادخل إلى حسابك'}</button>
<div className="status" id="loginStatus" role="status">{status}</div>
<div className="divider">أو</div>
<Link className="magic" href="/forgot-password">استعد الوصول إلى حسابك</Link>
<p className="signup">ليس لديك حساب؟ <Link href={nextPath ? `/signup?next=${encodeURIComponent(nextPath)}` : '/signup'}>أنشئ حساباً جديداً</Link></p>
<p className="fine">بالمتابعة، أنت توافق على <Link href="/terms">شروط الاستخدام</Link> و<Link href="/privacy">سياسة الخصوصية</Link>.</p>
</form>
</section>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,149 @@
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { safeNext } from '@/lib/next'
type Viewer = { id: number; email: string; interest: string | null; level: string | null; onboarded: boolean } | null
const COPY = ['الخطوة 1 من 3', 'الخطوة 2 من 3', 'الخطوة 3 من 3']
const PERCENTS = ['33٪', '67٪', '100٪']
/* Wizard markup/steps from member-onboarding.html. Interest + level PATCH the
user record when leaving each step (falls back to localStorage if no session). */
export default function OnboardingWizard({ viewer }: { viewer: Viewer }) {
const [step, setStep] = useState(0)
const [interest, setInterest] = useState(viewer?.interest ?? '')
const [level, setLevel] = useState(viewer?.level ?? '')
const [status, setStatus] = useState('')
const [nextPath, setNextPath] = useState<string | null>(null)
useEffect(() => {
try { setNextPath(safeNext(sessionStorage.getItem('anwarpi:signup-next'))) } catch { /* private mode */ }
}, [])
function show(next: number) {
setStep(Math.max(0, Math.min(2, next)))
}
async function persist(fields: Record<string, unknown>) {
try {
localStorage.setItem('anwarpi:member-onboarding', JSON.stringify({ interest, experience: level }))
} catch { /* private mode */ }
if (!viewer) return
await fetch(`/api/users/${viewer.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields),
}).catch(() => null)
}
async function next() {
if (step === 0) {
if (!interest) { setStatus('اختر مساراً واحداً للمتابعة.'); return }
setStatus('')
await persist({ interest })
show(1)
return
}
if (step === 1) {
if (!level) { setStatus('اختر مستوى واحداً للمتابعة.'); return }
setStatus('')
await persist({ level, onboarded: true })
show(2)
}
}
const summary = interest && level ? `اخترت مسار ${interest} بمستوى ${level}. حسابك المجاني جاهز، ويمكنك الترقية الآن أو لاحقاً.` : 'حسابك المجاني جاهز. يمكنك الترقية الآن أو لاحقاً من صفحة العضوية.'
useEffect(() => {
const bar = document.getElementById('progressBar')
if (bar) bar.style.width = `${((step + 1) / 3) * 100}%`
const label = document.getElementById('progressLabel')
if (label) label.textContent = COPY[step]
const percent = document.getElementById('progressPercent')
if (percent) percent.textContent = PERCENTS[step]
}, [step])
return (
<>
<section className="container flow" data-od-id="member-onboarding-flow">
<nav className="step-nav" aria-label="خطوات إعداد الحساب" data-od-id="onboarding-steps">
{[0, 1, 2].map(i => (
<div key={i} className={i === step ? 'active' : i < step ? 'done' : ''} data-step-label={i}>
<span>{['01', '02', '03'][i]}</span><span>{['اهتمامك', 'مستواك', 'خيارات العضوية'][i]}</span>
</div>
))}
</nav>
<form id="memberOnboarding" noValidate onSubmit={e => e.preventDefault()}>
<section className={`panel${step === 0 ? ' active' : ''}`} data-step="0" data-od-id="step-interest">
<h2>أي مسار تريد أن يظهر أولاً؟</h2>
<p className="intro">اختر مساراً واحداً الآن. ستظل بقية المكتبة متاحة لك.</p>
<div className="choices">
{[
['الوكلاء', 'الوكلاء وتنسيقهم', 'التخطيط والتنفيذ والتحقق وتنسيق عدة وكلاء.'],
['RAG', 'التوليد المعزّز بالاسترجاع (RAG)', 'اختيار المصادر وقياس جودة الاسترجاع والإجابة.'],
['الأتمتة', 'الأتمتة للأعمال', 'دمج الذكاء الاصطناعي في تدفقات عمل قابلة للمراقبة.'],
['النماذج المفتوحة', 'النماذج المفتوحة', 'التشغيل المحلي والضبط الدقيق واختيار النموذج المناسب.'],
].map(([value, label, desc]) => (
<label className="choice" key={value}>
<input type="radio" name="interest" value={value} checked={interest === value} onChange={() => { setInterest(value); setStatus('') }} />
<span className="choice-body"><strong>{label}</strong><small>{desc}</small><span className="choice-mark" aria-hidden="true"></span></span>
</label>
))}
</div>
<p className="status" id="interestStatus" role="status" aria-live="polite">{step === 0 ? status : ''}</p>
<div className="actions">
<button className="btn btn-primary" type="button" data-next onClick={next}>تابع إلى مستواك</button>
</div>
</section>
<section className={`panel${step === 1 ? ' active' : ''}`} data-step="1" data-od-id="step-experience">
<h2>ما مستوى خبرتك العملية؟</h2>
<p className="intro">سنستخدمه فقط لترتيب نقطة البداية، لا لإخفاء أي محتوى مجاني.</p>
<div className="choices">
{[
['مبتدئ', 'أبدأ من الأساسيات', 'أستخدم أدوات الذكاء الاصطناعي ولم أبنِ مشروعاً بعد.'],
['متوسط', 'بنيت تجارب بسيطة', 'جرّبت واجهات برمجة التطبيقات (API) أو أتمتة أو تطبيقاً صغيراً.'],
['متقدم', 'أعمل على أنظمة فعلية', 'أبحث عن أنماط أعمق للقياس والموثوقية والتشغيل.'],
].map(([value, label, desc]) => (
<label className="choice" key={value}>
<input type="radio" name="experience" value={value} checked={level === value} onChange={() => { setLevel(value); setStatus('') }} />
<span className="choice-body"><strong>{label}</strong><small>{desc}</small><span className="choice-mark" aria-hidden="true"></span></span>
</label>
))}
</div>
<p className="status" id="experienceStatus" role="status" aria-live="polite">{step === 1 ? status : ''}</p>
<div className="actions">
<button className="btn btn-secondary" type="button" data-back onClick={() => show(0)}>رجوع</button>
<button className="btn btn-primary" type="button" data-next onClick={next}>شاهد خيارات الوصول</button>
</div>
</section>
<section className={`panel${step === 2 ? ' active' : ''}`} data-step="2" data-od-id="step-access">
<p className="eyebrow">اختيارك، بلا ضغط</p>
<h2>ابدأ مجاناً أو افتح محتوى العضوية الكامل.</h2>
<p className="intro" id="summaryCopy">{summary}</p>
<div className="benefits" data-od-id="vip-benefits">
<article className="benefit"><span className="benefit-num">01</span><div><h3>الأدلة المتقدمة كاملة</h3><p>اقرأ ما بعد المعاينة في أدلة الوكلاء وRAG والأتمتة والنماذج.</p></div></article>
<article className="benefit"><span className="benefit-num">02</span><div><h3>ملفات العمل</h3><p>نزّل القوالب وقوائم الفحص المرتبطة بكل دليل.</p></div></article>
<article className="benefit"><span className="benefit-num">03</span><div><h3>نقطة بداية مخصصة</h3><p>إعداد العضوية ترتّب دليلك الأول ورسائلك بناءً على هدفك.</p></div></article>
</div>
<div className="upgrade-note">
<strong>الحساب المجاني لا يختفي.</strong>
<p>يمكنك قراءة الأدلة المجانية وحفظها، والترقية عندما تحتاج الأدلة الحصرية.</p>
</div>
<div className="actions" data-od-id="final-actions">
{nextPath ? (
<Link className="btn btn-primary" href={nextPath} data-od-id="continue-next-cta">تابع إلى وجهتك</Link>
) : (
<>
<Link className="btn btn-secondary" href="/articles?access=free" data-od-id="continue-free-cta">ابدأ بالأدلة المجانية</Link>
<Link className="btn btn-primary" href="/subscription" data-od-id="view-membership-cta">شاهد العضوية والأسعار</Link>
</>
)}
</div>
</section>
</form>
</section>
</>
)
}
@@ -0,0 +1,56 @@
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import { getPayload } from 'payload'
import { headers } from 'next/headers'
import configPromise from '@payload-config'
import Link from 'next/link'
import { redirect } from 'next/navigation'
import OnboardingWizard from './client'
import '../styles/member-onboarding.css'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'إعداد حسابك' }
/* Signup onboarding — design from member-onboarding.html. Interest + level
persist to the user record (backend-aware, per locked decision). */
export default async function OnboardingPage() {
let viewer: { id: number; email: string; interest: string | null; level: string | null; onboarded: boolean } | null = null
try {
const payload = await getPayload({ config: configPromise })
const { user } = await payload.auth({ headers: await headers() })
if (user) {
viewer = {
id: user.id,
email: user.email,
interest: (user.interest as string | null) ?? null,
level: (user.level as string | null) ?? null,
onboarded: !!user.onboarded,
}
}
} catch {
/* render wizard without persistence */
}
if (!viewer) redirect('/login?next=%2Fonboarding')
return (
<div id="site" className="site-page page-onboarding">
<Topnav viewer={viewer} />
<main id="content">
<section className="head" data-od-id="onboarding-hero">
<div className="container">
<p className="eyebrow">حسابك جاهز · إعداد سريع</p>
<h1 data-od-id="onboarding-title">اختر ما تريد تعلّمه أولاً.</h1>
<p className="lead">سؤالان فقط لترتيب المكتبة حول هدفك، ثم نوضح الفرق بين الحساب المجاني والعضوية المدفوعة.</p>
<div className="progress" aria-label="تقدم الإعداد">
<div className="progress-copy"><span id="progressLabel">الخطوة 1 من 3</span><span id="progressPercent">33٪</span></div>
<div className="progress-track"><div className="progress-bar" id="progressBar"></div></div>
</div>
</div>
</section>
<OnboardingWizard viewer={viewer} />
</main>
<Footer />
</div>
)
}
+153
View File
@@ -0,0 +1,153 @@
import { getViewer } from '@/lib/viewer'
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import Link from 'next/link'
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import { TOPICS, topicsFor, INTEREST_MAP } from '@/lib/topics'
import { formatNumber, guidesLabel, minutesLabel, MEMBERSHIP_PRICE } from '@/lib/format'
export const dynamic = 'force-dynamic'
/* Editorial picks for «ابدأ من هنا». */
const START_HERE = ['first-arabic-agent', 'choosing-your-model', 'prototype-to-production', 'simple-arabic-rag', 'safe-agent-guardrails', 'agent-loop-stop-condition']
type Doc = { id: number; title: string; slug: string; dek?: string | null; access: 'free' | 'vip'; category: string; level?: string | null; readingMinutes: number; cover?: { url?: string | null } | number | null }
export default async function HomePage() {
const viewer = await getViewer()
const payload = await getPayload({ config: configPromise })
const { docs: raw } = await payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
depth: 1,
limit: 200,
sort: '-publishedAt',
select: { title: true, slug: true, dek: true, access: true, category: true, level: true, readingMinutes: true, cover: true },
})
const docs = raw as unknown as Doc[]
const freeCount = docs.filter(d => d.access === 'free').length
// Hero personalization: matching the viewer's interest floats to the top,
// level match ranks higher, ties keep the -publishedAt order. Anonymous
// viewers get the plain newest-first list.
const interestTopic = viewer?.interest ? INTEREST_MAP[viewer.interest] : undefined
const score = (d: Doc) => {
if (!interestTopic || !topicsFor(d).includes(interestTopic)) return 0
return d.level === viewer?.level ? 2 : 1
}
const hero = docs
.map((d, i) => ({ d, i, s: score(d) }))
.sort((a, b) => b.s - a.s || a.i - b.i)
.slice(0, 4)
.map(({ d }) => d)
const counts = Object.fromEntries(TOPICS.map(t => [t.key, docs.filter(d => topicsFor(d).includes(t.key)).length]))
const startHere = START_HERE.map(slug => docs.find(d => d.slug === slug)).filter((d): d is Doc => Boolean(d))
return (
<div id="site" className="site-page v2 v2-home">
<Topnav viewer={viewer} />
<main id="content">
<section className="home-hero">
<div className="wrap home-hero-grid">
<div className="home-hero-copy">
<p className="kicker">مكتبة عربية للذكاء الاصطناعي العملي</p>
<h1>افهم الذكاء الاصطناعي، ثم ابنِ به.</h1>
<p className="lede">أدلة عربية عملية عن الوكلاء والنماذج والأتمتة، يكتبها مهندسو بنية تحتية لمن يريد أن يشغّل شيئاً حقيقياً.</p>
<div className="actions">
<a className="btn btn-primary" href="/articles?access=free">ابدأ بدليل مجاني</a>
<Link className="btn btn-quiet" href="/articles">تصفح المكتبة</Link>
</div>
</div>
{hero.length > 0 && (
<aside className="hero-panel" aria-labelledby="new-title">
<h2 id="new-title">أحدث الأدلة</h2>
<ul role="list">
{hero.map(d => {
const coverUrl = typeof d.cover === 'object' ? d.cover?.url ?? null : null
return (
<li key={d.id}>
<Link href={`/posts/${d.slug}`}>
{coverUrl && <img className="hero-panel-thumb" src={coverUrl} alt="" width={72} height={48} loading="lazy" />}
<span className="hero-panel-title">{d.title}</span>
<span className="meta-row"><span>{d.category}</span><span>{minutesLabel(d.readingMinutes)}</span><span className={d.access === 'vip' ? 'is-vip' : undefined}>{d.access === 'free' ? 'مجاني' : 'للأعضاء'}</span></span>
</Link>
</li>
)
})}
</ul>
<Link className="text-link" href="/articles">كل الأدلة</Link>
</aside>
)}
</div>
</section>
<section className="home-paths" id="topics" aria-labelledby="paths-title">
<div className="wrap">
<header className="section-head">
<h2 id="paths-title">اختر مساراً حسب ما تريد بناءه</h2>
<p>كل دليل ينتمي إلى مسار، وأدلة المبتدئين تظهر أيضاً في الأساسيات.</p>
</header>
<ol className="paths" role="list">
{TOPICS.map(topic => (
<li key={topic.key}>
<Link href={`/articles?topic=${topic.key}`} className="path">
<span className="path-name">{topic.label}</span>
<span className="path-hint">{topic.hint}</span>
<span className="path-count">{guidesLabel(counts[topic.key])}</span>
</Link>
</li>
))}
</ol>
</div>
</section>
{startHere.length > 0 && (
<section className="home-start" aria-labelledby="start-title">
<div className="wrap">
<header className="section-head">
<h2 id="start-title">ابدأ من هنا</h2>
<Link className="text-link" href="/articles">كل الأدلة ({formatNumber(docs.length)})</Link>
</header>
<ul className="card-grid" role="list">
{startHere.map(post => {
const coverUrl = typeof post.cover === 'object' ? post.cover?.url ?? null : null
return (
<li key={post.id}>
<article className="guide">
{coverUrl && <img className="guide-card-art" src={coverUrl} alt="" width={640} height={320} loading="lazy" />}
<p className="meta-row"><span className="cat">{post.category}</span><span>{minutesLabel(post.readingMinutes)}</span><span className={post.access === 'vip' ? 'is-vip' : undefined}>{post.access === 'free' ? 'مجاني' : 'للأعضاء'}</span></p>
<h3><Link href={`/posts/${post.slug}`}>{post.title}</Link></h3>
<p className="guide-dek">{post.dek}</p>
<span className="guide-link" aria-hidden="true">اقرأ الدليل</span>
</article>
</li>
)
})}
</ul>
</div>
</section>
)}
<section className="home-membership" id="membership" aria-labelledby="membership-title">
<div className="wrap membership-panel">
<div>
<h2 id="membership-title">العضوية تفتح الأدلة المتقدمة وملفات العمل.</h2>
<p>الأدلة المجانية كاملة دائماً. العضوية تضيف الأدلة المعمّقة، القوالب الجاهزة للتنزيل، والنقاش مع الفريق.</p>
</div>
<div className="membership-offer">
<p className="price"><bdi dir="ltr">{MEMBERSHIP_PRICE}</bdi> <span>شهرياً، وتلغي متى شئت</span></p>
<ul role="list">
<li>{guidesLabel(docs.length - freeCount)} للأعضاء، وتُضاف أدلة جديدة باستمرار</li>
<li>قوالب وقوائم فحص قابلة للتنزيل</li>
<li>التعليق والنقاش على كل دليل</li>
</ul>
<Link className="btn btn-primary" href="/subscription">اطّلع على العضوية</Link>
</div>
</div>
</section>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,37 @@
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import Link from 'next/link'
import '../styles/payment-cancelled.css'
export const metadata = { title: 'لم يكتمل الدفع' }
/* Payment cancelled — design from payment-cancelled.html. */
export default function PaymentCancelledPage() {
return (
<div id="site" className="site-page page-payment-cancelled">
<Topnav />
<main id="content">
<section className="section" data-od-id="payment-cancelled">
<div className="container">
<article className="state-card">
<div className="status-mark" aria-hidden="true"><svg viewBox="0 0 32 32" fill="none" stroke="currentColor" strokeWidth={2}><path d="M9 9l14 14M23 9 9 23" /></svg></div>
<p className="eyebrow">لم يكتمل الدفع</p>
<h1 data-od-id="cancelled-title">لم تُخصم أي دفعة.</h1>
<p className="lead">ألغيت عملية الدفع أو عدت قبل إكمالها. حسابك المجاني وأدلتك المحفوظة لم تتغير.</p>
<div className="notice" data-od-id="account-safety-note">
<h2 data-od-id="account-safety-title">يمكنك المتابعة وقتما يناسبك</h2>
<p>ارجع إلى صفحة العضوية لمراجعة المزايا أو أكمل استخدام المكتبة المجانية الآن.</p>
</div>
<div className="actions">
<Link className="btn btn-primary" href="/subscription" data-od-id="retry-payment-cta">العودة إلى العضوية</Link>
<Link className="btn btn-secondary" href="/articles" data-od-id="browse-free-articles-cta">تصفح الأدلة المجانية</Link>
</div>
<p className="help">واجهت مشكلة أثناء الدفع؟ <a href="mailto:hello@anwarpy.ai?subject=%D9%85%D8%B4%D9%83%D9%84%D8%A9%20%D9%81%D9%8A%20%D8%A7%D9%84%D8%AF%D9%81%D8%B9" data-od-id="payment-help-link">تواصل مع الفريق</a>.</p>
</article>
</div>
</section>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,99 @@
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import Stripe from 'stripe'
import { getPayload } from 'payload'
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import configPromise from '@payload-config'
import Link from 'next/link'
import '../styles/payment-success.css'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'تم تفعيل العضوية' }
type Props = { searchParams: Promise<{ session_id?: string }> }
/* Payment success — design from payment-success.html.
- real Stripe: verifies the checkout session_id with Stripe before
activating (paid + the checkout email matches the signed-in account)
- no session: nobody is activated here — /api/stripe/webhook is the primary
activator; this page only re-verifies a session it can prove */
export default async function PaymentSuccessPage({ searchParams }: Props) {
const { session_id: sessionId } = await searchParams
const stripeKey = process.env.STRIPE_SECRET_KEY
const payload = await getPayload({ config: configPromise })
const { user } = await payload.auth({ headers: await headers() })
if (!user) redirect('/login?next=/payment-success')
let activate = false
if (sessionId && stripeKey) {
try {
const stripe = new Stripe(stripeKey)
const session = await stripe.checkout.sessions.retrieve(sessionId)
activate =
session.payment_status === 'paid' &&
(session.customer_details?.email ?? '').toLowerCase() === user.email.toLowerCase()
} catch {
// Any Stripe failure (bad key, unknown session, network) → no activation.
activate = false
}
}
if (activate && user.role !== 'vip' && user.role !== 'admin') {
await payload.update({
collection: 'users',
id: user.id,
data: { role: 'vip' },
overrideAccess: true,
context: { systemActivation: true },
})
}
const vipNow = user.role === 'vip' || user.role === 'admin' || activate
return (
<div id="site" className="site-page page-payment-success">
<Topnav viewer={user} />
<main id="content">
<section className="section" data-od-id="payment-success">
<div className="container">
<article className="state-card">
<div className="status-mark" aria-hidden="true"><svg viewBox="0 0 32 32" fill="none" stroke="currentColor" strokeWidth={2}><path d="m8 17 5 5 11-13" /></svg></div>
{vipNow ? (
<>
<p className="eyebrow">تم الدفع · العضوية فعّالة</p>
<h1 data-od-id="success-title">مرحباً بك في عضوية أنور بآي.</h1>
<p className="lead">تم تفعيل وصولك إلى الأدلة الحصرية وملفات العمل. بقيت خطوة قصيرة لاختيار نقطة البداية المناسبة لك.</p>
<dl className="summary" data-od-id="payment-summary">
<div className="summary-row"><dt>الحالة</dt><dd>عضوية فعّالة</dd></div>
<div className="summary-row"><dt>الخطة</dt><dd>عضوية فردية · شهرياً · <bdi dir="ltr">$10</bdi></dd></div>
<div className="summary-row"><dt>الوصول</dt><dd>كل محتوى العضوية وملفات الأعضاء</dd></div>
</dl>
<div className="next-step" data-od-id="next-step">
<h2 data-od-id="next-step-title">جهّز مسارك الأول</h2>
<p>حدد هدفك ومستواك في أقل من خمس دقائق، ثم ابدأ بدليل واحد يناسبك.</p>
<div className="actions">
<Link className="btn btn-primary" href="/vip-onboarding" data-od-id="start-onboarding-cta">ابدأ إعداد العضوية</Link>
<Link className="btn btn-secondary" href="/posts/agent-loop-stop-condition" data-od-id="open-member-guide-cta">افتح دليل الأعضاء</Link>
</div>
</div>
</>
) : (
<>
<p className="eyebrow">لم تكتمل عملية الدفع</p>
<h1 data-od-id="payment-incomplete-title">لم يتم تأكيد الدفع بعد</h1>
<p className="lead">لم نتمكن من تأكيد عملية دفع مرتبطة بحسابك. إن أتممت الدفع للتو فقد يحتاج التأكيد بضع دقائق، ويمكنك إعادة المحاولة من صفحة الاشتراك.</p>
<div className="actions">
<Link className="btn btn-primary" href="/subscription" data-od-id="retry-subscription-cta">العودة إلى صفحة الاشتراك</Link>
</div>
</>
)}
</article>
</div>
</section>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,53 @@
'use client'
import { useEffect, useState } from 'react'
/* Views ping — counts one view per browser session per post (global counter
on the backend). */
export function ViewPing({ slug }: { slug: string }) {
useEffect(() => {
const key = `anw-viewed:${slug}`
try {
if (sessionStorage.getItem(key)) return
sessionStorage.setItem(key, '1')
} catch { /* private mode — count anyway */ }
fetch(`/api/posts/${slug}/view`, { method: 'POST', keepalive: true }).catch(() => null)
}, [slug])
return null
}
/* Like toggle — login required; one like per user per post. */
export function LikeButton({ slug, loggedIn, initialLiked, initialCount }: { slug: string; loggedIn: boolean; initialLiked: boolean; initialCount: number }) {
const [liked, setLiked] = useState(initialLiked)
const [count, setCount] = useState(initialCount)
const [busy, setBusy] = useState(false)
async function toggle() {
if (!loggedIn) {
location.href = `/login?next=/posts/${slug}`
return
}
setBusy(true)
const res = await fetch(`/api/posts/${slug}/like`, { method: 'POST' })
if (res.ok) {
const data = await res.json()
setLiked(data.liked)
setCount(data.likes)
}
setBusy(false)
}
return (
<button
className="like"
type="button"
onClick={toggle}
disabled={busy}
aria-pressed={liked}
aria-label={liked ? 'ألغِ الإعجاب' : 'أعجبني'}
>
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13.5 2.6 8.3a3.2 3.2 0 0 1 4.5-4.6L8 4.6l.9-.9a3.2 3.2 0 0 1 4.5 4.6Z" fill="none" stroke="currentColor" strokeWidth="1.4" /></svg>
<span>{count}</span>
</button>
)
}
@@ -0,0 +1,400 @@
import { getPayload } from 'payload'
import { notFound } from 'next/navigation'
import Link from 'next/link'
import configPromise from '@payload-config'
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import PostExtras from './post-extras'
import { LikeButton, ViewPing } from './engagement'
import { getViewer } from '@/lib/viewer'
import { TOPICS, topicsFor } from '@/lib/topics'
import { formatDate, formatNumber, minutesLabel, MEMBERSHIP_PRICE } from '@/lib/format'
export const dynamic = 'force-dynamic'
/* QA-06: article pages get their own title/description. */
export async function generateMetadata({ params }: Args) {
const { slug } = await params
const payload = await getPayload({ config: configPromise })
const res = await payload.find({
collection: 'posts',
where: { slug: { equals: slug } },
depth: 0,
limit: 1,
select: { title: true, dek: true, publishedAt: true },
})
const post = res.docs[0] as unknown as { title: string; dek: string; publishedAt?: string | null } | undefined
return post
? {
title: post.title,
description: post.dek,
openGraph: { type: 'article', title: post.title, description: post.dek, publishedTime: post.publishedAt ?? undefined },
}
: { title: 'أنور بآي' }
}
type Args = { params: Promise<{ slug: string }> }
type PostDoc = {
id: number
title: string
slug: string
dek: string
access: 'free' | 'vip'
category: string
level?: string | null
views?: number | null
likes?: number | null
readingMinutes: number
publishedAt?: string | null
teaser?: string | null
content?: unknown
cover?: { url?: string | null }
attachments?: (number | { id: number })[]
}
async function isLikedByViewer(postId: number, viewerId?: number): Promise<boolean> {
try {
if (!viewerId) return false
const payload = await getPayload({ config: configPromise })
const { docs } = await payload.find({
collection: 'post-likes',
where: { and: [{ user: { equals: viewerId } }, { post: { equals: postId } }] },
depth: 0,
limit: 1,
overrideAccess: true,
})
return docs.length > 0
} catch {
return false
}
}
export default async function PostPage({ params }: Args) {
const { slug } = await params
const payload = await getPayload({ config: configPromise })
const res = await payload.find({
collection: 'posts',
where: { and: [{ slug: { equals: slug } }, { status: { equals: 'published' } }] },
depth: 1,
limit: 1,
// content selected only for entitled viewers — never reaches non-VIP clients
select: {
title: true, slug: true, dek: true, access: true, category: true, level: true,
views: true, likes: true,
readingMinutes: true, publishedAt: true, teaser: true, cover: true,
content: true, attachments: true,
},
})
const post = res.docs[0] as unknown as PostDoc | undefined
if (!post) notFound()
// One auth lookup per request; everything below reads from it.
const viewer = await getViewer()
const role = viewer?.role ?? null
const viewerId = viewer?.id
const viewerLevel = viewer?.level ?? null
const unlocked = post.access === 'free' || role === 'vip' || role === 'admin'
const postTopics = topicsFor(post)
// The downloadable templates are agent-workflow files; only offer them on agent guides.
const hasAgentFiles = postTopics.includes('agents')
const liked = await isLikedByViewer(post.id, viewerId)
// Attachment metadata (label/access) is safe to expose — the bytes are
// gated by GET /api/files/[id]. Fetched with overrideAccess because the
// files collection read rule is vip/admin and depth population would
// otherwise collapse to bare ids for anonymous/free viewers.
const attachmentIds = (post.attachments ?? []).map(a => (typeof a === 'object' ? a.id : a))
type Attachment = { id: number; label: string; filename: string; access: 'free' | 'vip' }
let attachments: Attachment[] = []
if (attachmentIds.length > 0) {
const filesRes = await payload.find({
collection: 'files',
depth: 0,
limit: 20,
overrideAccess: true,
where: { id: { in: attachmentIds } },
select: { label: true, filename: true, access: true },
})
const fileDocs = filesRes.docs as unknown as Attachment[]
attachments = attachmentIds
.map(id => fileDocs.find(d => d.id === id))
.filter((d): d is Attachment => Boolean(d))
}
// «اقرأ بعد ذلك»: real published guides, same learning path first.
const { docs: others } = await payload.find({
collection: 'posts',
where: { and: [{ status: { equals: 'published' } }, { slug: { not_equals: post.slug } }] },
depth: 0,
limit: 100,
sort: '-publishedAt',
select: { title: true, slug: true, category: true, access: true, readingMinutes: true, level: true },
})
const related = [...others]
.sort((a, b) => Number(topicsFor(b).includes(postTopics[0])) - Number(topicsFor(a).includes(postTopics[0])))
.slice(0, 3)
// Approved comments (public read access allows the approved-only filter).
const comments = await payload.find({
collection: 'comments',
depth: 0,
limit: 50,
sort: '-createdAt',
where: { and: [{ post: { equals: post.id } }, { status: { equals: 'approved' } }] },
})
// Authors see their own pending comments tagged «قيد المراجعة» — needs
// overrideAccess because non-admin read access filters to approved only.
const myPending = viewerId
? await payload.find({
collection: 'comments',
depth: 0,
limit: 10,
overrideAccess: true,
where: {
and: [
{ post: { equals: post.id } },
{ author: { equals: viewerId } },
{ status: { equals: 'pending' } },
],
},
})
: null
// TOC from content h2s (server-extracted from the Lexical tree)
const toc = unlocked ? extractH2s(post.content) : []
const topic = postTopics[0]
const topicLabel = TOPICS.find(t => t.key === topic)?.label ?? post.category
return (
<div id="site" className="site-page v2 v2-article page-posts">
<ViewPing slug={post.slug} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', headline: post.title, datePublished: post.publishedAt ?? undefined, author: { '@type': 'Organization', name: 'أنور بآي' }, inLanguage: 'ar' }) }} />
<Topnav viewer={viewer} />
<main id="content">
<header className="art-head">
<div className="wrap art-head-grid">
<div className="art-head-copy">
<nav className="crumbs" aria-label="مسار التصفح">
<Link href="/articles">المكتبة</Link>
<span aria-hidden="true">/</span>
<Link href={`/articles?topic=${topic}`}>{topicLabel}</Link>
</nav>
<h1>{post.title}</h1>
<p className="lede">{post.dek}</p>
<div className="art-meta">
<span className={`badge${post.access === 'vip' ? ' badge-vip' : ''}`}>
{post.access === 'vip' && <LockIcon />}
{post.access === 'free' ? 'مجاني بالكامل' : unlocked ? 'للأعضاء' : 'معاينة مجانية'}
</span>
<span>{post.category}</span>
<span>{minutesLabel(post.readingMinutes)} للقراءة</span>
<span>{post.level ?? 'مبتدئ'}</span>
{post.publishedAt && <time dateTime={post.publishedAt}>{formatDate(post.publishedAt)}</time>}
{(post.level ?? 'مبتدئ') === viewerLevel && <span className="for-you">يناسب مستواك</span>}
</div>
<div className="art-actions">
<LikeButton slug={post.slug} loggedIn={role !== null} initialLiked={liked} initialCount={post.likes ?? 0} />
<span className="views" data-views>{formatNumber(post.views)} قراءة</span>
{unlocked && post.access === 'vip' && <SaveButton />}
</div>
</div>
</div>
</header>
{typeof post.cover === 'object' && post.cover?.url && (
<div className="wrap"><figure className="art-cover"><img src={post.cover.url} alt={post.title} width={1200} height={630} loading="eager" /></figure></div>
)}
<div className="wrap art-layout">
<article className="art-body">
{unlocked ? (
<RichText data={post.content} />
) : (
<>
{(post.teaser || '').split(/\n{2,}/).map((para, i, arr) => (
i === arr.length - 1 && arr.length > 1
? <p key={i} className="fade-out">{para}</p>
: <p key={i} className={i === 0 ? 'intro' : undefined}>{para}</p>
))}
<section className="paywall" aria-labelledby="paywall-title">
<h2 id="paywall-title">بقية هذا الدليل للأعضاء</h2>
<p>قرأت المقدمة المجانية. العضوية تفتح بقية الأقسام العملية{hasAgentFiles ? '، وقالب حالة الوكيل وقائمة الفحص قبل التشغيل' : ''}، وكل الأدلة الحصرية الأخرى.</p>
<div className="paywall-actions">
<a className="btn btn-primary" href="/subscription">اشترك بـ <bdi dir="ltr">{MEMBERSHIP_PRICE}</bdi> شهرياً</a>
<a className="btn btn-quiet" href={`/login?next=/posts/${post.slug}`}>لدي عضوية، تسجيل الدخول</a>
</div>
<p className="fine">تلغي في أي وقت من صفحة حسابك.</p>
</section>
</>
)}
</article>
<aside className="art-aside" aria-label="داخل هذا الدليل">
{unlocked && toc.length > 0 && (
<nav className="toc" aria-label="فهرس الدليل">
<p className="aside-title">في هذا الدليل</p>
<ol>{toc.map((t, i) => <li key={i}><a href={`#sec-${i}`}>{t}</a></li>)}</ol>
</nav>
)}
{unlocked && hasAgentFiles && (
<div className="files">
<p className="aside-title">ملفات العمل</p>
<ul>
<li><span>قالب حالة الوكيل <bdi dir="ltr">.md</bdi></span><button className="dl-btn" type="button" data-dl="agent-state.md">تنزيل</button></li>
<li><span>قائمة الفحص قبل التشغيل <bdi dir="ltr">.md</bdi></span><button className="dl-btn" type="button" data-dl="pre-run-checklist.md">تنزيل</button></li>
<li><span>سجل الأخطاء <bdi dir="ltr">.csv</bdi></span><button className="dl-btn" type="button" data-dl="error-log.csv">تنزيل</button></li>
</ul>
</div>
)}
{!unlocked && (
<div className="files is-locked">
<p className="aside-title">مع العضوية</p>
<ul>
<li><LockIcon />الأقسام العملية كاملة</li>
{hasAgentFiles && <li><LockIcon />ملفات العمل القابلة للتنزيل</li>}
<li><LockIcon />النقاش مع الفريق</li>
</ul>
</div>
)}
{attachments.length > 0 && (
<div className="files">
<p className="aside-title">ملفات هذا الدليل</p>
<ul>
{attachments.map(item => {
// Per-file gate mirrors GET /api/files/[id]: free streams to
// anyone; vip needs a vip/admin viewer (post access is
// irrelevant — a vip file on a free post stays locked).
const canDownload = item.access === 'free' || role === 'vip' || role === 'admin'
const accessLabel = item.access === 'free' ? 'مجاني' : 'للأعضاء'
return (
<li key={item.id}>
<span>{item.label || item.filename} <span className="file-access">· {accessLabel}</span></span>
{canDownload
? <a className="dl-btn" href={`/api/files/${item.id}`} download>تنزيل</a>
: <span className="dl-locked"><LockIcon />للأعضاء</span>}
</li>
)
})}
</ul>
</div>
)}
</aside>
</div>
{unlocked && (
<section className="art-comments" aria-labelledby="comments-title">
<div className="wrap narrow">
<h2 id="comments-title">النقاش</h2>
<div className="comment-list" id="commentList">
{comments.docs.map((c) => (
<article key={c.id} className="comment">
<header><strong>{c.authorName || 'عضو'}</strong><time dateTime={String(c.createdAt)}>{formatDate(String(c.createdAt))}</time></header>
<p>{c.body}</p>
</article>
))}
{myPending?.docs.map((c) => (
<article key={c.id} className="comment is-pending">
<header><strong>{c.authorName || 'عضو'}</strong><span>بانتظار المراجعة</span></header>
<p>{c.body}</p>
</article>
))}
{comments.docs.length === 0 && !(myPending && myPending.docs.length > 0) && (
<p className="muted">لا تعليقات بعد. شارك سؤالك أو تجربتك مع هذا الدليل.</p>
)}
</div>
<PostExtras slug={post.slug} loggedIn={role !== null} />
</div>
</section>
)}
{related.length > 0 && (
<section className="art-related" aria-labelledby="related-title">
<div className="wrap">
<h2 id="related-title">اقرأ بعد ذلك</h2>
<ul className="related-grid" role="list">
{related.map(item => (
<li key={item.id}>
<Link href={`/posts/${item.slug}`} className="related-item">
<span className="meta-row"><span>{item.category}</span><span>{minutesLabel(item.readingMinutes)}</span><span>{item.access === 'free' ? 'مجاني' : 'للأعضاء'}</span></span>
<span className="related-title">{item.title}</span>
</Link>
</li>
))}
</ul>
</div>
</section>
)}
</main>
<Footer />
</div>
)
}
function LockIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true"><rect x="3" y="7" width="10" height="7" /><path d="M5 7V5a3 3 0 0 1 6 0v2" /></svg>
)
}
function SaveButton() {
return <button className="save-btn" id="saveArticle" type="button" aria-pressed="false">احفظ للعودة لاحقاً</button>
}
// Minimal Lexical/JSON renderer — headings, paragraphs, lists, quotes, code, rules.
type Node = { type?: string; text?: string; children?: Node[]; tag?: string; url?: string; listType?: string; code?: string }
function renderChildren(nodes: Node[] | undefined, counter: { h2: number }): React.ReactNode[] {
return (nodes || []).map((n, i) => {
const key = `${n.type}-${i}`
if (n.type === 'text') {
const text = n.text || ''
return <span key={key}>{text}</span>
}
if (n.type === 'linebreak') return <br key={key} />
if (n.type === 'link') return <a key={key} href={n.url || '#'} className="text-link">{renderChildren(n.children, counter)}</a>
const inner = renderChildren(n.children, counter)
switch (n.type) {
case 'heading': {
const Tag = (n.tag || 'h2') as 'h1' | 'h2' | 'h3' | 'h4'
const id = Tag === 'h2' ? `sec-${counter.h2++}` : undefined
return <Tag key={key} id={id}>{inner}</Tag>
}
case 'quote': return <blockquote key={key}>{inner}</blockquote>
case 'list': {
if (n.listType === 'number') return <ol key={key}>{inner}</ol>
return <ul key={key}>{inner}</ul>
}
case 'listitem': return <li key={key}>{inner}</li>
case 'paragraph': return <p key={key}>{inner}</p>
case 'code': return <pre key={key} aria-label="مثال برمجي"><code>{n.code ?? innerText(n.children)}</code></pre>
case 'horizontalrule': return <hr key={key} />
default: return <p key={key}>{inner}</p>
}
})
}
function innerText(nodes?: Node[]): string {
return (nodes || []).map(n => (n.type === 'text' ? n.text || '' : innerText(n.children))).join('')
}
function extractH2s(data: unknown): string[] {
try {
const root = (data as { root?: { children?: Node[] } })?.root
if (!root?.children) return []
return root.children
.filter(n => n.type === 'heading' && (n.tag || 'h2') === 'h2')
.map(n => innerText(n.children))
} catch {
return []
}
}
function RichText({ data }: { data: unknown }) {
if (!data || typeof data !== 'object' || !('root' in data)) return null
const rootUnknown = data.root
if (!rootUnknown || typeof rootUnknown !== 'object' || !('children' in rootUnknown)) return null
const root = rootUnknown as { children?: Node[] } // named const: same JSON tree the editor produced
return <>{renderChildren(root.children, { h2: 0 })}</>
}
@@ -0,0 +1,158 @@
'use client'
import { useEffect, useRef, useState } from 'react'
/* Save button + file download buttons + comment form — client behaviors
ported from post-unlocked.html (client-side only, faithful to the original). */
export default function PostExtras({ slug, loggedIn }: { slug: string; loggedIn: boolean }) {
useEffect(() => {
const save = document.getElementById('saveArticle')
const onSave = () => {
if (!save) return
const active = save.getAttribute('aria-pressed') !== 'true'
save.setAttribute('aria-pressed', String(active))
save.textContent = active ? 'تم الحفظ ✓' : 'احفظ للعودة لاحقاً'
}
save?.addEventListener('click', onSave)
/* QA-02: download buttons generate real files (client-side, no backend
needed) with proper names and content types. */
const DL_FILES: Record<string, { name: string; type: string; body: string }> = {
'agent-state.md': {
name: 'anwarpi-agent-state-template.md',
type: 'text/markdown;charset=utf-8',
body: [
'# قالب حالة الوكيل',
'',
'> انسخ هذا القالب لكل وكيل تبنيه؛ الحقول الفارغة تعني منطقاً ناقصاً.',
'',
'## الهدف الواحد',
'- ما الذي ينجزه الوكيل بالضبط؟',
'',
'## حدود الحلقة',
'- أقصى عدد خطوات: ',
'- شرط التوقف: ',
'- ما لا يفعله الوكيل: ',
'',
'## سجل الحالة',
'| الحقل | القيمة | آخر تحديث |',
'| --- | --- | --- |',
'| المهمة الحالية | | |',
'| الخطوة | | |',
'| الملاحظات | | |',
'',
'## الأدوات والصلاحيات',
'- الأداة: الصلاحية الممنوحة: التأكيد البشري المطلوب: ',
'',
'## سجل التدقيق',
'- الوقت: الفعل: النتيجة: ',
].join('\n'),
},
'pre-run-checklist.md': {
name: 'anwarpi-pre-run-checklist.md',
type: 'text/markdown;charset=utf-8',
body: [
'# قائمة الفحص قبل التشغيل',
'',
'- [ ] الهدف مكتوب في جملة واحدة قابلة للقياس',
'- [ ] شرط توقف واضح يمنع الحلقة اللانهائية',
'- [ ] كل أداة لها صلاحية محددة وشخص مسؤول عن الموافقة',
'- [ ] بيانات الاختبار جاهزة وليست بيانات إنتاج',
'- [ ] سجل تدقيق يكتب كل فعل يقوم به الوكيل',
'- [ ] سقف تكلفة وزمن محدد لكل تشغيل',
'- [ ] خطة مراجعة بشرية للمخرجات قبل نشرها',
].join('\n'),
},
'error-log.csv': {
name: 'anwarpi-error-log.csv',
type: 'text/csv;charset=utf-8',
body: '\uFEFF' + [
'الوقت,المرحلة,نوع الخطأ,الوصف,الإجراء',
',استرجاع,,,',
',تنفيذ,,,',
',تحقق,,,',
].join('\n'),
},
}
const dlHandler = (e: Event) => {
const b = e.currentTarget as HTMLButtonElement
const file = DL_FILES[b.dataset.dl ?? '']
if (file) {
const blob = new Blob([file.body], { type: file.type })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = file.name
document.body.append(a)
a.click()
a.remove()
setTimeout(() => URL.revokeObjectURL(url), 1000)
}
b.textContent = 'تم التنزيل ✓'
setTimeout(() => { b.textContent = 'تنزيل' }, 1600)
}
document.querySelectorAll('[data-dl]').forEach(b => b.addEventListener('click', dlHandler))
return () => {
save?.removeEventListener('click', onSave)
document.querySelectorAll('[data-dl]').forEach(b => b.removeEventListener('click', dlHandler))
}
}, [])
const [status, setStatus] = useState('')
const [busy, setBusy] = useState(false)
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const form = e.currentTarget
if (!form.checkValidity()) {
setStatus('اكتب تعليقاً من 8 أحرف على الأقل.')
form.reportValidity()
return
}
const text = (form.elements.namedItem('comment') as HTMLTextAreaElement).value.trim()
setBusy(true)
setStatus('')
try {
const res = await fetch(`/api/posts/${slug}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: text }),
})
const data = await res.json().catch(() => null)
if (res.status === 201) {
form.reset()
setStatus('شكراً — وصل تعليقك وسيظهر بعد المراجعة.')
} else if (res.status === 401) {
setStatus('سجّل الدخول كي تعلّق.')
} else {
setStatus(data?.error || 'تعذّر الإرسال — حاول مجدداً.')
}
} catch {
setStatus('تعذّر الإرسال — حاول مجدداً.')
} finally {
setBusy(false)
}
}
if (!loggedIn) {
return (
<div className="comment-gate">
<p>سجّل الدخول لتشارك سؤالك أو تجربتك مع هذا الدليل.</p>
<a className="btn btn-quiet" href={`/login?next=/posts/${slug}`}>سجّل الدخول للتعليق</a>
<p>لا تملك حساباً؟ <a href={`/signup?next=/posts/${slug}`}>أنشئ حساباً مجاناً</a></p>
</div>
)
}
return (
<form className="comment-form" id="commentForm" method="post" noValidate onSubmit={onSubmit}>
<div className="field">
<label htmlFor="commentBody">تعليقك</label>
<textarea className="textarea" id="commentBody" name="comment" required minLength={8} maxLength={2000}></textarea>
</div>
<button className="btn btn-primary" type="submit" disabled={busy}>{busy ? 'جارٍ الإرسال…' : 'أرسل التعليق'}</button>
<p className="status" id="commentStatus" role="status">{status}</p>
</form>
)
}
+32
View File
@@ -0,0 +1,32 @@
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import '../styles/privacy.css'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'الخصوصية' }
export default function PrivacyPage() {
return (
<div id="site" className="site-page page-privacy">
<Topnav />
<main id="content">
<section className="hero" data-od-id="privacy-hero">
<div className="container">
<p className="eyebrow">الخصوصية · مسودة ما قبل الإطلاق</p>
<h1 data-od-id="privacy-title">بياناتك ليست مادة للمحتوى.</h1>
<p className="lead">توضح هذه المسودة الحد الأدنى المقترح لجمع البيانات واستخدامها داخل أنور بآي.</p>
<p className="notice" role="note">هذه سياسة تجريبية للواجهة وليست نصاً قانونياً نهائياً. يجب مراجعتها واعتمادها قبل نشر الموقع.</p>
</div>
</section>
<div className="container policy" data-od-id="privacy-content">
<section data-od-id="privacy-data"><h2>ما الذي نجمعه؟</h2><p>عند تشغيل الحسابات فعلياً قد نحتاج إلى الاسم والبريد الإلكتروني، وبيانات العضوية والفوترة التي يوفرها مزوّد الدفع. لا ينبغي تخزين بيانات البطاقة كاملة داخل المنصة.</p></section>
<section data-od-id="privacy-use"><h2>كيف تُستخدم البيانات؟</h2><ul><li>تسجيل الدخول وحفظ التقدّم والتفضيلات.</li><li>إدارة العضوية وإرسال الرسائل التي اختارها المستخدم.</li><li>حماية المنصة وتشخيص الأعطال وتحسين تجربة القراءة.</li></ul></section>
<section data-od-id="privacy-sharing"><h2>المشاركة والاحتفاظ</h2><p>لا تُباع البيانات الشخصية. قبل الإطلاق يجب تسمية مزوّدي الاستضافة والبريد والدفع والتحليلات، وتحديد مدة الاحتفاظ بكل نوع من البيانات بوضوح.</p></section>
<section data-od-id="privacy-rights"><h2>حقوقك</h2><p>يمكنك طلب نسخة من بياناتك أو تصحيحها أو حذفها بعد تفعيل الحسابات. أرسل طلبك إلى <a href="mailto:hello@anwarpy.ai">hello@anwarpy.ai</a>.</p></section>
<section data-od-id="privacy-updates"><h2>التحديثات</h2><p>يجب إضافة تاريخ السريان وملخص أي تغيير جوهري عند اعتماد النسخة القانونية النهائية.</p></section>
</div>
</main>
<Footer />
</div>
)
}
+7
View File
@@ -0,0 +1,7 @@
import type { Metadata } from 'next'
export const metadata: Metadata = { title: 'إنشاء حساب' }
export default function SignupLayout({ children }: { children: React.ReactNode }) {
return children
}
+101
View File
@@ -0,0 +1,101 @@
'use client'
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { safeNext } from '@/lib/next'
import '../styles/signup.css'
/* Signup — markup from signup.html, wired to real Payload registration
(create + auto-login) instead of the simulated client-only flow. */
export default function SignupPage() {
const router = useRouter()
const [showPassword, setShowPassword] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const [status, setStatus] = useState('')
const [busy, setBusy] = useState(false)
const [nextPath, setNextPath] = useState<string | null>(null)
useEffect(() => {
setNextPath(safeNext(new URLSearchParams(window.location.search).get('next')))
}, [])
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const form = e.currentTarget
const data = new FormData(form)
const confirm = form.elements.namedItem('confirmPassword') as HTMLInputElement
confirm.setCustomValidity(data.get('password') === data.get('confirmPassword') ? '' : 'كلمتا المرور غير متطابقتان.')
if (!form.checkValidity()) {
setStatus(data.get('password') !== data.get('confirmPassword') ? 'كلمتا المرور غير متطابقتان.' : 'تحقق من جميع الحقول المطلوبة.')
form.reportValidity()
return
}
setBusy(true)
setStatus('')
try {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: data.get('email'), password: data.get('password'), fullName: data.get('name'), termsVersion: '2026-10-03' }),
})
if (!res.ok) {
const err = await res.json().catch(() => null)
setStatus(err?.errors?.[0]?.message || 'تعذّر إنشاء الحساب — قد يكون البريد مستخدماً بالفعل.')
setBusy(false)
return
}
const login = await fetch('/api/users/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: data.get('email'), password: data.get('password') }),
})
if (login.ok) {
try { sessionStorage.setItem('anwarpi:signup-email', String(data.get('email'))) } catch { /* private mode */ }
try { nextPath ? sessionStorage.setItem('anwarpi:signup-next', nextPath) : sessionStorage.removeItem('anwarpi:signup-next') } catch { /* private mode */ }
router.refresh()
router.push('/verify-email')
return
}
setStatus('تم إنشاء الحساب — سجّل الدخول للمتابعة.')
router.push(nextPath ? '/login?next=' + encodeURIComponent(nextPath) : '/login')
} catch {
setStatus('تعذّر الاتصال. حاول مرة أخرى بعد التحقق من اتصالك.')
} finally {
setBusy(false)
}
}
return (
<div id="site" className="site-page page-signup">
<Topnav />
<main id="content" className="auth">
<section className="auth-story">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="auth-mark" src="/brand/mark-light.svg" alt="" width={436} height={382} />
<h1>ابدأ بحساب مجاني.</h1>
<p>الحساب المجاني يفتح كل الأدلة المجانية كاملة والتعليق عليها. يمكنك الاشتراك في العضوية لاحقاً إن احتجتها.</p>
</section>
<section className="auth-form-wrap" data-od-id="signup-form-section">
<form className="auth-form" id="signupForm" method="post" action="/api/users" noValidate onSubmit={onSubmit} data-od-id="signup-form">
<p className="eyebrow">حساب جديد</p>
<h2 data-od-id="signup-title">إنشاء حساب</h2>
<p className="lead">أدخل بياناتك الأساسية للبدء.</p>
<div className="field"><label htmlFor="name">الاسم</label><input className="input" id="name" name="name" type="text" autoComplete="name" minLength={2} required onInvalid={e => e.currentTarget.setCustomValidity('أدخل اسمك.')} onInput={e => e.currentTarget.setCustomValidity('')} /></div>
<div className="field"><label htmlFor="email">البريد الإلكتروني</label><input className="input" id="email" name="email" type="email" inputMode="email" autoComplete="email" placeholder="name@example.com" dir="ltr" required onInvalid={e => e.currentTarget.setCustomValidity('أدخل بريداً إلكترونياً صحيحاً.')} onInput={e => e.currentTarget.setCustomValidity('')} /></div>
<div className="field"><label htmlFor="password">كلمة المرور</label><div className="input-wrap"><input className="input" id="password" name="password" type={showPassword ? 'text' : 'password'} autoComplete="new-password" minLength={8} aria-describedby="passwordHint" required onInvalid={e => e.currentTarget.setCustomValidity('أدخل كلمة مرور من ثمانية أحرف على الأقل.')} onInput={e => e.currentTarget.setCustomValidity('')} /><button className="password-toggle" type="button" aria-controls="password" onClick={() => setShowPassword(v => !v)}>{showPassword ? 'إخفاء' : 'إظهار'}</button></div><span className="fine" id="passwordHint">ثمانية أحرف على الأقل.</span></div>
<div className="field"><label htmlFor="confirmPassword">تأكيد كلمة المرور</label><div className="input-wrap"><input className="input" id="confirmPassword" name="confirmPassword" type={showConfirm ? 'text' : 'password'} autoComplete="new-password" minLength={8} required onInvalid={e => e.currentTarget.setCustomValidity('أعد إدخال كلمة المرور.')} onInput={e => e.currentTarget.setCustomValidity('')} /><button className="password-toggle" type="button" aria-controls="confirmPassword" onClick={() => setShowConfirm(v => !v)}>{showConfirm ? 'إخفاء' : 'إظهار'}</button></div></div>
<label className="check"><input id="terms" name="terms" type="checkbox" required /><span>أوافق على <Link href="/privacy">سياسة الخصوصية</Link> و<Link href="/terms">شروط الاستخدام</Link>.</span></label>
<button className="btn btn-primary" type="submit" disabled={busy} data-od-id="create-account-cta">{busy ? 'جارٍ إنشاء الحساب…' : 'أنشئ حسابي'}</button>
<div className="status" id="signupStatus" role="status" aria-live="polite">{status}</div>
<p className="signin">لديك حساب بالفعل؟ <Link href={nextPath ? `/login?next=${encodeURIComponent(nextPath)}` : '/login'}>سجّل الدخول</Link></p>
</form>
</section>
</main>
<Footer />
</div>
)
}
+89
View File
@@ -0,0 +1,89 @@
/* Page-scoped styles: .page-about. Shared tokens live in globals.css. */
.page-about *,.page-about *::before,.page-about *::after{box-sizing:border-box}
.page-about{-webkit-text-size-adjust:100%}
.page-about{margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body);text-rendering:optimizeLegibility}
.page-about img,.page-about svg{display:block;max-width:100%;height:auto}
.page-about a{color:inherit;text-decoration:none}
.page-about h1,.page-about h2,.page-about h3{font-family:var(--font-display);text-wrap:balance}
.page-about p{text-wrap:pretty}
.page-about :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-about .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-about .topnav{position:sticky;top:0;z-index:20;background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px);border-bottom:1px solid var(--border)}
.page-about .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.page-about .logo{display:inline-flex;align-items:center;gap:10px;font-size:23px;font-weight:700}
.page-about .logo-mark{width:30px;height:34px;object-fit:contain;flex:none;border-radius:4px}
.page-about .nav-links{display:flex;align-items:center;gap:28px}
.page-about .nav-links a{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}
.page-about .nav-links a:hover,.page-about .nav-links a[aria-current="page"]{color:var(--fg)}
.page-about .hero{padding-block:clamp(70px,10vw,132px);border-bottom:1px solid var(--border)}
.page-about .hero-grid{display:grid;grid-template-columns:1.25fr .75fr;gap:clamp(48px,9vw,112px);align-items:flex-end}
.page-about .eyebrow{font:12px/1.5 var(--font-mono);color:var(--accent);margin:0 0 22px}
.page-about .hero h1{font-size:clamp(48px,7.5vw,92px);line-height:1.06;max-width:10ch;margin:0}
.page-about .lead{font-size:clamp(19px,2vw,23px);line-height:1.75;color:var(--muted);max-width:54ch;margin:28px 0 0}
.page-about .hero-note{border-top:1px solid var(--fg);padding-top:24px}
.page-about .hero-note strong{display:block;font-size:clamp(24px,3vw,34px);line-height:1.35}
.page-about .hero-note p{color:var(--muted);margin:10px 0 0}
.page-about .team{padding-block:clamp(64px,9vw,104px);border-bottom:1px solid var(--border)}
.page-about .section-head{display:flex;align-items:flex-end;justify-content:space-between;gap:40px;margin-bottom:46px}
.page-about .section-head h2{font-size:clamp(38px,5vw,62px);line-height:1.12;max-width:12ch;margin:0}
.page-about .section-head p{color:var(--muted);max-width:48ch;margin:0}
.page-about .profile-grid{display:grid;grid-template-columns:1.15fr .85fr;gap:42px;border-top:1px solid var(--fg)}
.page-about .profile{padding:34px 0 0;display:flex;flex-direction:column;min-width:0}
.page-about .profile+.profile{padding-inline-start:42px;border-inline-start:1px solid var(--border)}
.page-about .profile-label{font:12px/1.5 var(--font-mono);color:var(--muted)}
.page-about .profile-main .profile-label{color:var(--accent)}
.page-about .profile h3{font-size:clamp(32px,4vw,48px);line-height:1.15;margin:20px 0 8px}
.page-about .role{color:var(--fg);font-weight:700;margin:0 0 22px}
.page-about .bio{color:var(--muted);font-size:17px;margin:0;max-width:58ch}
.page-about .topics{display:flex;flex-wrap:wrap;gap:8px;margin:28px 0 32px}
.page-about .topic{display:inline-flex;align-items:center;min-height:34px;padding:6px 12px;border:1px solid var(--border);color:var(--muted);font-size:13px}
.page-about .profile-link{min-height:50px;display:inline-flex;align-items:center;justify-content:space-between;gap:20px;margin-top:auto;padding:12px 16px;border:1px solid var(--fg);font-weight:700}
.page-about .profile-link:hover{background:var(--fg);color:var(--surface)}
.page-about .principles{padding-block:clamp(64px,9vw,104px)}
.page-about .principle-grid{display:grid;grid-template-columns:.8fr 1.2fr;gap:clamp(48px,9vw,112px);align-items:flex-start}
.page-about .principle-grid h2{font-size:clamp(38px,5vw,60px);line-height:1.14;margin:0;max-width:11ch}
.page-about .principle-list{border-top:1px solid var(--fg)}
.page-about .principle{display:grid;grid-template-columns:56px 1fr;gap:20px;padding:26px 0;border-bottom:1px solid var(--border)}
.page-about .principle-index{font:12px/1.5 var(--font-mono);color:var(--muted);padding-top:7px}
.page-about .principle h3{font-size:24px;margin:0 0 8px}
.page-about .principle p{color:var(--muted);margin:0}
.page-about .pagefoot{padding-block:50px;border-top:1px solid var(--border);color:var(--muted)}
.page-about .footer-bottom{display:flex;justify-content:space-between;align-items:center;gap:20px;flex-wrap:wrap}
.page-about .footer-links{display:flex;gap:20px;flex-wrap:wrap}
.page-about .footer-links a{min-height:44px;display:inline-flex;align-items:center}
.page-about .footer-links a:hover{color:var(--fg);text-decoration:underline;text-underline-offset:4px}
@media(max-width:920px){
.page-about .hero-grid,.page-about .profile-grid,.page-about .principle-grid{grid-template-columns:1fr}
.page-about .profile-grid{gap:0}
.page-about .profile+.profile{padding-inline-start:0;border-inline-start:0;border-top:1px solid var(--border);margin-top:42px;padding-top:42px}
.page-about .section-head{align-items:flex-start;flex-direction:column}
.page-about .hero-note{max-width:560px}
}
@media(max-width:620px){
.page-about .hero{padding-block:54px}
.page-about .team,.page-about .principles{padding-block:58px}
.page-about .profile h3{font-size:36px}
.page-about .principle{grid-template-columns:42px 1fr}
.page-about .footer-bottom,.page-about .footer-links{align-items:stretch;flex-direction:column;gap:8px}
}
@media(prefers-reduced-motion:reduce){.page-about *{scroll-behavior:auto!important;transition-duration:.01ms!important}}
/* Matched team cards, independent of generic card and heading overrides. */
#site.page-about .section-head { display: grid; grid-template-columns: 1fr 1fr; align-items: start; gap: 40px; margin-bottom: 32px; }
#site.page-about .section-head h2 { max-width: none; margin: 0; font-size: clamp(28px, 3vw, 40px); }
#site.page-about .section-head p { max-width: none; margin: 0; line-height: 1.9; }
#site.page-about .profile-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; border: 0; align-items: stretch; }
#site.page-about .profile, #site.page-about .profile + .profile { margin: 0; padding: 32px; border: 1px solid var(--border); background: var(--surface); border-radius: 16px; }
#site.page-about .profile-label { font: 13px/1.8 var(--font-body); color: var(--accent); min-height: 24px; }
#site.page-about .profile h3 { font-size: 32px; line-height: 1.6; margin: 16px 0 4px; }
#site.page-about .profile .role { font-size: 16px; line-height: 1.8; margin: 0 0 20px; }
#site.page-about .profile .bio { font-size: 16px; line-height: 2; max-width: none; }
#site.page-about .topics { margin: 24px 0; gap: 8px; }
#site.page-about .topic { border-radius: 24px; background: var(--accent-soft); border-color: transparent; color: var(--muted); }
#site.page-about .profile-link { margin-top: auto; border: 1px solid var(--border); border-radius: 8px; color: var(--accent); font-size: 14px; }
#site.page-about .profile-link:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
@media (max-width: 760px) {
#site.page-about .section-head, #site.page-about .profile-grid { grid-template-columns: minmax(0, 1fr); gap: 24px; }
#site.page-about .profile, #site.page-about .profile + .profile { padding: 24px; }
#site.page-about .profile h3 { font-size: 28px; }
}
@@ -0,0 +1,108 @@
/* Page-scoped styles: .page-account-settings. Shared tokens live in globals.css. */
.page-account-settings *,.page-account-settings *::before,.page-account-settings *::after{box-sizing:border-box} .page-account-settings{scroll-behavior:smooth;-webkit-text-size-adjust:100%} .page-account-settings{margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body);text-rendering:optimizeLegibility} .page-account-settings.locked{overflow:hidden}
.page-account-settings a{color:inherit;text-decoration:none} .page-account-settings button,.page-account-settings input{font:inherit} .page-account-settings button{cursor:pointer} .page-account-settings h1,.page-account-settings h2,.page-account-settings h3{font-family:var(--font-display);text-wrap:balance} .page-account-settings p{text-wrap:pretty} .page-account-settings :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-account-settings .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-account-settings .topnav{position:sticky;top:0;z-index:20;background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px);border-bottom:1px solid var(--border)} .page-account-settings .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.page-account-settings .logo{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-display);font-size:23px;font-weight:700}.page-account-settings .logo-mark{width:12px;height:12px;background:var(--fg);transform:rotate(45deg)} .page-account-settings .nav-links{display:flex;align-items:center;gap:28px}.page-account-settings .nav-links a{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}.page-account-settings .nav-links a:hover{color:var(--fg)}
.page-account-settings .nav-tools{display:flex;align-items:center;gap:8px}
.page-account-settings .nav-account{min-height:44px;display:inline-flex;align-items:center;gap:10px;padding-inline:16px;border:1px solid var(--fg);background:var(--fg);color:var(--surface);font-weight:700}.page-account-settings .nav-account:hover{background:var(--accent);border-color:var(--accent)}
.page-account-settings .nav-account .avatar{width:22px;height:22px;background:var(--surface);color:var(--fg);display:grid;place-items:center;font:12px/1 var(--font-mono);font-weight:700}
.page-account-settings .icon-btn{min-height:44px;width:46px;border:1px solid var(--border);background:transparent;color:var(--fg);display:inline-grid;place-items:center}.page-account-settings .icon-btn:hover{border-color:var(--fg);background:var(--fg-soft)}
.page-account-settings .demo-strip{background:var(--fg);color:var(--surface);font:11px/1.6 var(--font-mono);padding:8px 0;text-align:center}
.page-account-settings .demo-strip b{color:var(--surface);font-weight:700}
.page-account-settings .page-head{padding-block:clamp(48px,7vw,84px) 0}
.page-account-settings .crumb{font:12px/1.5 var(--font-mono);color:var(--accent);margin:0 0 18px}
.page-account-settings .page-head h1{font-size:clamp(42px,6vw,72px);line-height:1.1;margin:0}
.page-account-settings .page-head .sub{color:var(--muted);margin:16px 0 0;max-width:52ch}
.page-account-settings .settings-layout{display:grid;grid-template-columns:230px minmax(0,1fr);gap:clamp(40px,7vw,90px);align-items:start;padding-block:56px 90px}
.page-account-settings .settings-nav{position:sticky;top:100px;border-top:1px solid var(--fg)}
.page-account-settings .settings-nav button{width:100%;min-height:52px;display:flex;justify-content:space-between;align-items:center;gap:12px;background:transparent;border:0;border-bottom:1px solid var(--border);color:var(--muted);font-size:16px;text-align:right;padding:0 4px}
.page-account-settings .settings-nav button:hover{color:var(--fg)}
.page-account-settings .settings-nav button[aria-selected="true"]{color:var(--fg);font-weight:700}
.page-account-settings .settings-nav button[aria-selected="true"]::after{content:"";width:8px;height:8px;background:var(--accent);transform:rotate(45deg)}
.page-account-settings .panel{display:none}
.page-account-settings .panel.active{display:block}
.page-account-settings .panel h2{font-size:clamp(30px,3.4vw,42px);line-height:1.2;margin:0 0 10px}
.page-account-settings .panel .panel-sub{color:var(--muted);max-width:56ch;margin:0 0 34px}
.page-account-settings .form-grid{display:grid;gap:24px;max-width:560px}
.page-account-settings .field{display:grid;gap:7px}
.page-account-settings .field label{font-size:13px;color:var(--muted)}
.page-account-settings .field .hint{font-size:12px;color:var(--muted)}
.page-account-settings .input{width:100%;border:1px solid var(--border);background:var(--surface);color:var(--fg);padding:13px 14px;min-height:48px}
.page-account-settings .input:hover{border-color:var(--muted)}
.page-account-settings .input:focus{outline:2px solid var(--accent);outline-offset:1px}
.page-account-settings .input[disabled]{background:var(--fg-soft);color:var(--muted);cursor:not-allowed}
.page-account-settings .input.invalid{border-color:var(--accent)}
.page-account-settings .btn{min-height:46px;display:inline-flex;align-items:center;justify-content:center;padding:11px 22px;border:1px solid transparent;font-weight:700}
.page-account-settings .btn-primary{background:var(--accent);color:var(--surface);border-color:var(--accent)}.page-account-settings .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black)}
.page-account-settings .btn-secondary{border-color:var(--border);background:transparent;color:var(--fg)}.page-account-settings .btn-secondary:hover{border-color:var(--fg);background:var(--fg-soft)}
.page-account-settings .status{font-size:14px;min-height:24px;display:flex;align-items:center;gap:8px}
.page-account-settings .status.ok{color:var(--accent);font-weight:700}
.page-account-settings .status.err{color:var(--accent)}
.page-account-settings .pw-rules{list-style:none;padding:0;margin:4px 0 0;display:grid;gap:4px}
.page-account-settings .pw-rules li{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:8px}
.page-account-settings .pw-rules li::before{content:"";width:7px;height:7px;border:1px solid var(--muted);transform:rotate(45deg);flex:none}
.page-account-settings .pw-rules li.ok{color:var(--fg)}
.page-account-settings .pw-rules li.ok::before{background:var(--accent);border-color:var(--accent)}
.page-account-settings .prefs{border-top:1px solid var(--fg);max-width:640px}
.page-account-settings .pref-row{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:22px 0;border-bottom:1px solid var(--border)}
.page-account-settings .pref-row strong{display:block;font-size:17px}
.page-account-settings .pref-row p{margin:4px 0 0;color:var(--muted);font-size:14px;max-width:44ch}
.page-account-settings .switch{position:relative;width:52px;min-height:32px;flex:none}
.page-account-settings .switch input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}
.page-account-settings .switch .track{position:absolute;inset:0;border:1px solid var(--muted);background:var(--surface);pointer-events:none;transition:background .15s,border-color .15s}
.page-account-settings .switch input:checked + .track{background:var(--accent);border-color:var(--accent)}
.page-account-settings .switch .track::after{content:"";position:absolute;top:4px;inset-inline-start:4px;width:22px;height:22px;background:var(--fg);transition:transform .15s}
.page-account-settings .switch input:checked + .track::after{transform:translateX(-20px);background:var(--surface)}
.page-account-settings .switch input:focus-visible + .track{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-account-settings .membership-card{border:1px solid var(--fg);padding:clamp(24px,4vw,38px);max-width:640px}
.page-account-settings .membership-head{display:flex;justify-content:space-between;align-items:start;gap:18px;padding-bottom:22px;border-bottom:1px solid var(--border)}
.page-account-settings .membership-head h3{font-size:28px;margin:0}
.page-account-settings .state-chip{font:11px/1.5 var(--font-mono);padding:6px 12px;border:1px solid var(--accent);color:var(--accent);white-space:nowrap}
.page-account-settings .state-chip.cancelled{border-color:var(--muted);color:var(--muted)}
.page-account-settings .plan-details{display:grid;grid-template-columns:repeat(2,1fr);gap:20px 32px;padding-block:24px}
.page-account-settings .plan-details dt{font:12px/1.6 var(--font-mono);color:var(--muted)}
.page-account-settings .plan-details dd{margin:4px 0 0;font-size:16px}
.page-account-settings .price-tag{font-family:var(--font-display);font-size:44px;line-height:1}
.page-account-settings .price-tag small{display:block;font:11px/1.5 var(--font-mono);color:var(--muted);margin-top:6px}
.page-account-settings .membership-actions{display:flex;gap:14px;flex-wrap:wrap;padding-top:22px;border-top:1px solid var(--border)}
.page-account-settings .cancel-zone{margin-top:34px;max-width:640px;border-top:1px solid var(--border);padding-top:24px}
.page-account-settings .cancel-zone h3{font-size:22px;margin:0 0 6px}
.page-account-settings .cancel-zone p{color:var(--muted);font-size:14px;margin:0 0 16px;max-width:52ch}
.page-account-settings .billing{margin-top:34px;max-width:640px;border-top:1px solid var(--border);padding-top:24px}
.page-account-settings .billing h3{font-size:22px;margin:0 0 6px}
.page-account-settings .billing-sub{color:var(--muted);font-size:14px;margin:0 0 18px;max-width:52ch}
.page-account-settings .billing-table{border:1px solid var(--border)}
.page-account-settings .billing-row{display:grid;grid-template-columns:1.1fr 1.5fr .7fr .8fr auto;gap:12px;align-items:center;padding:13px 18px;border-bottom:1px solid var(--border);font-size:14px}
.page-account-settings .billing-row:last-child{border-bottom:0}
.page-account-settings .billing-row.head{font:11px/1.6 var(--font-mono);color:var(--muted);padding:10px 18px}
.page-account-settings .billing-row .amt,.page-account-settings .billing-row .bdate{font-family:var(--font-mono);white-space:nowrap}
.page-account-settings .billing-row .desc{color:var(--fg)}
.page-account-settings .chip{display:inline-block;padding:2px 10px;border:1px solid var(--border);font:11px/1.9 var(--font-mono);color:var(--muted);white-space:nowrap}
.page-account-settings .chip.paid{color:var(--fg);border-color:var(--fg)}
.page-account-settings .chip.upcoming{color:var(--accent);border-color:var(--accent)}
.page-account-settings .chip.voided{color:var(--muted);border-color:var(--border);text-decoration:line-through}
.page-account-settings .inv-link{font:12px/1.6 var(--font-mono);color:var(--fg);text-decoration:underline;text-underline-offset:3px}
.page-account-settings .inv-link:hover{color:var(--accent)}
.page-account-settings .inv-link:disabled{color:var(--muted);text-decoration:none;cursor:default}
.page-account-settings .inv-link:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.page-account-settings .text-danger{background:transparent;border:0;color:var(--accent);font-weight:700;text-decoration:underline;text-underline-offset:4px;min-height:44px;padding:0 4px}
.page-account-settings .text-danger:hover{color:color-mix(in oklch,var(--accent) 80%,black)}
.page-account-settings .dialog{position:fixed;inset:0;z-index:60;visibility:hidden;pointer-events:none}.page-account-settings .dialog.open{visibility:visible;pointer-events:auto}
.page-account-settings .dialog-scrim{position:absolute;inset:0;border:0;background:color-mix(in oklch,var(--fg) 58%,transparent);opacity:0;transition:opacity .2s}
.page-account-settings .dialog.open .dialog-scrim{opacity:1}
.page-account-settings .dialog-panel{position:absolute;inset-inline:0;top:50%;transform:translateY(calc(-50% + 12px));margin-inline:auto;width:min(560px,92vw);background:var(--bg);border:1px solid var(--fg);padding:clamp(26px,4vw,40px);opacity:0;transition:opacity .2s,transform .2s}
.page-account-settings .dialog.open .dialog-panel{opacity:1;transform:translateY(-50%)}
.page-account-settings .dialog-panel h2{font-size:30px;margin:0 0 12px}
.page-account-settings .dialog-panel p{color:var(--muted);margin:0 0 14px}
.page-account-settings .dialog-panel .keep-note{border-inline-start:2px solid var(--border);padding-inline-start:14px}
.page-account-settings .dialog-actions{display:flex;gap:14px;flex-wrap:wrap;margin-top:26px}
.page-account-settings .pagefoot{padding-block:54px;border-top:1px solid var(--border);color:var(--muted)}
.page-account-settings .footer-grid{display:grid;grid-template-columns:1.4fr repeat(3,1fr);gap:44px}
.page-account-settings .footer-col strong{display:block;color:var(--fg);margin-bottom:12px}
.page-account-settings .footer-col a{display:block;min-height:36px}.page-account-settings .footer-col a:hover{color:var(--fg);text-decoration:underline}
.page-account-settings .footer-bottom{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;padding-top:28px;margin-top:38px;border-top:1px solid var(--border);font-size:13px}
.page-account-settings .menu-panel{position:fixed;inset:0;z-index:60;visibility:hidden;pointer-events:none}.page-account-settings .menu-panel.open{visibility:visible;pointer-events:auto}.page-account-settings .menu-scrim{position:absolute;inset:0;border:0;background:color-mix(in oklch,var(--fg) 55%,transparent);opacity:0}.page-account-settings .menu-panel.open .menu-scrim{opacity:1}.page-account-settings .menu-drawer{position:absolute;inset-block:0;inset-inline-start:0;width:min(520px,92vw);padding:28px clamp(24px,5vw,54px);background:var(--bg);border-inline-end:1px solid var(--fg);transform:translateX(105%);transition:transform .25s ease;display:flex;flex-direction:column}.page-account-settings .menu-panel.open .menu-drawer{transform:translateX(0)}.page-account-settings .menu-head{display:flex;justify-content:space-between;align-items:center;padding-bottom:24px;border-bottom:1px solid var(--border)}.page-account-settings .menu-links{display:flex;flex-direction:column}.page-account-settings .menu-links a{display:flex;justify-content:space-between;align-items:center;min-height:58px;border-bottom:1px solid var(--border);font-size:18px}
@media(max-width:920px){.page-account-settings .nav-links{display:none}.page-account-settings .settings-layout{grid-template-columns:1fr}.page-account-settings .settings-nav{position:static;display:grid;grid-template-columns:1fr 1fr;border-top:0;margin-bottom:10px}.page-account-settings .settings-nav button{border:1px solid var(--border)}.page-account-settings .footer-grid{grid-template-columns:1fr 1fr}}
@media(max-width:620px){.page-account-settings .nav-account .name-label{display:none}.page-account-settings .page-head h1{font-size:44px}.page-account-settings .settings-layout{padding-block:40px 60px}.page-account-settings .footer-grid{grid-template-columns:1fr;gap:28px}.page-account-settings .plan-details{grid-template-columns:1fr}.page-account-settings .dialog-actions{flex-direction:column}.page-account-settings .dialog-actions .btn{width:100%}.page-account-settings .billing-row{grid-template-columns:1fr 1fr;row-gap:2px}.page-account-settings .billing-row.head{display:none}.page-account-settings .billing-row .bdate{grid-column:1}.page-account-settings .billing-row .desc{grid-column:1 / -1;color:var(--muted)}}
@media(prefers-reduced-motion:reduce){.page-account-settings *{scroll-behavior:auto!important;transition-duration:.01ms!important}}
@@ -0,0 +1,189 @@
/* Page-scoped styles: .page-home. Shared tokens live in globals.css. */
.page-home *, .page-home *::before, .page-home *::after { box-sizing: border-box; }
.page-home { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
.page-home {
margin: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
font-size: var(--fs-body);
line-height: 1.65;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
.page-home.modal-open { overflow: hidden; }
.page-home img, .page-home svg { display: block; max-width: 100%; }
.page-home a { color: inherit; text-decoration: none; }
.page-home button, .page-home input { font: inherit; }
.page-home button { cursor: pointer; }
.page-home p { text-wrap: pretty; }
.page-home h1, .page-home h2, .page-home h3 { text-wrap: balance; }
.page-home :focus-visible { outline: 3px solid color-mix(in oklch, var(--accent) 55%, transparent); outline-offset: 3px; }
.page-home .container { max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
.page-home .section { padding-block: clamp(64px, 9vw, var(--gap-2xl)); }
.page-home .section + .section { border-top: 1px solid var(--border); }
.page-home .stack { display: flex; flex-direction: column; }
.page-home .stack > * + * { margin-top: var(--gap-md); }
.page-home .row { display: flex; align-items: center; gap: var(--gap-md); }
.page-home .row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--gap-md); }
.page-home .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gap-lg); }
.page-home .grid-2-1 { display: grid; grid-template-columns: 2fr 1fr; gap: var(--gap-xl); align-items: start; }
.page-home .h1, .page-home h1 { font-family: var(--font-display); font-size: var(--fs-h1); line-height: 1.08; margin: 0; }
.page-home .h2, .page-home h2 { font-family: var(--font-display); font-size: var(--fs-h2); line-height: 1.15; margin: 0; }
.page-home .h3, .page-home h3 { font-family: var(--font-display); font-size: var(--fs-h3); line-height: 1.35; margin: 0; }
.page-home .lead { font-size: var(--fs-lead); line-height: 1.7; color: var(--muted); max-width: 54ch; margin: 0; }
.page-home .eyebrow { font-family: var(--font-mono); font-size: 11px; color: var(--accent); margin: 0 0 var(--gap-md); }
.page-home .meta { font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--muted); }
.page-home .num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.page-home .topnav { position: sticky; top: 0; z-index: 20; background: color-mix(in oklch, var(--bg) 92%, transparent); backdrop-filter: blur(14px); border-bottom: 1px solid var(--border); }
.page-home .topnav-inner { min-height: 70px; display: flex; align-items: center; justify-content: space-between; gap: var(--gap-lg); }
.page-home .logo { display: inline-flex; align-items: center; gap: 10px; font-family: var(--font-display); font-size: 23px; font-weight: 700; }
.page-home .logo-mark { width: 12px; height: 12px; background: var(--fg); transform: rotate(45deg); }
.page-home .logo:hover { opacity: .72; }
.page-home .topnav nav { display: flex; align-items: center; gap: var(--gap-lg); }
.page-home .topnav nav a { min-height: 44px; display: inline-flex; align-items: center; color: var(--muted); font-size: 14px; border-bottom: 1px solid transparent; }
.page-home .topnav nav a:hover { color: var(--fg); border-color: var(--fg); }
.page-home .nav-action { min-height: 44px; display: inline-flex; align-items: center; padding-inline: 16px; border: 1px solid var(--border); background: transparent; color: var(--fg); }
.page-home .nav-action:hover { border-color: var(--fg); background: var(--fg-soft); }
.page-home .nav-tools { display: flex; align-items: center; gap: 8px; }
.page-home .menu-trigger { width: 46px; height: 46px; display: grid; place-items: center; border: 1px solid var(--fg); background: var(--fg); color: var(--surface); }
.page-home .menu-trigger:hover { background: var(--accent); border-color: var(--accent); color: var(--surface); }
.page-home .menu-trigger svg { width: 20px; height: 20px; }
.page-home .menu-panel { position: fixed; inset: 0; z-index: 60; visibility: hidden; pointer-events: none; }
.page-home .menu-panel.open { visibility: visible; pointer-events: auto; }
.page-home .menu-scrim { position: absolute; inset: 0; border: 0; background: color-mix(in oklch, var(--fg) 55%, transparent); opacity: 0; transition: opacity .2s ease; }
.page-home .menu-panel.open .menu-scrim { opacity: 1; }
.page-home .menu-drawer { position: absolute; inset-block: 0; inset-inline-start: 0; width: min(520px, 92vw); padding: 28px clamp(24px, 5vw, 54px); background: var(--bg); border-inline-end: 1px solid var(--fg); transform: translateX(105%); transition: transform .25s ease; display: flex; flex-direction: column; }
.page-home .menu-panel.open .menu-drawer { transform: translateX(0); }
.page-home .menu-head { display: flex; align-items: center; justify-content: space-between; padding-bottom: 24px; border-bottom: 1px solid var(--border); }
.page-home .menu-links { display: grid; margin-top: 34px; }
.page-home .menu-links a { min-height: 70px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border); font-family: var(--font-display); font-size: clamp(27px, 4vw, 38px); }
.page-home .menu-links a:hover { color: var(--accent); }
.page-home .menu-foot { margin-top: auto; padding-top: 24px; color: var(--muted); }
.page-home .btn { min-height: 46px; display: inline-flex; align-items: center; justify-content: center; gap: 9px; padding: 11px 20px; border-radius: var(--radius); border: 1px solid transparent; font-weight: 700; transition: transform .12s ease, background .16s ease, border-color .16s ease; }
.page-home .btn:active { transform: translateY(1px); }
.page-home .btn-primary { background: var(--accent); color: var(--surface); border-color: var(--accent); }
.page-home .btn-primary:hover { background: color-mix(in oklch, var(--accent) 86%, black); color: var(--surface); }
.page-home .btn-secondary { background: transparent; color: var(--fg); border-color: var(--border); }
.page-home .btn-secondary:hover { background: var(--fg); color: var(--surface); border-color: var(--fg); }
.page-home .btn-ghost { background: transparent; color: var(--fg); border-color: transparent; padding-inline: 8px; }
.page-home .btn-ghost:hover { border-color: var(--fg); }
.page-home .hero { padding-block: clamp(56px, 8vw, 112px); overflow: hidden; }
.page-home .hero-split { display: grid; grid-template-columns: .85fr 1.15fr; gap: clamp(42px, 7vw, 96px); align-items: center; }
.page-home .hero-copy h1 { max-width: 9ch; }
.page-home .hero-copy .lead { margin-top: 24px; }
.page-home .hero-cta { display: flex; gap: var(--gap-sm); flex-wrap: wrap; margin-top: 32px; }
.page-home .trust-line { margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--border); color: var(--muted); font-size: 13px; }
.page-home .deck { position: relative; min-height: 510px; }
.page-home .deck-frame { position: relative; min-height: 470px; border: 1px solid var(--fg); background: var(--surface); overflow: hidden; }
.page-home .deck-slide { position: absolute; inset: 0; padding: clamp(28px, 4vw, 52px); display: none; grid-template-rows: auto 1fr auto; }
.page-home .deck-slide.active { display: grid; }
.page-home .deck-slide::after { content: ""; position: absolute; inset-inline-start: 0; bottom: 0; width: 100%; height: 4px; background: var(--fg); }
.page-home .deck-slide-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.page-home .deck-slide-kicker { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.page-home .deck-slide-index { font-family: var(--font-display); font-size: 30px; }
.page-home .deck-slide-body { align-self: center; }
.page-home .deck-slide-body h2 { font-size: clamp(38px, 5vw, 66px); max-width: 9ch; }
.page-home .deck-slide-body p { color: var(--muted); max-width: 38ch; margin: 18px 0 0; }
.page-home .deck-slide-foot { display: flex; align-items: end; justify-content: space-between; gap: 20px; border-top: 1px solid var(--border); padding-top: 18px; }
.page-home .deck-slide-format { display: inline-flex; align-items: center; gap: 8px; font-weight: 700; }
.page-home .deck-slide-link { min-height: 44px; display: inline-flex; align-items: center; font-weight: 700; border-bottom: 1px solid var(--fg); }
.page-home .deck-slide-link:hover { color: var(--accent); border-color: var(--accent); }
.page-home .play { width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid var(--fg); border-radius: 50%; }
.page-home .deck-controls { min-height: 44px; display: flex; justify-content: space-between; align-items: center; margin-top: 14px; }
.page-home .deck-dots { display: flex; gap: 8px; }
.page-home .deck-dot { width: 34px; height: 4px; border: 0; padding: 0; background: var(--border); }
.page-home .deck-dot.active { background: var(--fg); }
.page-home .deck-arrows { display: flex; gap: 8px; direction: ltr; }
.page-home .icon-btn { width: 44px; height: 44px; display: grid; place-items: center; border: 1px solid var(--border); background: transparent; color: var(--fg); }
.page-home .icon-btn:hover { border-color: var(--fg); background: var(--fg-soft); }
.page-home .topic-grid { display: grid; grid-template-columns: repeat(4, 1fr); border-block: 1px solid var(--border); }
.page-home .topic { min-height: 132px; padding: 24px; border-inline-start: 1px solid var(--border); display: flex; flex-direction: column; justify-content: space-between; }
.page-home .topic:first-child { border-inline-start: 0; }
.page-home .topic strong { font-family: var(--font-display); font-size: 22px; }
.page-home .topic span { color: var(--muted); font-size: 13px; }
.page-home .topic:hover { background: var(--fg-soft); }
.page-home .section-head { margin-bottom: 42px; align-items: end; }
.page-home .section-head > div { max-width: 54ch; }
.page-home .filterbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 34px; }
.page-home .filter { min-height: 44px; padding: 9px 16px; border: 1px solid var(--border); background: transparent; color: var(--muted); }
.page-home .filter:hover { color: var(--fg); border-color: var(--fg); }
.page-home .filter.active { background: var(--fg); color: var(--surface); border-color: var(--fg); }
.page-home .catalog { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 28px; border-top: 1px solid var(--fg); }
.page-home .catalog-card { padding-block: 28px; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; min-height: 330px; }
.page-home .catalog-card[hidden] { display: none; }
.page-home .cover { min-height: 142px; padding: 20px; margin-bottom: 22px; background: var(--fg); color: var(--surface); display: flex; flex-direction: column; justify-content: space-between; position: relative; overflow: hidden; }
.page-home .cover::before { content: ""; position: absolute; width: 120px; height: 120px; border: 1px solid color-mix(in oklch, var(--surface) 28%, transparent); border-radius: 50%; inset-inline-end: -28px; top: -35px; }
.page-home .cover-light { background: var(--border); color: var(--fg); }
.page-home .cover-lined { background: repeating-linear-gradient(0deg, var(--surface), var(--surface) 23px, var(--border) 24px); color: var(--fg); border: 1px solid var(--border); }
.page-home .cover-label { position: relative; font-family: var(--font-mono); font-size: 10px; }
.page-home .cover-title { position: relative; font-family: var(--font-display); font-size: 27px; line-height: 1.2; max-width: 11ch; }
.page-home .card-meta { display: flex; align-items: center; justify-content: space-between; gap: 14px; color: var(--muted); font-size: 12px; margin-bottom: 12px; }
.page-home .catalog-card h3 { font-size: 24px; }
.page-home .catalog-card p { color: var(--muted); margin: 9px 0 20px; font-size: 14px; }
.page-home .card-link { margin-top: auto; min-height: 44px; display: inline-flex; align-items: center; justify-content: space-between; border: 0; border-top: 1px solid var(--border); background: transparent; color: var(--fg); padding: 14px 0 0; font-weight: 700; text-align: start; }
.page-home .card-link:hover { border-color: var(--fg); }
.page-home .card-link span:last-child { direction: ltr; }
.page-home .empty { display: none; padding: 40px 0; color: var(--muted); }
.page-home .empty.show { display: block; }
.page-home .membership { background: var(--fg); color: var(--surface); }
.page-home .membership .eyebrow { color: var(--surface); opacity: .7; }
.page-home .membership .lead { color: color-mix(in oklch, var(--surface) 70%, transparent); }
.page-home .membership-grid { display: grid; grid-template-columns: 1.35fr .65fr; gap: var(--gap-xl); align-items: end; }
.page-home .membership-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid color-mix(in oklch, var(--surface) 25%, transparent); }
.page-home .membership-list li { display: flex; justify-content: space-between; gap: 16px; padding: 14px 0; border-bottom: 1px solid color-mix(in oklch, var(--surface) 15%, transparent); }
.page-home .membership .btn-primary { background: var(--surface); color: var(--fg); border-color: var(--surface); margin-top: 24px; width: 100%; }
.page-home .membership .btn-primary:hover { background: var(--border); color: var(--fg); }
.page-home .pagefoot { padding-block: var(--gap-xl); color: var(--muted); font-size: 13px; border-top: 1px solid var(--border); }
.page-home .footer-grid { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: var(--gap-xl); padding-bottom: var(--gap-xl); }
.page-home .footer-brand p { max-width: 34ch; margin: 14px 0 0; }
.page-home .footer-col strong { display: block; color: var(--fg); margin-bottom: 12px; }
.page-home .footer-col a { display: block; min-height: 36px; }
.page-home .footer-col a:hover { color: var(--fg); text-decoration: underline; text-underline-offset: 4px; }
.page-home .footer-bottom { padding-top: 22px; border-top: 1px solid var(--border); flex-wrap: wrap; }
.page-home .modal { position: fixed; inset: 0; z-index: 50; display: none; place-items: center; padding: 24px; background: color-mix(in oklch, var(--fg) 62%, transparent); }
.page-home .modal.open { display: grid; }
.page-home .modal-panel { width: min(620px, 100%); max-height: min(680px, 90vh); overflow: auto; background: var(--surface); color: var(--fg); border: 1px solid var(--fg); padding: clamp(26px, 5vw, 52px); }
.page-home .modal-close { float: left; margin: -14px -14px 20px 0; }
.page-home .modal-panel h2 { clear: both; font-size: clamp(32px, 5vw, 50px); }
.page-home .modal-panel p { color: var(--muted); }
.page-home .modal-outline { margin: 28px 0; padding: 0; list-style: none; border-top: 1px solid var(--border); }
.page-home .modal-outline li { padding: 13px 0; border-bottom: 1px solid var(--border); }
@media (max-width: 920px) {
.page-home .hero-split, .page-home .grid-3, .page-home .grid-2-1, .page-home .membership-grid { grid-template-columns: 1fr; }
.page-home .topnav nav { display: none; }
.page-home .footer-grid { grid-template-columns: 1fr 1fr; }
.page-home .hero-copy h1 { max-width: 11ch; }
.page-home .deck { min-height: 440px; }
.page-home .deck-frame { min-height: 400px; }
.page-home .topic-grid { grid-template-columns: repeat(2, 1fr); }
.page-home .topic:nth-child(3) { border-inline-start: 0; border-top: 1px solid var(--border); }
.page-home .topic:nth-child(4) { border-top: 1px solid var(--border); }
.page-home .catalog { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 620px) {
.page-home .nav-action { padding-inline: 12px; }
.page-home .hero { padding-top: 42px; }
.page-home .deck-slide { padding: 26px; }
.page-home .deck-slide-body h2 { font-size: 38px; }
.page-home .deck-slide-foot { align-items: start; flex-direction: column; }
.page-home .topic-grid, .page-home .catalog { grid-template-columns: 1fr; }
.page-home .topic { min-height: 104px; border-inline-start: 0; border-top: 1px solid var(--border); }
.page-home .topic:first-child { border-top: 0; }
.page-home .section-head { align-items: start; flex-direction: column; }
.page-home .catalog-card { min-height: auto; }
.page-home .nav-action { display: none; }
.page-home .footer-grid { grid-template-columns: 1fr; gap: 28px; }
}
@media (prefers-reduced-motion: reduce) {
.page-home *, .page-home *::before, .page-home *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; }
}
@@ -0,0 +1,16 @@
/* Page-scoped styles: .page-posts. Shared tokens live in globals.css. */
.page-posts *,.page-posts *::before,.page-posts *::after{box-sizing:border-box} .page-posts{scroll-behavior:smooth;-webkit-text-size-adjust:100%} .page-posts{margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body);text-rendering:optimizeLegibility} .page-posts.locked{overflow:hidden}
.page-posts a{color:inherit;text-decoration:none} .page-posts button,.page-posts input,.page-posts textarea{font:inherit} .page-posts button{cursor:pointer} .page-posts h1,.page-posts h2,.page-posts h3{font-family:var(--font-display);text-wrap:balance} .page-posts p{text-wrap:pretty} .page-posts :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-posts .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)} .page-posts .topnav{position:sticky;top:0;z-index:20;background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px);border-bottom:1px solid var(--border)} .page-posts .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.page-posts .logo{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-display);font-size:23px;font-weight:700}.page-posts .logo-mark{width:12px;height:12px;background:var(--fg);transform:rotate(45deg)} .page-posts .nav-links{display:flex;align-items:center;gap:28px}.page-posts .nav-links a{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}.page-posts .nav-links a:hover{color:var(--fg)}
.page-posts .nav-tools{display:flex;align-items:center;gap:8px}.page-posts .nav-login,.page-posts .icon-btn{min-height:44px;border:1px solid var(--border);background:transparent;color:var(--fg);display:inline-grid;place-items:center}.page-posts .nav-login{padding-inline:16px}.page-posts .nav-login:hover,.page-posts .icon-btn:hover{border-color:var(--fg);background:var(--fg-soft)}.page-posts .icon-btn{width:46px}.page-posts .menu-trigger{background:var(--fg);color:var(--surface);border-color:var(--fg)}.page-posts .menu-trigger:hover{background:var(--accent);color:var(--surface);border-color:var(--accent)}
.page-posts .article-head{padding-block:clamp(58px,9vw,110px);border-bottom:1px solid var(--border)}.page-posts .crumb{font:12px/1.5 var(--font-mono);color:var(--accent);margin:0 0 20px}.page-posts .access-chip{display:inline-flex;align-items:center;min-height:34px;padding:6px 14px;border:1px solid var(--accent);color:var(--accent);font:12px/1.5 var(--font-mono);margin-bottom:22px}.page-posts .article-head h1{font-size:clamp(48px,7.4vw,94px);line-height:1.06;max-width:12ch;margin:0}.page-posts .dek{font-size:clamp(19px,2vw,24px);line-height:1.7;color:var(--muted);max-width:58ch;margin:28px 0 0}.page-posts .article-meta{display:flex;align-items:center;flex-wrap:wrap;gap:14px 24px;margin-top:34px;padding-top:20px;border-top:1px solid var(--border);font:12px/1.5 var(--font-mono);color:var(--muted)}
.page-posts .article-layout{display:grid;grid-template-columns:minmax(0,740px) 240px;gap:clamp(48px,8vw,100px);align-items:start;padding-block:80px}.page-posts .article-body{font-size:19px;line-height:2}.page-posts .article-body h2{font-size:clamp(32px,4vw,48px);line-height:1.2;margin:72px 0 20px}.page-posts .article-body h3{font-size:27px;margin:42px 0 14px}.page-posts .article-body p{margin:0 0 26px}.page-posts .article-body ul{padding-inline-start:24px}.page-posts .article-body li{margin-block:10px}.page-posts .lede{font-family:var(--font-display);font-size:28px;line-height:1.6}.page-posts .code{direction:ltr;text-align:left;background:var(--fg);color:var(--surface);padding:26px;overflow:auto;font:14px/1.8 var(--font-mono);margin:34px 0}.page-posts .note{border-block:1px solid var(--fg);padding:28px 0;margin:42px 0}.page-posts .note strong{display:block;font-family:var(--font-display);font-size:24px;margin-bottom:8px}.page-posts .steps{counter-reset:step;list-style:none;padding:0}.page-posts .steps li{counter-increment:step;display:grid;grid-template-columns:52px 1fr;gap:18px;padding:22px 0;border-top:1px solid var(--border)}.page-posts .steps li::before{content:counter(step,decimal-leading-zero);font:14px/1.7 var(--font-mono);color:var(--accent)}
.page-posts .toc{position:sticky;top:100px;border-top:1px solid var(--fg);padding-top:18px}.page-posts .toc strong{font-family:var(--font-display);font-size:20px}.page-posts .toc a{display:block;padding:11px 0;border-bottom:1px solid var(--border);color:var(--muted)}.page-posts .toc a:hover{color:var(--fg)}.page-posts .member-note{margin-top:28px;padding:22px;background:var(--fg);color:var(--surface)}.page-posts .member-note p{color:color-mix(in oklch,var(--surface) 72%,transparent);font-size:14px}.page-posts .btn{min-height:46px;display:inline-flex;align-items:center;justify-content:center;padding:11px 20px;border:1px solid transparent;font-weight:700}.page-posts .btn-primary{background:var(--accent);color:var(--surface);border-color:var(--accent)}.page-posts .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black);color:var(--surface)}.page-posts .btn-secondary{background:transparent;color:var(--fg);border-color:var(--border)}.page-posts .btn-secondary:hover{background:var(--fg);color:var(--surface);border-color:var(--fg)}
.page-posts .comments{padding-block:80px;border-top:1px solid var(--border)}.page-posts .comments-wrap{max-width:740px}.page-posts .comments h2{font-size:clamp(36px,5vw,54px);margin:0}.page-posts .comment-empty{color:var(--muted);padding:28px 0;border-bottom:1px solid var(--border)}.page-posts .comment-list{display:grid}.page-posts .comment{padding:24px 0;border-bottom:1px solid var(--border)}.page-posts .comment-head{display:flex;justify-content:space-between;gap:16px}.page-posts .comment p{margin:8px 0 0}.page-posts .comment-form{display:grid;gap:18px;margin-top:36px}.page-posts .field{display:grid;gap:7px}.page-posts .field label{font-size:13px;color:var(--muted)}.page-posts .input,.page-posts .textarea{width:100%;border:1px solid var(--border);background:var(--surface);color:var(--fg);padding:13px 14px;border-radius:0}.page-posts .textarea{min-height:130px;resize:vertical}.page-posts .input:focus,.page-posts .textarea:focus{outline:2px solid var(--accent);outline-offset:0;border-color:var(--accent)}.page-posts .form-status{min-height:24px;color:var(--accent);font-weight:700}
.page-posts .related{padding-block:80px;border-top:1px solid var(--border)}.page-posts .related-grid{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid var(--fg)}.page-posts .related-card{padding:24px 0;border-bottom:1px solid var(--border)}.page-posts .related-card+.related-card{margin-inline-start:28px}.page-posts .related-card span{font:12px/1.5 var(--font-mono);color:var(--muted)}.page-posts .related-card h3{font-size:24px;margin:12px 0}.page-posts .related-card a{font-weight:700}.page-posts .related-card a:hover{color:var(--accent)}
.page-posts .pagefoot{padding-block:54px;border-top:1px solid var(--border);color:var(--muted)}.page-posts .footer-grid{display:grid;grid-template-columns:1.4fr repeat(3,1fr);gap:44px}.page-posts .footer-col strong{display:block;color:var(--fg);margin-bottom:12px}.page-posts .footer-col a{display:block;min-height:36px}.page-posts .footer-col a:hover{color:var(--fg);text-decoration:underline}.page-posts .footer-bottom{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;padding-top:28px;margin-top:38px;border-top:1px solid var(--border);font-size:13px}
.page-posts .menu-panel{position:fixed;inset:0;z-index:60;visibility:hidden;pointer-events:none}.page-posts .menu-panel.open{visibility:visible;pointer-events:auto}.page-posts .menu-scrim{position:absolute;inset:0;border:0;background:color-mix(in oklch,var(--fg) 55%,transparent);opacity:0}.page-posts .menu-panel.open .menu-scrim{opacity:1}.page-posts .menu-drawer{position:absolute;inset-block:0;inset-inline-start:0;width:min(520px,92vw);padding:28px clamp(24px,5vw,54px);background:var(--bg);border-inline-end:1px solid var(--fg);transform:translateX(105%);transition:transform .25s ease;display:flex;flex-direction:column}.page-posts .menu-panel.open .menu-drawer{transform:translateX(0)}.page-posts .menu-head{display:flex;justify-content:space-between;align-items:center;padding-bottom:24px;border-bottom:1px solid var(--border)}.page-posts .menu-links{display:grid;margin-top:34px}.page-posts .menu-links a{min-height:70px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);font-family:var(--font-display);font-size:clamp(27px,4vw,38px)}.page-posts .menu-links a:hover{color:var(--accent)}.page-posts .menu-foot{margin-top:auto;color:var(--muted)}
@media(max-width:920px){.page-posts .nav-links{display:none}.page-posts .article-layout{grid-template-columns:1fr}.page-posts .toc{position:static;order:0;margin-top:24px}.page-posts .footer-grid{grid-template-columns:1fr 1fr}.page-posts .related-grid{grid-template-columns:1fr}.page-posts .related-card+.related-card{margin-inline-start:0}.page-posts .article-body{font-size:18px}}
@media(max-width:620px){.page-posts .nav-login{display:none}.page-posts .article-head h1{font-size:48px}.page-posts .article-layout,.page-posts .comments,.page-posts .related{padding-block:56px}.page-posts .footer-grid{grid-template-columns:1fr;gap:28px}.page-posts .article-meta{align-items:flex-start;flex-direction:column}.page-posts .steps li{grid-template-columns:38px 1fr}}
@media(prefers-reduced-motion:reduce){.page-posts *{scroll-behavior:auto!important;transition-duration:.01ms!important}}
@@ -0,0 +1,95 @@
/* Page-scoped styles: .page-articles. Shared tokens live in globals.css. */
.page-articles *, .page-articles *::before, .page-articles *::after { box-sizing: border-box; }
.page-articles { -webkit-text-size-adjust: 100%; }
.page-articles { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.7 var(--font-body); text-rendering: optimizeLegibility; }
.page-articles a { color: inherit; text-decoration: none; }
.page-articles button { color: inherit; font: inherit; cursor: pointer; }
.page-articles h1, .page-articles h2, .page-articles h3 { margin: 0; font-family: var(--font-display); text-wrap: balance; }
.page-articles p { text-wrap: pretty; }
.page-articles :focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.page-articles .container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
.page-articles .topnav { position: sticky; top: 0; z-index: 20; background: color-mix(in oklch, var(--bg) 92%, transparent); backdrop-filter: blur(14px); border-bottom: 1px solid var(--border); }
.page-articles .topnav-inner { min-height: 70px; display: flex; align-items: center; justify-content: space-between; gap: 28px; }
.page-articles .logo { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; font-size: 23px; font-weight: 700; }
.page-articles .logo-mark { width: 30px; height: 34px; object-fit: contain; border-radius: 4px; }
.page-articles .topnav nav { display: flex; align-items: center; gap: 28px; }
.page-articles .topnav nav a { min-height: 44px; display: inline-flex; align-items: center; color: var(--muted); border-bottom: 1px solid transparent; }
.page-articles .topnav nav a:hover, .page-articles .topnav nav a[aria-current="page"] { color: var(--fg); border-color: var(--accent); }
.page-articles .nav-action { min-height: 44px; display: inline-flex; align-items: center; padding-inline: 16px; border: 1px solid var(--border); }
.page-articles .nav-action:hover { border-color: var(--fg); background: var(--fg-soft); }
.page-articles .hero { padding-block: clamp(56px, 8vw, 96px); border-bottom: 1px solid var(--border); }
.page-articles .hero-grid { display: grid; grid-template-columns: minmax(0, .9fr) minmax(360px, 1.1fr); gap: clamp(36px, 7vw, 88px); align-items: end; }
.page-articles .eyebrow { margin: 0 0 18px; color: var(--accent); font: 11px/1.5 var(--font-mono); }
.page-articles h1 { max-width: 10ch; font-size: clamp(48px, 7vw, 88px); line-height: 1.06; }
.page-articles .lead { max-width: 56ch; margin: 24px 0 0; color: var(--muted); font-size: clamp(18px, 2vw, 22px); }
.page-articles .weekly { display: block; padding: clamp(24px, 4vw, 40px); background: var(--surface); border: 1px solid var(--border); border-top-color: var(--accent); }
.page-articles .weekly:hover { border-color: var(--fg); background: color-mix(in oklch, var(--surface) 88%, var(--fg)); }
.page-articles .weekly-meta { display: flex; justify-content: space-between; gap: 16px; color: var(--muted); font: 11px/1.5 var(--font-mono); }
.page-articles .weekly h2 { margin-top: 42px; font-size: clamp(28px, 4vw, 44px); line-height: 1.18; }
.page-articles .weekly p { margin: 14px 0 30px; color: var(--muted); }
.page-articles .weekly-link { min-height: 44px; display: inline-flex; align-items: center; font-weight: 700; border-bottom: 1px solid var(--fg); }
.page-articles .library { padding-block: clamp(52px, 8vw, 88px); }
.page-articles .library-tools { display: grid; grid-template-columns: minmax(0, 1fr) 240px; gap: 16px; margin-bottom: 22px; }
.page-articles .control { width: 100%; min-height: 48px; padding: 11px 14px; border: 1px solid var(--border); background: var(--surface); color: var(--fg); font: inherit; }
.page-articles .control::placeholder { color: var(--muted); }
.page-articles .control:hover { border-color: var(--fg); }
.page-articles .control:focus { outline: 3px solid var(--accent-soft); border-color: var(--accent); }
.page-articles .filter-panel { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; margin-bottom: 44px; padding-block: 22px; border-block: 1px solid var(--border); }
.page-articles .filter-group { min-width: 0; }
.page-articles .filter-label { display: block; margin-bottom: 10px; color: var(--muted); font: 11px/1.5 var(--font-mono); }
.page-articles .filters { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 3px; scrollbar-width: none; }
.page-articles .filters::-webkit-scrollbar { display: none; }
.page-articles .filter { flex: none; min-height: 44px; padding: 9px 15px; border: 1px solid var(--border); background: transparent; color: var(--muted); }
.page-articles .filter:hover { color: var(--fg); border-color: var(--fg); }
.page-articles .filter[aria-pressed="true"] { background: var(--fg); color: var(--surface); border-color: var(--fg); }
.page-articles .results-head { display: flex; align-items: end; justify-content: space-between; gap: 20px; margin-bottom: 24px; }
.page-articles h2 { font-size: clamp(30px, 4vw, 46px); line-height: 1.15; }
.page-articles .foryou { color: var(--accent); font: 11px/1.5 var(--font-mono); }
.page-articles .count { color: var(--muted); font: 12px/1.5 var(--font-mono); }
.page-articles .posts { border-top: 1px solid var(--fg); }
.page-articles .post { display: grid; grid-template-columns: 138px minmax(0, 1fr) 168px; gap: 28px; align-items: start; padding-block: 28px; border-bottom: 1px solid var(--border); }
.page-articles .post[hidden] { display: none; }
.page-articles .post-meta { color: var(--muted); font: 12px/1.8 var(--font-mono); }
.page-articles .post-meta span { display: block; }
.page-articles .post h3 { font-size: clamp(22px, 2.5vw, 30px); line-height: 1.3; }
.page-articles .post p { max-width: 62ch; margin: 9px 0 0; color: var(--muted); }
.page-articles .post-side { justify-self: stretch; display: grid; gap: 12px; }
.page-articles .access { justify-self: end; display: inline-flex; align-items: center; min-height: 34px; padding: 5px 11px; border: 1px solid var(--border); color: var(--muted); font: 11px/1.5 var(--font-mono); }
.page-articles .access.free { border-color: var(--fg); color: var(--fg); }
.page-articles .engagement { display: flex; justify-content: flex-end; gap: 12px; color: var(--muted); font: 11px/1.5 var(--font-mono); }
.page-articles .like { min-height: 44px; padding: 0 10px; border: 1px solid transparent; background: transparent; color: var(--muted); }
.page-articles .like:hover, .page-articles .like[aria-pressed="true"] { color: var(--fg); border-color: var(--border); }
.page-articles .post-link { display: inline-flex; align-items: center; min-height: 44px; margin-top: 14px; font-weight: 700; border-bottom: 1px solid transparent; }
.page-articles .post-link:hover { color: var(--accent); border-color: var(--accent); }
.page-articles .soon { color: var(--muted); cursor: default; }
.page-articles .empty { display: none; padding: 42px 0; color: var(--muted); }
.page-articles .empty.show { display: block; }
.page-articles .pagefoot { padding-block: 48px; border-top: 1px solid var(--border); color: var(--muted); font-size: 13px; }
.page-articles .footer-row { display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
@media (max-width: 920px) {
.page-articles .topnav-inner { flex-wrap: wrap; gap: 0 16px; padding-top: 10px; }
.page-articles .logo, .page-articles .nav-action { margin-bottom: 10px; }
.page-articles .topnav nav { order: 3; flex: 0 0 calc(100% + (var(--gutter) * 2)); gap: 6px; margin-inline: calc(var(--gutter) * -1); padding-inline: var(--gutter); overflow-x: auto; border-top: 1px solid var(--border); scrollbar-width: none; }
.page-articles .topnav nav::-webkit-scrollbar { display: none; }
.page-articles .topnav nav a { flex: none; min-height: 52px; padding-inline: 12px; white-space: nowrap; }
.page-articles .hero-grid, .page-articles .filter-panel { grid-template-columns: 1fr; gap: 20px; }
.page-articles .weekly { margin-top: 8px; }
.page-articles .post { grid-template-columns: 112px minmax(0, 1fr); }
.page-articles .post-side { justify-self: start; grid-column: 2; }
.page-articles .access { justify-self: start; }
.page-articles .engagement { justify-content: flex-start; }
}
@media (max-width: 620px) {
.page-articles .nav-action { padding-inline: 12px; }
.page-articles h1 { font-size: clamp(42px, 13vw, 58px); }
.page-articles .library-tools { grid-template-columns: 1fr; }
.page-articles .results-head { align-items: start; flex-direction: column; }
.page-articles .post { grid-template-columns: 1fr; gap: 12px; }
.page-articles .post-side { grid-column: 1; }
.page-articles .post-link { width: 100%; border-top: 1px solid var(--border); padding-top: 12px; }
}
@media (prefers-reduced-motion: reduce) {
.page-articles * { scroll-behavior: auto !important; transition-duration: .01ms !important; }
}
@@ -0,0 +1,8 @@
/* Page-scoped styles: .page-verify-email. Shared tokens live in globals.css. */
.page-verify-email *,.page-verify-email *::before,.page-verify-email *::after{box-sizing:border-box}.page-verify-email{-webkit-text-size-adjust:100%}.page-verify-email{margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body);min-height:100vh}.page-verify-email a{color:inherit;text-decoration:none}.page-verify-email button{font:inherit;cursor:pointer}.page-verify-email h1,.page-verify-email h2{font-family:var(--font-display);text-wrap:balance}.page-verify-email p{text-wrap:pretty}.page-verify-email :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}.page-verify-email .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-verify-email .topnav{border-bottom:1px solid var(--border)}.page-verify-email .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:20px}.page-verify-email .logo{display:inline-flex;align-items:center;gap:10px;font-size:23px;font-weight:700}.page-verify-email .logo-mark{width:30px;height:34px;object-fit:contain;border-radius:4px}.page-verify-email .nav-return{min-height:44px;display:inline-flex;align-items:center;color:var(--muted);border-bottom:1px solid transparent}.page-verify-email .nav-return:hover{color:var(--fg);border-color:var(--fg)}
.page-verify-email .verification{min-height:calc(100vh - 70px);display:grid;place-items:center;padding-block:clamp(54px,9vw,110px)}.page-verify-email .card{width:min(700px,100%);background:color-mix(in oklch,var(--surface) 92%,transparent);border:1px solid var(--border);padding:clamp(28px,6vw,64px)}.page-verify-email .eyebrow{font:11px/1.5 var(--font-mono);color:var(--accent);margin:0 0 20px}.page-verify-email .card h1{font-size:clamp(42px,7vw,72px);line-height:1.08;margin:0;max-width:11ch}.page-verify-email .lead{font-size:clamp(18px,2vw,21px);color:var(--muted);max-width:54ch;margin:22px 0 0}.page-verify-email .email{display:block;color:var(--fg);font:700 16px/1.6 var(--font-mono);direction:ltr;text-align:right;overflow-wrap:anywhere;margin-top:8px}.page-verify-email .steps{list-style:none;margin:36px 0;padding:0;border-top:1px solid var(--fg)}.page-verify-email .steps li{display:grid;grid-template-columns:34px 1fr;gap:14px;padding:17px 0;border-bottom:1px solid var(--border)}.page-verify-email .step-num{font:12px/1.8 var(--font-mono);color:var(--accent)}.page-verify-email .actions{display:flex;gap:12px;flex-wrap:wrap}.page-verify-email .btn{min-height:50px;display:inline-flex;align-items:center;justify-content:center;padding:12px 22px;border:1px solid transparent;font-weight:700}.page-verify-email .btn-primary{background:var(--accent);color:var(--bg);border-color:var(--accent)}.page-verify-email .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black);color:var(--bg)}.page-verify-email .btn-secondary{background:transparent;color:var(--fg);border-color:var(--border)}.page-verify-email .btn-secondary:hover{border-color:var(--fg);background:var(--fg-soft)}.page-verify-email .status{min-height:27px;color:var(--muted);font-size:14px;margin:16px 0 0}.page-verify-email .fine{color:var(--muted);font-size:12px;margin:24px 0 0;padding-top:20px;border-top:1px solid var(--border)}
@media(max-width:620px){.page-verify-email .nav-inner{gap:10px}.page-verify-email .logo{font-size:20px}.page-verify-email .nav-return{font-size:13px}.page-verify-email .verification{align-items:start;padding-block:34px}.page-verify-email .card{padding:28px 20px}.page-verify-email .actions{align-items:stretch}.page-verify-email .actions .btn{width:100%}}
@media(prefers-reduced-motion:reduce){.page-verify-email *{transition-duration:.01ms!important}}
@@ -0,0 +1,5 @@
/* Page-scoped styles: .page-forgot-password. Shared tokens live in globals.css. */
.page-forgot-password *,.page-forgot-password *::before,.page-forgot-password *::after{box-sizing:border-box}.page-forgot-password{-webkit-text-size-adjust:100%}.page-forgot-password{margin:0;background:var(--bg);color:var(--fg);font:16px/1.7 var(--font-body)}.page-forgot-password a{color:inherit;text-decoration:none}.page-forgot-password button,.page-forgot-password input{font:inherit}.page-forgot-password button{cursor:pointer}.page-forgot-password h1{font-family:var(--font-display);text-wrap:balance}.page-forgot-password p{text-wrap:pretty}.page-forgot-password .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-forgot-password .topnav{border-bottom:1px solid var(--border)}.page-forgot-password .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}.page-forgot-password .logo{display:inline-flex;align-items:center;gap:10px;font-size:23px;font-weight:700}.page-forgot-password .nav-return{min-height:44px;display:inline-flex;align-items:center;color:var(--muted);border-bottom:1px solid transparent}.page-forgot-password .nav-return:hover{color:var(--fg);border-color:var(--fg)}
.page-forgot-password .reset{min-height:calc(100dvh - 142px);display:grid;place-items:center;padding:64px var(--gutter)}.page-forgot-password .reset-card{width:min(520px,100%);border:1px solid var(--border);background:var(--surface);padding:clamp(28px,6vw,54px)}.page-forgot-password .eyebrow{font:11px/1.5 var(--font-mono);color:var(--accent);margin:0 0 18px}.page-forgot-password .reset-card h1{font-size:clamp(38px,6vw,58px);line-height:1.12;margin:0}.page-forgot-password .lead{color:var(--muted);font-size:18px;margin:16px 0 30px}.page-forgot-password .field{display:grid;gap:8px}.page-forgot-password .field label{font-size:13px;color:var(--muted)}.page-forgot-password .input{width:100%;min-height:52px;border:1px solid var(--border);background:var(--bg);color:var(--fg);padding:13px 14px}.page-forgot-password .input:focus{outline:2px solid var(--accent);outline-offset:1px}.page-forgot-password .btn{width:100%;min-height:50px;margin-top:18px;border:1px solid var(--accent);background:var(--accent);color:var(--surface);font-weight:700;padding:12px 20px}.page-forgot-password .btn:hover{background:color-mix(in oklch,var(--accent) 86%,black)}.page-forgot-password .status{min-height:26px;margin:16px 0 0;color:var(--fg)}.page-forgot-password .back{min-height:44px;display:inline-flex;align-items:center;margin-top:18px;color:var(--muted);text-decoration:underline;text-underline-offset:4px}.page-forgot-password .back:hover{color:var(--fg)}.page-forgot-password .pagefoot{padding-block:26px;border-top:1px solid var(--border);color:var(--muted);text-align:center;font-size:13px}
@media(max-width:620px){.page-forgot-password .nav-return{display:none}.page-forgot-password .reset{padding-block:40px;align-items:start}.page-forgot-password .reset-card{padding:26px 20px}}
@@ -0,0 +1,124 @@
/* Homepage presentation, scoped to avoid changing article and account layouts. */
.home-page {
--border: #2a2d32;
--surface: #15171a;
--muted: #a5a39d;
--radius: 10px;
--radius-lg: 20px;
background: linear-gradient(180deg, #0b0c0eee, #0c0d0f 70%);
}
.home-page .topnav { background: #0c0d0fed; }
.home-page .nav-inner { min-height: 84px; }
.home-page .logo { text-decoration: none; }
.home-page .nav-action { border-radius: 8px; font-size: 14px; }
.home-page .section { border-bottom: 0; padding-block: 76px; }
.home-page section[id] { scroll-margin-top: 135px; }
.home-page .hero { padding-block: 86px 72px; background: radial-gradient(ellipse at 20% 30%, #3a332640, transparent 60%); }
.home-page .hero-split { grid-template-columns: 1.1fr 1fr; gap: 64px; }
.home-page .eyebrow { font-family: var(--font-body); font-size: 13px; font-weight: 600; }
.home-page .hero-copy h1 { max-width: 15ch; font-size: clamp(42px, 4.7vw, 64px); line-height: 1.4; letter-spacing: -.025em; }
.home-page .hero-highlight { color: var(--accent); }
.home-page .hero .lead { font-size: 18px; max-width: 44ch; margin-top: 20px; }
.home-page .btn { min-height: 52px; padding-inline: 24px; }
.home-page .btn-ghost { border-color: var(--border); }
.home-page .trust-line { font-size: 13px; line-height: 1.9; }
.home-page .deck { min-height: 460px; }
.home-page .deck-frame { min-height: 400px; border: 1px solid #33363b; border-radius: 20px; background: linear-gradient(135deg, #1c1e22, #121316); box-shadow: 0 24px 60px #0003; }
.home-page .deck-frame::before { content: ''; position: absolute; width: 280px; height: 280px; border: 1px solid #d4b47c26; border-radius: 50%; left: -100px; top: -120px; box-shadow: 0 0 0 35px #d4b47c0a, 0 0 0 70px #d4b47c08; pointer-events: none; }
.home-page .deck-slide { padding: 30px; }
.home-page .deck-slide::after { background: var(--accent); height: 3px; opacity: .65; }
.home-page .deck-slide-index { color: var(--accent); font: 24px var(--font-mono); }
.home-page .deck-slide-body h2 { font-size: clamp(30px, 3.2vw, 42px); line-height: 1.5; max-width: 16ch; margin: 0; }
.home-page .deck-slide-body p { font-size: 15px; line-height: 1.9; }
.home-page .deck-slide-kicker { font: 12px var(--font-body); }
.home-page .deck-slide-foot { font-size: 13px; }
.home-page .deck-slide-link { color: var(--accent); border: 0; }
.home-page .icon-btn { border-radius: 50%; }
.home-page .deck-dot { height: 44px; position: relative; background: transparent; }
.home-page .deck-dot::before { content: ''; position: absolute; inset: 20px 0; background: var(--border); border-radius: 8px; }
.home-page .deck-dot.active::before { background: var(--accent); }
.home-page .section-head { margin-bottom: 30px; gap: 32px; }
.home-page .section h2 { font-size: clamp(28px, 3vw, 40px); line-height: 1.5; margin: 0; }
.home-page .section-head .lead { font-size: 16px; max-width: 35ch; }
.home-page .topic-grid { border: 0; gap: 16px; }
.home-page .topic { position: relative; min-height: 158px; border: 1px solid var(--border); border-radius: 12px; padding: 24px; background: #141619; }
.home-page .topic::before { content: '01'; color: var(--accent); font: 12px var(--font-mono); margin-bottom: 12px; }
.home-page .topic:nth-child(2)::before { content: '02'; color: #c9c6bf; }
.home-page .topic:nth-child(3)::before { content: '03'; color: #d4b47c; }
.home-page .topic:nth-child(4)::before { content: '04'; color: #c9c6bf; }
.home-page .topic::after { content: '↖'; position: absolute; top: 20px; left: 22px; color: var(--muted); }
.home-page .topic strong { font-size: 21px; margin-bottom: 4px; }
.home-page .topic:hover { border-color: var(--accent); background: #1a1c20; }
.home-page #catalog { background: #101114; }
.home-page .filterbar { gap: 10px; margin-bottom: 28px; }
.home-page .filter { border-radius: 30px; font-size: 14px; padding-inline: 22px; }
.home-page .filter.active { background: var(--fg); border-color: var(--fg); color: var(--bg); }
.home-page .catalog { border: 0; gap: 24px; }
.home-page .catalog-card { border: 1px solid var(--border); padding: 14px; border-radius: 16px; background: #141619; }
.home-page .cover { min-height: 172px; border-radius: 9px; margin-bottom: 20px; background: linear-gradient(135deg, #3a3d43, #1a1c20); color: #f4f2ee; }
.home-page .cover-light { background: linear-gradient(135deg, #43392d, #1f1b16); }
.home-page .cover-lined { background: repeating-linear-gradient(135deg, transparent, transparent 22px, #ffffff06 23px, #ffffff06 24px), #2a2826; border: 0; }
.home-page .cover-label { font: 12px var(--font-body); color: #e4e1db; }
.home-page .cover-title { font-size: 26px; line-height: 1.5; max-width: 14ch; }
.home-page .card-meta, .home-page .catalog-card h3, .home-page .catalog-card p, .home-page .card-link { margin-inline: 8px; }
.home-page .catalog-card h3 { font-size: 20px; line-height: 1.65; }
.home-page .catalog-card p { line-height: 1.9; }
.home-page .card-link { color: var(--accent); font-size: 14px; padding-bottom: 8px; }
.home-page .membership { margin: 64px auto; max-width: 1116px; border: 1px solid #3a3d43; border-radius: 20px; background: linear-gradient(115deg, #1e1d1b, #141517); color: var(--fg); }
.home-page .membership .eyebrow { color: var(--accent); opacity: 1; }
.home-page .membership .lead { color: var(--muted); font-size: 17px; }
.home-page .membership-list, .home-page .membership-list li { border-color: #ffffff20; }
.home-page .membership-list li span:last-child { color: var(--accent); font-size: 14px; }
.home-page .membership .btn-primary { background: var(--fg); border-color: var(--fg); color: var(--bg); }
.home-page .pagefoot { padding-block: 36px; }
.home-page .pagefoot .row-between { flex-wrap: wrap; font-size: 13px; }
.home-page .footer-home-brand { font-size: 20px; font-weight: 700; color: var(--fg); }
.home-page .footer-home-brand span { display: block; font-size: 12px; font-weight: 400; color: var(--muted); }
@media (max-width: 920px) {
.home-page .hero-split { grid-template-columns: 1fr; gap: 36px; }
.home-page .hero-copy h1 { max-width: 20ch; }
.home-page .hero { padding-top: 48px; }
.home-page .deck { min-height: 0; }
.home-page .membership { margin-inline: 22px; }
.home-page .nav-inner { min-height: 64px; }
}
@media (max-width: 620px) {
.home-page .section { padding-block: 44px; }
.home-page .hero-copy h1 { font-size: 36px !important; line-height: 1.5; }
.home-page .hero .lead { font-size: 16px; }
.home-page .hero-cta { margin-top: 24px; }
.home-page .nav-action { display: inline-flex; font-size: 12px; padding-inline: 10px; }
.home-page .topic-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.home-page .topic { padding: 16px; min-height: 150px; }
.home-page .topic span { font-size: 12px; }
.home-page .topic::after { left: 14px; top: 14px; }
.home-page .deck-frame { min-height: 390px; }
.home-page .deck-slide { padding: 24px; }
.home-page .deck-slide-body h2 { font-size: 30px; }
.home-page .deck-slide-foot { flex-direction: row; align-items: center; gap: 12px; }
.home-page .deck-slide-format { font-size: 12px; }
.home-page .play { width: 28px; height: 28px; }
.home-page .membership { margin: 32px 18px; }
.home-page .membership-grid { gap: 28px; }
.home-page .filterbar { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 6px; }
.home-page .filter { flex: none; }
.home-page .pagefoot .row-between { flex-direction: column; align-items: start; gap: 18px; }
}
/* Carousel controls and content must not inherit legacy active backgrounds. */
#site.home-page .deck-dot, #site.home-page .deck-dot.active { width: 34px; height: 44px; border: 0; padding: 0; background: transparent; border-radius: 8px; }
#site.home-page .deck-dot::before { inset: 20px 0; background: var(--border); }
#site.home-page .deck-dot.active::before { background: var(--accent); }
#site.home-page .deck-frame { display: grid; min-height: 400px; }
#site.home-page .deck-slide { position: relative; inset: auto; min-height: 400px; gap: 24px; }
#site.home-page .deck-slide-body h2 { max-width: none; }
#site.home-page .deck-slide-foot { flex-wrap: wrap; }
#site.home-page .deck-controls { gap: 16px; align-items: center; }
#site.home-page .deck-arrows { display: flex; gap: 8px; }
#site.home-page .deck-controls button:disabled { cursor: default; }
@media (max-width: 620px) {
#site.home-page .deck-slide { min-height: 390px; padding: 24px; }
#site.home-page .deck-controls { gap: 8px; }
#site.home-page .deck-arrows { gap: 6px; }
#site.home-page .deck-dot, #site.home-page .deck-dot.active { width: 28px; }
}
+9
View File
@@ -0,0 +1,9 @@
/* Page-scoped styles: .page-login. Shared tokens live in globals.css. */
.page-login *,.page-login *::before,.page-login *::after{box-sizing:border-box}.page-login{-webkit-text-size-adjust:100%}.page-login{margin:0;background:var(--bg);color:var(--fg);font:16px/1.7 var(--font-body);text-rendering:optimizeLegibility}.page-login.locked{overflow:hidden}.page-login a{color:inherit;text-decoration:none}.page-login button,.page-login input{font:inherit}.page-login button{cursor:pointer}.page-login h1,.page-login h2,.page-login h3{font-family:var(--font-display);text-wrap:balance}.page-login p{text-wrap:pretty}.page-login :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}.page-login .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-login .topnav{border-bottom:1px solid var(--border)}.page-login .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}.page-login .logo{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-display);font-size:23px;font-weight:700}.page-login .logo-mark{width:12px;height:12px;background:var(--fg);transform:rotate(45deg)}.page-login .nav-return{min-height:44px;display:inline-flex;align-items:center;color:var(--muted);border-bottom:1px solid transparent}.page-login .nav-return:hover{color:var(--fg);border-color:var(--fg)}.page-login .icon-btn{width:46px;height:46px;display:grid;place-items:center;border:1px solid var(--fg);background:var(--fg);color:var(--surface)}.page-login .icon-btn:hover{background:var(--accent);border-color:var(--accent);color:var(--surface)}.page-login .nav-tools{display:flex;align-items:center;gap:14px}
.page-login .auth{min-height:calc(100vh - 70px);display:grid;grid-template-columns:1fr 1fr}.page-login .auth-story{padding:clamp(54px,8vw,104px) max(var(--gutter),calc((100vw - var(--container))/2 + var(--gutter)));padding-inline-end:clamp(48px,8vw,100px);background:var(--fg);color:var(--surface);display:flex;flex-direction:column;justify-content:space-between}.page-login .eyebrow{font:11px/1.5 var(--font-mono);color:var(--accent);margin:0 0 20px}.page-login .auth-story .eyebrow{color:var(--surface);opacity:.68}.page-login .auth-story h1{font-size:clamp(46px,6vw,82px);line-height:1.08;max-width:9ch;margin:0}.page-login .auth-story>div>p{color:color-mix(in oklch,var(--surface) 68%,transparent);font-size:19px;max-width:40ch}.page-login .story-list{list-style:none;margin:54px 0 0;padding:0;border-top:1px solid color-mix(in oklch,var(--surface) 24%,transparent)}.page-login .story-list li{padding:15px 0;border-bottom:1px solid color-mix(in oklch,var(--surface) 15%,transparent);display:flex;justify-content:space-between;gap:20px}.page-login .story-mark{font:12px/1.5 var(--font-mono);opacity:.58}.page-login .story-foot{font-size:13px;color:color-mix(in oklch,var(--surface) 58%,transparent)}
.page-login .auth-form-wrap{padding:clamp(54px,8vw,104px) max(var(--gutter),calc((100vw - var(--container))/2 + var(--gutter)));padding-inline-start:clamp(48px,8vw,100px);display:flex;align-items:center}.page-login .auth-form{width:min(440px,100%)}.page-login .auth-form h2{font-size:clamp(38px,4vw,54px);line-height:1.15;margin:0}.page-login .lead{color:var(--muted);font-size:18px;margin:12px 0 34px}.page-login .field{display:grid;gap:7px;margin-top:18px}.page-login .field label{font-size:13px;color:var(--muted)}.page-login .input-wrap{position:relative}.page-login .input{width:100%;min-height:52px;border:1px solid var(--border);background:var(--surface);color:var(--fg);padding:13px 14px;border-radius:0}.page-login .input:focus{outline:2px solid var(--accent);outline-offset:0;border-color:var(--accent)}.page-login .password-toggle{position:absolute;inset-inline-end:6px;top:6px;min-width:70px;height:40px;border:0;background:transparent;color:var(--muted)}.page-login .password-toggle:hover{color:var(--fg);background:var(--fg-soft)}.page-login .form-row{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:18px 0 24px;font-size:13px}.page-login .check{display:flex;align-items:center;gap:8px}.page-login .form-row a{text-decoration:underline;text-underline-offset:3px}.page-login .form-row a:hover{color:var(--accent)}.page-login .btn{min-height:50px;width:100%;display:inline-flex;align-items:center;justify-content:center;padding:12px 20px;border:1px solid transparent;font-weight:700}.page-login .btn-primary{background:var(--accent);color:var(--surface);border-color:var(--accent)}.page-login .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black);color:var(--surface)}.page-login .divider{display:flex;align-items:center;gap:14px;color:var(--muted);font-size:12px;margin:26px 0}.page-login .divider::before,.page-login .divider::after{content:"";height:1px;background:var(--border);flex:1}.page-login .magic{min-height:50px;width:100%;border:1px solid var(--border);background:transparent;color:var(--fg);font-weight:700}.page-login .magic:hover{background:var(--fg);color:var(--surface);border-color:var(--fg)}.page-login .signup{text-align:center;color:var(--muted);font-size:14px;margin-top:24px}.page-login .signup a{color:var(--fg);font-weight:700;text-decoration:underline;text-underline-offset:3px}.page-login .status{min-height:25px;color:var(--accent);font-weight:700;margin-top:14px}.page-login .fine{font-size:12px;color:var(--muted);margin-top:28px}
.page-login .menu-panel{position:fixed;inset:0;z-index:60;visibility:hidden;pointer-events:none}.page-login .menu-panel.open{visibility:visible;pointer-events:auto}.page-login .menu-scrim{position:absolute;inset:0;border:0;background:color-mix(in oklch,var(--fg) 55%,transparent);opacity:0}.page-login .menu-panel.open .menu-scrim{opacity:1}.page-login .menu-drawer{position:absolute;inset-block:0;inset-inline-start:0;width:min(520px,92vw);padding:28px clamp(24px,5vw,54px);background:var(--bg);border-inline-end:1px solid var(--fg);transform:translateX(105%);transition:transform .25s ease;display:flex;flex-direction:column}.page-login .menu-panel.open .menu-drawer{transform:translateX(0)}.page-login .menu-head{display:flex;justify-content:space-between;align-items:center;padding-bottom:24px;border-bottom:1px solid var(--border)}.page-login .menu-links{display:grid;margin-top:34px}.page-login .menu-links a{min-height:70px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);font-family:var(--font-display);font-size:clamp(27px,4vw,38px)}.page-login .menu-links a:hover{color:var(--accent)}.page-login .menu-foot{margin-top:auto;color:var(--muted)}
@media(max-width:920px){.page-login .auth{grid-template-columns:1fr}.page-login .auth-story{min-height:440px;padding:64px var(--gutter)}.page-login .auth-form-wrap{padding:64px var(--gutter);justify-content:center}.page-login .story-foot{margin-top:54px}}
@media(max-width:620px){.page-login .nav-return{display:none}.page-login .auth-story{min-height:auto}.page-login .auth-story h1{font-size:46px}.page-login .auth-form-wrap{padding-block:50px}.page-login .form-row{align-items:flex-start;flex-direction:column}}
@media(prefers-reduced-motion:reduce){.page-login *{transition-duration:.01ms!important}}
@@ -0,0 +1,11 @@
/* Page-scoped styles: .page-onboarding. Shared tokens live in globals.css. */
.page-onboarding *,.page-onboarding *::before,.page-onboarding *::after{box-sizing:border-box}.page-onboarding{-webkit-text-size-adjust:100%}.page-onboarding{margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body)}.page-onboarding a{color:inherit;text-decoration:none}.page-onboarding button,.page-onboarding input{font:inherit}.page-onboarding button{cursor:pointer}.page-onboarding h1,.page-onboarding h2,.page-onboarding h3{font-family:var(--font-display);text-wrap:balance}.page-onboarding p{text-wrap:pretty}.page-onboarding :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}.page-onboarding .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-onboarding .topnav{border-bottom:1px solid var(--border)}.page-onboarding .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:20px}.page-onboarding .logo{display:inline-flex;align-items:center;gap:10px;font-size:23px;font-weight:700}.page-onboarding .logo-mark{width:30px;height:34px;object-fit:contain;border-radius:4px}.page-onboarding .nav-return{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}.page-onboarding .nav-return:hover{color:var(--fg)}
.page-onboarding .head{padding-block:clamp(40px,6vw,64px) 34px;border-bottom:1px solid var(--border)}.page-onboarding .eyebrow{font:11px/1.5 var(--font-mono);color:var(--accent);margin:0 0 18px}.page-onboarding .head h1{font-size:clamp(40px,5vw,58px);line-height:1.16;margin:0;max-width:15ch}.page-onboarding .lead{font-size:clamp(18px,2vw,21px);color:var(--muted);max-width:58ch;margin:16px 0 0}.page-onboarding .progress{margin-top:26px;max-width:720px}.page-onboarding .progress-copy{display:flex;justify-content:space-between;color:var(--fg);font:12px/1.5 var(--font-mono);margin-bottom:10px}.page-onboarding .progress-track{height:6px;background:var(--border)}.page-onboarding .progress-bar{height:100%;width:33.333%;background:var(--accent);transition:width .2s ease}
.page-onboarding .flow{display:grid;grid-template-columns:220px minmax(0,1fr);gap:clamp(38px,7vw,84px);align-items:start;padding-block:46px 72px}.page-onboarding .step-nav{border-top:1px solid var(--fg)}.page-onboarding .step-nav div{min-height:58px;display:grid;grid-template-columns:34px 1fr;align-items:center;gap:12px;border-bottom:1px solid var(--border);color:var(--muted)}.page-onboarding .step-nav div.active{color:var(--fg);font-weight:700;border-bottom-color:var(--fg)}.page-onboarding .step-nav div.done{color:var(--fg)}.page-onboarding .step-nav span:first-child{font:12px/1 var(--font-mono)}.page-onboarding .step-nav div.active span:first-child{color:var(--accent)}.page-onboarding .panel{display:none;max-width:760px}.page-onboarding .panel.active{display:block}.page-onboarding .panel h2{font-size:clamp(32px,4vw,48px);line-height:1.2;margin:0 0 12px}.page-onboarding .intro{color:var(--muted);max-width:58ch;margin:0 0 30px}.page-onboarding .choices{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.page-onboarding .choice{position:relative;display:block}.page-onboarding .choice input{position:absolute;opacity:0}.page-onboarding .choice-body{min-height:145px;display:flex;flex-direction:column;padding:22px;border:1px solid var(--border);background:color-mix(in oklch,var(--surface) 90%,transparent)}.page-onboarding .choice-body:hover{border-color:var(--fg)}.page-onboarding .choice input:checked+.choice-body{border-color:var(--fg);background:var(--fg-soft)}.page-onboarding .choice input:focus-visible+.choice-body{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}.page-onboarding .choice strong{font-size:19px;padding-inline-end:34px}.page-onboarding .choice small{color:var(--muted);font-size:14px;margin-top:8px}.page-onboarding .choice-mark{position:absolute;inset-block-start:24px;inset-inline-end:22px;width:18px;height:18px;border:1px solid var(--fg)}.page-onboarding .choice input:checked+.choice-body .choice-mark{background:var(--fg);box-shadow:inset 0 0 0 4px var(--surface)}
.page-onboarding .benefits{border-top:1px solid var(--fg)}.page-onboarding .benefit{display:grid;grid-template-columns:42px 1fr;gap:16px;padding:20px 0;border-bottom:1px solid var(--border)}.page-onboarding .benefit-num{font:12px/1.8 var(--font-mono);color:var(--muted)}.page-onboarding .benefit h3{font-size:22px;margin:0 0 5px}.page-onboarding .benefit p{color:var(--muted);margin:0}.page-onboarding .upgrade-note{margin-top:26px;padding:22px;border:1px solid var(--border);background:var(--surface)}.page-onboarding .upgrade-note strong{display:block;font-size:18px}.page-onboarding .upgrade-note p{color:var(--muted);margin:6px 0 0}.page-onboarding .actions{display:flex;gap:12px;align-items:center;margin-top:32px}.page-onboarding .btn{min-height:50px;display:inline-flex;align-items:center;justify-content:center;padding:12px 22px;border:1px solid transparent;font-weight:700}.page-onboarding .btn-primary{background:var(--accent);color:var(--bg);border-color:var(--accent)}.page-onboarding .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black);color:var(--bg)}.page-onboarding .btn-secondary{background:transparent;color:var(--fg);border-color:var(--border)}.page-onboarding .btn-secondary:hover{border-color:var(--fg);background:var(--fg-soft)}.page-onboarding .status{min-height:26px;color:var(--muted);font-size:14px;margin:14px 0 0}.page-onboarding .pagefoot{padding-block:42px;border-top:1px solid var(--border);color:var(--muted);font-size:13px}.page-onboarding .footer-row{display:flex;justify-content:space-between;gap:18px;flex-wrap:wrap}
@media(max-width:920px){.page-onboarding .flow{grid-template-columns:1fr}.page-onboarding .step-nav{display:grid;grid-template-columns:repeat(3,1fr);border-top:0}.page-onboarding .step-nav div{padding-inline:12px;border:1px solid var(--border)}.page-onboarding .choices{grid-template-columns:1fr 1fr}}
@media(max-width:620px){.page-onboarding .logo{font-size:20px}.page-onboarding .nav-return{font-size:13px}.page-onboarding .head{padding-block:40px 30px}.page-onboarding .flow{padding-block:30px 60px;gap:30px}.page-onboarding .step-nav{overflow-x:auto;grid-template-columns:repeat(3,minmax(140px,1fr))}.page-onboarding .choices{grid-template-columns:1fr}.page-onboarding .choice-body{min-height:130px}.page-onboarding .actions{align-items:stretch;flex-direction:column-reverse}.page-onboarding .actions .btn{width:100%}.page-onboarding .footer-row{flex-direction:column}}
@media(prefers-reduced-motion:reduce){.page-onboarding *{transition-duration:.01ms!important}}
@@ -0,0 +1,39 @@
/* Page-scoped styles: .page-payment-cancelled. Shared tokens live in globals.css. */
.page-payment-cancelled *,.page-payment-cancelled *::before,.page-payment-cancelled *::after{box-sizing:border-box}
.page-payment-cancelled{-webkit-text-size-adjust:100%}
.page-payment-cancelled{min-height:100vh;margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body);text-rendering:optimizeLegibility}
.page-payment-cancelled a{color:inherit;text-decoration:none}
.page-payment-cancelled h1,.page-payment-cancelled h2{font-family:var(--font-display);text-wrap:balance}
.page-payment-cancelled p{text-wrap:pretty}
.page-payment-cancelled :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-payment-cancelled .container{width:min(100%,var(--container));margin-inline:auto;padding-inline:var(--gutter)}
.page-payment-cancelled .topnav{border-bottom:1px solid var(--border);background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px)}
.page-payment-cancelled .topnav-inner{min-height:72px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.page-payment-cancelled .logo{min-height:44px;display:inline-flex;align-items:center;gap:10px;font-size:22px;font-weight:700}
.page-payment-cancelled .logo-mark{width:30px;height:34px;object-fit:contain;border-radius:4px}
.page-payment-cancelled .nav-link{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}
.page-payment-cancelled .nav-link:hover{color:var(--fg)}
.page-payment-cancelled .section{padding-block:clamp(56px,9vw,104px)}
.page-payment-cancelled .state-card{max-width:760px;margin-inline:auto;padding:clamp(28px,6vw,64px);border:1px solid var(--border);background:color-mix(in oklch,var(--surface) 92%,transparent)}
.page-payment-cancelled .status-mark{width:64px;height:64px;display:grid;place-items:center;border:1px solid var(--fg);margin-bottom:30px}
.page-payment-cancelled .status-mark svg{width:28px;height:28px}
.page-payment-cancelled .eyebrow{margin:0 0 18px;color:var(--muted);font:12px/1.5 var(--font-mono);}
.page-payment-cancelled h1{max-width:12ch;margin:0;font-size:clamp(42px,7vw,72px);line-height:1.1;}
.page-payment-cancelled .lead{max-width:54ch;margin:20px 0 0;color:var(--muted);font-size:clamp(18px,2.3vw,22px)}
.page-payment-cancelled .notice{margin-top:34px;padding-block:22px;border-block:1px solid var(--border)}
.page-payment-cancelled .notice h2{margin:0 0 8px;font-size:24px}
.page-payment-cancelled .notice p{margin:0;color:var(--muted)}
.page-payment-cancelled .actions{display:flex;align-items:center;gap:12px;margin-top:30px}
.page-payment-cancelled .btn{min-height:50px;display:inline-flex;align-items:center;justify-content:center;padding:12px 22px;border:1px solid transparent;font-weight:700}
.page-payment-cancelled .btn-primary{background:var(--accent);color:var(--surface);border-color:var(--accent)}
.page-payment-cancelled .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black);color:var(--surface)}
.page-payment-cancelled .btn-secondary{background:transparent;color:var(--fg);border-color:var(--border)}
.page-payment-cancelled .btn-secondary:hover{background:var(--fg);color:var(--surface);border-color:var(--fg)}
.page-payment-cancelled .help{margin:24px 0 0;color:var(--muted);font-size:14px}
.page-payment-cancelled .help a{text-decoration:underline;text-underline-offset:4px;color:var(--fg)}
.page-payment-cancelled .help a:hover{color:var(--accent)}
.page-payment-cancelled .pagefoot{padding-block:36px;border-top:1px solid var(--border);color:var(--muted);font-size:13px}
.page-payment-cancelled .footer-row{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
@media(max-width:620px){.page-payment-cancelled .topnav-inner{min-height:64px}.page-payment-cancelled .state-card{padding:26px 20px}.page-payment-cancelled .actions{align-items:stretch;flex-direction:column}.page-payment-cancelled .actions .btn{width:100%}.page-payment-cancelled .footer-row{flex-direction:column;gap:8px}}
@@ -0,0 +1,40 @@
/* Page-scoped styles: .page-payment-success. Shared tokens live in globals.css. */
.page-payment-success *,.page-payment-success *::before,.page-payment-success *::after{box-sizing:border-box}
.page-payment-success{-webkit-text-size-adjust:100%}
.page-payment-success{min-height:100vh;margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body);text-rendering:optimizeLegibility}
.page-payment-success a{color:inherit;text-decoration:none}
.page-payment-success h1,.page-payment-success h2{font-family:var(--font-display);text-wrap:balance}
.page-payment-success p{text-wrap:pretty}
.page-payment-success :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-payment-success .container{width:min(100%,var(--container));margin-inline:auto;padding-inline:var(--gutter)}
.page-payment-success .topnav{border-bottom:1px solid var(--border);background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px)}
.page-payment-success .topnav-inner{min-height:72px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.page-payment-success .logo{min-height:44px;display:inline-flex;align-items:center;gap:10px;font-size:22px;font-weight:700}
.page-payment-success .logo-mark{width:30px;height:34px;object-fit:contain;border-radius:4px}
.page-payment-success .nav-link{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}
.page-payment-success .nav-link:hover{color:var(--fg)}
.page-payment-success .section{padding-block:clamp(56px,9vw,104px)}
.page-payment-success .state-card{max-width:760px;margin-inline:auto;padding:clamp(28px,6vw,64px);border:1px solid var(--border);background:color-mix(in oklch,var(--surface) 92%,transparent)}
.page-payment-success .status-mark{width:64px;height:64px;display:grid;place-items:center;border:1px solid var(--fg);margin-bottom:30px}
.page-payment-success .status-mark svg{width:30px;height:30px}
.page-payment-success .eyebrow{margin:0 0 18px;color:var(--accent);font:12px/1.5 var(--font-mono);}
.page-payment-success h1{max-width:12ch;margin:0;font-size:clamp(42px,7vw,72px);line-height:1.1;}
.page-payment-success .lead{max-width:54ch;margin:20px 0 0;color:var(--muted);font-size:clamp(18px,2.3vw,22px)}
.page-payment-success .summary{margin:34px 0 0;border-top:1px solid var(--fg)}
.page-payment-success .summary-row{display:grid;grid-template-columns:150px 1fr;gap:20px;padding:17px 0;border-bottom:1px solid var(--border)}
.page-payment-success .summary-row dt{color:var(--muted);font:12px/1.7 var(--font-mono)}
.page-payment-success .summary-row dd{margin:0;font-weight:700}
.page-payment-success .next-step{margin-top:34px;padding-top:30px;border-top:1px solid var(--border)}
.page-payment-success .next-step h2{margin:0 0 8px;font-size:26px}
.page-payment-success .next-step p{margin:0;color:var(--muted)}
.page-payment-success .actions{display:flex;align-items:center;gap:12px;margin-top:26px}
.page-payment-success .btn{min-height:50px;display:inline-flex;align-items:center;justify-content:center;padding:12px 22px;border:1px solid transparent;font-weight:700}
.page-payment-success .btn-primary{background:var(--accent);color:var(--surface);border-color:var(--accent)}
.page-payment-success .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black);color:var(--surface)}
.page-payment-success .btn-secondary{background:transparent;color:var(--fg);border-color:var(--border)}
.page-payment-success .btn-secondary:hover{background:var(--fg);color:var(--surface);border-color:var(--fg)}
.page-payment-success .pagefoot{padding-block:36px;border-top:1px solid var(--border);color:var(--muted);font-size:13px}
.page-payment-success .footer-row{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
@media(max-width:620px){.page-payment-success .topnav-inner{min-height:64px}.page-payment-success .state-card{padding:26px 20px}.page-payment-success .summary-row{grid-template-columns:1fr;gap:4px}.page-payment-success .actions{align-items:stretch;flex-direction:column}.page-payment-success .actions .btn{width:100%}.page-payment-success .footer-row{flex-direction:column;gap:8px}}
@@ -0,0 +1,63 @@
/* Page-scoped styles: .page-posts. Shared tokens live in globals.css. */
.page-posts *,.page-posts *::before,.page-posts *::after{box-sizing:border-box} .page-posts{scroll-behavior:smooth;-webkit-text-size-adjust:100%} .page-posts{margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body);text-rendering:optimizeLegibility} .page-posts.locked{overflow:hidden}
.page-posts a{color:inherit;text-decoration:none} .page-posts button,.page-posts input,.page-posts textarea{font:inherit} .page-posts button{cursor:pointer} .page-posts h1,.page-posts h2,.page-posts h3{font-family:var(--font-display);text-wrap:balance} .page-posts p{text-wrap:pretty} .page-posts :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-posts .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-posts .topnav{position:sticky;top:0;z-index:20;background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px);border-bottom:1px solid var(--border)} .page-posts .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.page-posts .logo{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-display);font-size:23px;font-weight:700}.page-posts .logo-mark{width:12px;height:12px;background:var(--fg);transform:rotate(45deg)} .page-posts .nav-links{display:flex;align-items:center;gap:28px}.page-posts .nav-links a{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}.page-posts .nav-links a:hover{color:var(--fg)}
.page-posts .nav-tools{display:flex;align-items:center;gap:8px}.page-posts .nav-login,.page-posts .icon-btn{min-height:44px;border:1px solid var(--border);background:transparent;color:var(--fg);display:inline-grid;place-items:center}.page-posts .nav-login{padding-inline:16px}.page-posts .nav-login:hover,.page-posts .icon-btn:hover{border-color:var(--fg);background:var(--fg-soft)}.page-posts .icon-btn{width:46px}
.page-posts .demo-strip{background:var(--fg);color:var(--surface);font:11px/1.6 var(--font-mono);padding:8px 0;text-align:center}
.page-posts .demo-strip b{color:var(--surface);font-weight:700}
.page-posts .article-head{padding-block:clamp(58px,9vw,100px);border-bottom:1px solid var(--border)}
.page-posts .crumb{font:12px/1.5 var(--font-mono);color:var(--accent);margin:0 0 20px}
.page-posts .lock-chip{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--fg);padding:6px 14px;font:12px/1.5 var(--font-mono);color:var(--fg);margin-bottom:22px}
.page-posts .lock-chip svg{width:14px;height:14px}
.page-posts .article-head h1{font-size:clamp(46px,7vw,88px);line-height:1.06;max-width:12ch;margin:0}
.page-posts .dek{font-size:clamp(19px,2vw,24px);line-height:1.7;color:var(--muted);max-width:58ch;margin:28px 0 0}
.page-posts .article-meta{display:flex;align-items:center;flex-wrap:wrap;gap:14px 24px;margin-top:34px;padding-top:20px;border-top:1px solid var(--border);font:12px/1.5 var(--font-mono);color:var(--muted)}
.page-posts .article-layout{display:grid;grid-template-columns:minmax(0,740px) 240px;gap:clamp(48px,8vw,100px);align-items:start;padding-block:80px}
.page-posts .article-body{font-size:19px;line-height:2}
.page-posts .article-body h2{font-size:clamp(32px,4vw,48px);line-height:1.2;margin:64px 0 20px}
.page-posts .article-body p{margin:0 0 26px}
.page-posts .lede{font-family:var(--font-display);font-size:28px;line-height:1.6}
.page-posts .note{border-block:1px solid var(--fg);padding:28px 0;margin:42px 0}
.page-posts .note strong{display:block;font-family:var(--font-display);font-size:22px;margin-bottom:8px}
.page-posts .free-tag{font:11px/1.5 var(--font-mono);color:var(--muted);margin-bottom:6px;display:block}
.page-posts .lock-cut{position:relative}
.page-posts .lock-cut::after{content:"";position:absolute;inset-inline:0;bottom:0;height:120px;background:linear-gradient(to bottom,transparent,var(--bg) 88%);pointer-events:none}
.page-posts .lock-cut .cut-p{margin-bottom:0}
.page-posts .paywall{margin-top:-30px;position:relative;border:1px solid var(--fg);background:var(--surface);padding:clamp(28px,4vw,44px);z-index:2}
.page-posts .paywall .crumb{margin-bottom:12px}
.page-posts .paywall h2{font-size:clamp(30px,3.6vw,44px);line-height:1.18;margin:0 0 14px;max-width:16ch}
.page-posts .paywall p{color:var(--muted);max-width:56ch;margin:0 0 24px}
.page-posts .locked-in{border-top:1px solid var(--fg);margin:0 0 26px}
.page-posts .locked-in li{display:flex;align-items:center;gap:12px;min-height:52px;border-bottom:1px solid var(--border);color:var(--muted);font-size:16px}
.page-posts .locked-in svg{width:14px;height:14px;flex:none}
.page-posts .locked-in .num{margin-inline-start:auto;font:12px/1.5 var(--font-mono)}
.page-posts .paywall-actions{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.page-posts .btn{min-height:46px;display:inline-flex;align-items:center;justify-content:center;padding:11px 22px;border:1px solid transparent;font-weight:700}
.page-posts .btn-primary{background:var(--accent);color:var(--surface);border-color:var(--accent)}
.page-posts .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black)}
.page-posts .btn-secondary{border-color:var(--border);background:transparent;color:var(--fg)}
.page-posts .btn-secondary:hover{border-color:var(--fg);background:var(--fg-soft)}
.page-posts .text-link{font-weight:700;color:var(--fg);text-decoration:underline;text-underline-offset:4px;min-height:44px;display:inline-flex;align-items:center}
.page-posts .text-link:hover{color:var(--accent)}
.page-posts .price-line{font:12px/1.6 var(--font-mono);color:var(--muted);margin-top:18px}
.page-posts .toc{position:sticky;top:100px;border-top:1px solid var(--fg);padding-top:18px}
.page-posts .toc strong{font-family:var(--font-display);font-size:20px}
.page-posts .toc a,.page-posts .toc .toc-locked{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--border);color:var(--muted)}
.page-posts .toc a:hover{color:var(--fg)}
.page-posts .toc .toc-locked{color:color-mix(in oklch,var(--muted) 60%,transparent)}
.page-posts .toc .toc-locked svg{width:12px;height:12px;flex:none}
.page-posts .member-note{margin-top:28px;border:1px solid var(--fg);padding:22px}
.page-posts .member-note strong{font-family:var(--font-display);font-size:20px;display:block;margin-bottom:8px}
.page-posts .member-note p{color:var(--muted);font-size:14px;margin:0 0 16px}
.page-posts .pagefoot{padding-block:54px;border-top:1px solid var(--border);color:var(--muted)}
.page-posts .footer-grid{display:grid;grid-template-columns:1.4fr repeat(3,1fr);gap:44px}
.page-posts .footer-col strong{display:block;color:var(--fg);margin-bottom:12px}
.page-posts .footer-col a{display:block;min-height:36px}
.page-posts .footer-col a:hover{color:var(--fg);text-decoration:underline}
.page-posts .footer-bottom{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;padding-top:28px;margin-top:38px;border-top:1px solid var(--border);font-size:13px}
.page-posts .menu-panel{position:fixed;inset:0;z-index:60;visibility:hidden;pointer-events:none}.page-posts .menu-panel.open{visibility:visible;pointer-events:auto}.page-posts .menu-scrim{position:absolute;inset:0;border:0;background:color-mix(in oklch,var(--fg) 55%,transparent);opacity:0}.page-posts .menu-panel.open .menu-scrim{opacity:1}.page-posts .menu-drawer{position:absolute;inset-block:0;inset-inline-start:0;width:min(520px,92vw);padding:28px clamp(24px,5vw,54px);background:var(--bg);border-inline-end:1px solid var(--fg);transform:translateX(105%);transition:transform .25s ease;display:flex;flex-direction:column}.page-posts .menu-panel.open .menu-drawer{transform:translateX(0)}.page-posts .menu-head{display:flex;justify-content:space-between;align-items:center;padding-bottom:24px;border-bottom:1px solid var(--border)}.page-posts .menu-links{display:flex;flex-direction:column}.page-posts .menu-links a{display:flex;justify-content:space-between;align-items:center;min-height:58px;border-bottom:1px solid var(--border);font-size:18px}
@media(max-width:920px){.page-posts .nav-links{display:none}.page-posts .article-layout{grid-template-columns:1fr}.page-posts .toc{position:static;order:0;margin-top:24px}.page-posts .footer-grid{grid-template-columns:1fr 1fr}.page-posts .article-body{font-size:18px}}
@media(max-width:620px){.page-posts .nav-login{display:none}.page-posts .article-head h1{font-size:46px}.page-posts .article-layout{padding-block:56px}.page-posts .footer-grid{grid-template-columns:1fr;gap:28px}.page-posts .paywall-actions{flex-direction:column;align-items:stretch}.page-posts .paywall-actions .btn{width:100%}}
@media(prefers-reduced-motion:reduce){.page-posts *{scroll-behavior:auto!important;transition-duration:.01ms!important}}
@@ -0,0 +1,62 @@
/* Page-scoped styles: .page-posts. Shared tokens live in globals.css. */
.page-posts *,.page-posts *::before,.page-posts *::after{box-sizing:border-box} .page-posts{scroll-behavior:smooth;-webkit-text-size-adjust:100%} .page-posts{margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body);text-rendering:optimizeLegibility} .page-posts.locked{overflow:hidden}
.page-posts a{color:inherit;text-decoration:none} .page-posts button,.page-posts input,.page-posts textarea{font:inherit} .page-posts button{cursor:pointer} .page-posts h1,.page-posts h2,.page-posts h3{font-family:var(--font-display);text-wrap:balance} .page-posts p{text-wrap:pretty} .page-posts :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-posts .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-posts .topnav{position:sticky;top:0;z-index:20;background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px);border-bottom:1px solid var(--border)} .page-posts .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.page-posts .logo{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-display);font-size:23px;font-weight:700}.page-posts .logo-mark{width:12px;height:12px;background:var(--fg);transform:rotate(45deg)} .page-posts .nav-links{display:flex;align-items:center;gap:28px}.page-posts .nav-links a{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}.page-posts .nav-links a:hover{color:var(--fg)}
.page-posts .nav-tools{display:flex;align-items:center;gap:8px}
.page-posts .nav-account{min-height:44px;display:inline-flex;align-items:center;gap:10px;padding-inline:16px;border:1px solid var(--fg);background:var(--fg);color:var(--surface);font-weight:700}.page-posts .nav-account:hover{background:var(--accent);border-color:var(--accent)}
.page-posts .nav-account .avatar{width:22px;height:22px;background:var(--surface);color:var(--fg);display:grid;place-items:center;font:12px/1 var(--font-mono);font-weight:700}
.page-posts .icon-btn{min-height:44px;width:46px;border:1px solid var(--border);background:transparent;color:var(--fg);display:inline-grid;place-items:center}.page-posts .icon-btn:hover{border-color:var(--fg);background:var(--fg-soft)}
.page-posts .demo-strip{background:var(--fg);color:var(--surface);font:11px/1.6 var(--font-mono);padding:8px 0;text-align:center}
.page-posts .demo-strip b{color:var(--surface);font-weight:700}
.page-posts .article-head{padding-block:clamp(58px,9vw,100px);border-bottom:1px solid var(--border)}
.page-posts .crumb{font:12px/1.5 var(--font-mono);color:var(--accent);margin:0 0 20px}
.page-posts .open-chip{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--accent);color:var(--accent);padding:6px 14px;font:12px/1.5 var(--font-mono);margin-bottom:22px}
.page-posts .article-head h1{font-size:clamp(46px,7vw,88px);line-height:1.06;max-width:12ch;margin:0}
.page-posts .dek{font-size:clamp(19px,2vw,24px);line-height:1.7;color:var(--muted);max-width:58ch;margin:28px 0 0}
.page-posts .article-meta{display:flex;align-items:center;flex-wrap:wrap;gap:14px 24px;margin-top:34px;padding-top:20px;border-top:1px solid var(--border);font:12px/1.5 var(--font-mono);color:var(--muted)}
.page-posts .save-btn{min-height:40px;padding:6px 14px;border:1px solid var(--border);background:transparent;color:var(--fg);font:12px/1.5 var(--font-mono)}.page-posts .save-btn:hover{border-color:var(--fg);background:var(--fg-soft)}.page-posts .save-btn[aria-pressed="true"]{border-color:var(--accent);color:var(--accent)}
.page-posts .article-layout{display:grid;grid-template-columns:minmax(0,740px) 240px;gap:clamp(48px,8vw,100px);align-items:start;padding-block:80px}
.page-posts .article-body{font-size:19px;line-height:2}
.page-posts .article-body h2{font-size:clamp(32px,4vw,48px);line-height:1.2;margin:64px 0 20px}
.page-posts .article-body h3{font-size:27px;margin:42px 0 14px}
.page-posts .article-body p{margin:0 0 26px}
.page-posts .article-body ul{padding-inline-start:24px}.page-posts .article-body li{margin-block:10px}
.page-posts .lede{font-family:var(--font-display);font-size:28px;line-height:1.6}
.page-posts .code{direction:ltr;text-align:left;background:var(--fg);color:var(--surface);padding:26px;overflow:auto;font:14px/1.8 var(--font-mono);margin:34px 0}
.page-posts .note{border-block:1px solid var(--fg);padding:28px 0;margin:42px 0}
.page-posts .note strong{display:block;font-family:var(--font-display);font-size:22px;margin-bottom:8px}
.page-posts .steps{counter-reset:step;list-style:none;padding:0}.page-posts .steps li{counter-increment:step;display:grid;grid-template-columns:56px 1fr;gap:18px;padding:22px 0;border-bottom:1px solid var(--border)}.page-posts .steps li::before{content:"٠" counter(step,arabic-indic);font-family:var(--font-display);font-size:34px;color:var(--accent);line-height:1}
.page-posts .steps strong{display:block;font-size:20px;margin-bottom:6px}.page-posts .steps p{margin:0;color:var(--muted)}
.page-posts .toc{position:sticky;top:100px;border-top:1px solid var(--fg);padding-top:18px}
.page-posts .toc strong{font-family:var(--font-display);font-size:20px}
.page-posts .toc a{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--border);color:var(--muted)}.page-posts .toc a:hover{color:var(--fg)}
.page-posts .progress-card{margin-top:28px;padding:22px;background:var(--fg);color:var(--surface)}
.page-posts .progress-card strong{font-size:18px;display:block;margin-bottom:12px}
.page-posts .progress-track{height:6px;background:color-mix(in oklch,var(--surface) 25%,transparent);margin-bottom:8px}
.page-posts .progress-bar{height:100%;background:var(--accent);width:40%}
.page-posts .progress-card .p-label{font:11px/1.6 var(--font-mono);color:color-mix(in oklch,var(--surface) 72%,transparent)}
.page-posts .files-card{margin-top:18px;border:1px solid var(--fg);padding:22px}
.page-posts .files-card strong{font-family:var(--font-display);font-size:20px;display:block;margin-bottom:12px}
.page-posts .file-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid var(--border)}
.page-posts .file-row:last-of-type{border-bottom:0}
.page-posts .file-row span{font-size:14px}
.page-posts .dl-btn{min-height:38px;padding:4px 12px;border:1px solid var(--border);background:transparent;color:var(--accent);font:12px/1.5 var(--font-mono);font-weight:700}.page-posts .dl-btn:hover{border-color:var(--accent);background:var(--accent-soft)}
.page-posts .comments{padding-block:80px;border-top:1px solid var(--border)}.page-posts .comments-wrap{max-width:740px}.page-posts .comments h2{font-size:clamp(36px,5vw,54px);margin:0}
.page-posts .comment-list{display:grid}.page-posts .comment{padding:24px 0;border-bottom:1px solid var(--border)}.page-posts .comment-head{display:flex;justify-content:space-between;gap:16px}.page-posts .comment p{margin:8px 0 0}
.page-posts .comment-form{display:grid;gap:18px;margin-top:36px}.page-posts .field{display:grid;gap:7px}.page-posts .field label{font-size:13px;color:var(--muted)}
.page-posts .input,.page-posts .textarea{width:100%;border:1px solid var(--border);background:var(--surface);color:var(--fg);padding:13px 14px}.page-posts .textarea{min-height:130px;resize:vertical}
.page-posts .btn{min-height:46px;display:inline-flex;align-items:center;justify-content:center;padding:11px 22px;border:1px solid transparent;font-weight:700}
.page-posts .btn-primary{background:var(--accent);color:var(--surface);border-color:var(--accent)}.page-posts .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black)}
.page-posts .status{font-size:14px;color:var(--muted);min-height:22px}
.page-posts .related{padding-block:80px;border-top:1px solid var(--border)}.page-posts .related-grid{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid var(--fg)}.page-posts .related-card{padding:24px 0;border-bottom:1px solid var(--border)}.page-posts .related-card+.related-card{margin-inline-start:28px}.page-posts .related-card span{font:12px/1.5 var(--font-mono);color:var(--muted)}.page-posts .related-card h3{font-size:24px;margin:12px 0}.page-posts .related-card a{font-weight:700}.page-posts .related-card a:hover{color:var(--accent)}
.page-posts .pagefoot{padding-block:54px;border-top:1px solid var(--border);color:var(--muted)}
.page-posts .footer-grid{display:grid;grid-template-columns:1.4fr repeat(3,1fr);gap:44px}
.page-posts .footer-col strong{display:block;color:var(--fg);margin-bottom:12px}
.page-posts .footer-col a{display:block;min-height:36px}.page-posts .footer-col a:hover{color:var(--fg);text-decoration:underline}
.page-posts .footer-bottom{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;padding-top:28px;margin-top:38px;border-top:1px solid var(--border);font-size:13px}
.page-posts .menu-panel{position:fixed;inset:0;z-index:60;visibility:hidden;pointer-events:none}.page-posts .menu-panel.open{visibility:visible;pointer-events:auto}.page-posts .menu-scrim{position:absolute;inset:0;border:0;background:color-mix(in oklch,var(--fg) 55%,transparent);opacity:0}.page-posts .menu-panel.open .menu-scrim{opacity:1}.page-posts .menu-drawer{position:absolute;inset-block:0;inset-inline-start:0;width:min(520px,92vw);padding:28px clamp(24px,5vw,54px);background:var(--bg);border-inline-end:1px solid var(--fg);transform:translateX(105%);transition:transform .25s ease;display:flex;flex-direction:column}.page-posts .menu-panel.open .menu-drawer{transform:translateX(0)}.page-posts .menu-head{display:flex;justify-content:space-between;align-items:center;padding-bottom:24px;border-bottom:1px solid var(--border)}.page-posts .menu-links{display:flex;flex-direction:column}.page-posts .menu-links a{display:flex;justify-content:space-between;align-items:center;min-height:58px;border-bottom:1px solid var(--border);font-size:18px}
@media(max-width:920px){.page-posts .nav-links{display:none}.page-posts .article-layout{grid-template-columns:1fr}.page-posts .toc{position:static;order:0;margin-top:24px}.page-posts .footer-grid{grid-template-columns:1fr 1fr}.page-posts .related-grid{grid-template-columns:1fr}.page-posts .related-card+.related-card{margin-inline-start:0}.page-posts .article-body{font-size:18px}}
@media(max-width:620px){.page-posts .nav-account .name-label{display:none}.page-posts .article-head h1{font-size:46px}.page-posts .article-layout,.page-posts .comments,.page-posts .related{padding-block:56px}.page-posts .footer-grid{grid-template-columns:1fr;gap:28px}.page-posts .article-meta{align-items:flex-start;flex-direction:column}.page-posts .steps li{grid-template-columns:38px 1fr}}
@media(prefers-reduced-motion:reduce){.page-posts *{scroll-behavior:auto!important;transition-duration:.01ms!important}}
@@ -0,0 +1,7 @@
/* Page-scoped styles: :is(.page-privacy, .page-terms). Shared tokens live in globals.css. */
:is(.page-privacy, .page-terms) *,:is(.page-privacy, .page-terms) *::before,:is(.page-privacy, .page-terms) *::after{box-sizing:border-box}:is(.page-privacy, .page-terms){-webkit-text-size-adjust:100%}:is(.page-privacy, .page-terms){margin:0;background:var(--bg);color:var(--fg);font:16px/1.85 var(--font-body)}:is(.page-privacy, .page-terms) a{color:inherit;text-decoration:none}:is(.page-privacy, .page-terms) h1,:is(.page-privacy, .page-terms) h2{font-family:var(--font-display);text-wrap:balance}:is(.page-privacy, .page-terms) p{text-wrap:pretty}:is(.page-privacy, .page-terms) .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
:is(.page-privacy, .page-terms) .topnav{position:sticky;top:0;z-index:20;background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px);border-bottom:1px solid var(--border)}:is(.page-privacy, .page-terms) .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}:is(.page-privacy, .page-terms) .logo{display:inline-flex;align-items:center;gap:10px;font-size:23px;font-weight:700}:is(.page-privacy, .page-terms) .nav-links{display:flex;align-items:center;gap:28px}:is(.page-privacy, .page-terms) .nav-links a{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}:is(.page-privacy, .page-terms) .nav-links a:hover{color:var(--fg)}
:is(.page-privacy, .page-terms) .hero{padding-block:clamp(58px,9vw,110px);border-bottom:1px solid var(--border)}:is(.page-privacy, .page-terms) .eyebrow{font:12px/1.5 var(--font-mono);color:var(--accent);margin:0 0 18px}:is(.page-privacy, .page-terms) .hero h1{font-size:clamp(44px,7vw,78px);line-height:1.08;margin:0;max-width:12ch}:is(.page-privacy, .page-terms) .lead{font-size:19px;color:var(--muted);max-width:58ch;margin:24px 0 0}:is(.page-privacy, .page-terms) .notice{margin-top:28px;border:1px solid var(--accent);padding:18px 20px;color:var(--fg);max-width:64ch}
:is(.page-privacy, .page-terms) .policy{padding-block:72px 90px}:is(.page-privacy, .page-terms) .policy section{padding-block:30px;border-top:1px solid var(--border)}:is(.page-privacy, .page-terms) .policy h2{font-size:clamp(28px,4vw,40px);margin:0 0 12px}:is(.page-privacy, .page-terms) .policy p,:is(.page-privacy, .page-terms) .policy li{color:var(--muted)}:is(.page-privacy, .page-terms) .policy ul{margin:0;padding-inline-start:24px}:is(.page-privacy, .page-terms) .policy a{color:var(--fg);text-decoration:underline;text-underline-offset:4px}:is(.page-privacy, .page-terms) .pagefoot{padding-block:46px;border-top:1px solid var(--border);color:var(--muted)}:is(.page-privacy, .page-terms) .footer-bottom{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
@media(max-width:920px){:is(.page-privacy, .page-terms) .nav-links{display:flex}}
@media(max-width:620px){:is(.page-privacy, .page-terms) .hero{padding-block:48px}:is(.page-privacy, .page-terms) .policy{padding-block:48px 64px}:is(.page-privacy, .page-terms) .notice{padding:16px}}
+8
View File
@@ -0,0 +1,8 @@
/* Page-scoped styles: .page-signup. Shared tokens live in globals.css. */
.page-signup *,.page-signup *::before,.page-signup *::after{box-sizing:border-box}.page-signup{-webkit-text-size-adjust:100%}.page-signup{margin:0;background:var(--bg);color:var(--fg);font:16px/1.7 var(--font-body);text-rendering:optimizeLegibility}.page-signup a{color:inherit;text-decoration:none}.page-signup button,.page-signup input{font:inherit}.page-signup button{cursor:pointer}.page-signup h1,.page-signup h2{font-family:var(--font-display);text-wrap:balance}.page-signup p{text-wrap:pretty}.page-signup :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}.page-signup .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-signup .topnav{border-bottom:1px solid var(--border)}.page-signup .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}.page-signup .logo{display:inline-flex;align-items:center;gap:10px;font-size:23px;font-weight:700}.page-signup .logo-mark{width:30px;height:34px;object-fit:contain;border-radius:4px}.page-signup .nav-return{min-height:44px;display:inline-flex;align-items:center;color:var(--muted);border-bottom:1px solid transparent}.page-signup .nav-return:hover{color:var(--fg);border-color:var(--fg)}
.page-signup .auth{min-height:calc(100vh - 70px);display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.page-signup .auth>*{min-width:0}.page-signup .auth-story{padding:clamp(54px,8vw,104px) max(var(--gutter),calc((100% - var(--container))/2 + var(--gutter)));padding-inline-end:clamp(48px,8vw,100px);background:var(--fg);color:var(--surface);display:flex;flex-direction:column;justify-content:space-between}.page-signup .eyebrow{font:11px/1.5 var(--font-mono);color:var(--accent);margin:0 0 20px}.page-signup .auth-story .eyebrow{color:var(--surface);opacity:.78}.page-signup .auth-story h1{font-size:clamp(46px,6vw,82px);line-height:1.08;max-width:10ch;margin:0}.page-signup .auth-story>div>p{color:color-mix(in oklch,var(--surface) 78%,transparent);font-size:19px;max-width:40ch}.page-signup .story-list{list-style:none;margin:54px 0 0;padding:0;border-top:1px solid color-mix(in oklch,var(--surface) 32%,transparent)}.page-signup .story-list li{padding:15px 0;border-bottom:1px solid color-mix(in oklch,var(--surface) 22%,transparent);display:flex;justify-content:space-between;gap:20px}.page-signup .story-mark{font:12px/1.5 var(--font-mono);opacity:.74}.page-signup .story-foot{font-size:13px;color:color-mix(in oklch,var(--surface) 72%,transparent)}
.page-signup .auth-form-wrap{padding:clamp(46px,7vw,88px) max(var(--gutter),calc((100% - var(--container))/2 + var(--gutter)));padding-inline-start:clamp(48px,8vw,100px);display:flex;align-items:center}.page-signup .auth-form{width:min(440px,100%)}.page-signup .auth-form h2{font-size:clamp(38px,4vw,54px);line-height:1.15;margin:0}.page-signup .lead{color:var(--muted);font-size:18px;margin:12px 0 28px}.page-signup .field{display:grid;gap:7px;margin-top:16px}.page-signup .field label{font-size:13px;color:var(--muted)}.page-signup .input-wrap{position:relative}.page-signup .input{width:100%;min-height:52px;border:1px solid color-mix(in oklch,var(--border) 72%,var(--fg));background:var(--surface);color:var(--fg);padding:13px 14px;border-radius:0}.page-signup .input:focus{outline:2px solid var(--accent);outline-offset:0;border-color:var(--accent)}.page-signup .password-toggle{position:absolute;inset-inline-end:6px;top:6px;min-width:70px;height:40px;border:0;background:transparent;color:var(--muted)}.page-signup .password-toggle:hover{color:var(--fg);background:var(--fg-soft)}.page-signup .check{display:flex;align-items:flex-start;gap:10px;margin:20px 0}.page-signup .check input{margin-top:6px;flex:none}.page-signup .check a{text-decoration:underline;text-underline-offset:3px}.page-signup .check a:hover{color:var(--accent)}.page-signup .btn{min-height:52px;width:100%;display:inline-flex;align-items:center;justify-content:center;padding:12px 20px;border:1px solid transparent;font-weight:700}.page-signup .btn-primary{background:var(--accent);color:var(--bg);border-color:var(--accent)}.page-signup .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black);color:var(--bg)}.page-signup .signin{text-align:center;color:var(--muted);font-size:14px;margin-top:22px}.page-signup .signin a{color:var(--fg);text-decoration:underline;text-underline-offset:3px}.page-signup .signin a:hover{color:var(--accent)}.page-signup .fine{font-size:12px;color:var(--muted);margin-top:14px}.page-signup .status{min-height:28px;margin-top:12px;color:var(--accent);font-size:13px}
@media(max-width:920px){.page-signup .auth{grid-template-columns:1fr}.page-signup .auth-story{min-height:390px;padding:58px var(--gutter)}.page-signup .auth-form-wrap{padding:58px var(--gutter);justify-content:center}.page-signup .story-foot{margin-top:48px}}
@media(max-width:620px){.page-signup .nav-inner{gap:12px}.page-signup .nav-return{font-size:13px}.page-signup .auth-story{min-height:auto}.page-signup .auth-story h1{font-size:46px}.page-signup .auth-form-wrap{padding-block:46px}.page-signup .story-list{margin-top:38px}}
@media(prefers-reduced-motion:reduce){.page-signup *{transition-duration:.01ms!important}}
@@ -0,0 +1,21 @@
/* Page-scoped styles: .page-subscription. Shared tokens live in globals.css. */
.page-subscription *,.page-subscription *::before,.page-subscription *::after{box-sizing:border-box}.page-subscription{scroll-behavior:smooth;-webkit-text-size-adjust:100%}.page-subscription{margin:0;background:var(--bg);color:var(--fg);font:16px/1.7 var(--font-body);text-rendering:optimizeLegibility}.page-subscription.locked{overflow:hidden}.page-subscription a{color:inherit;text-decoration:none}.page-subscription button{font:inherit;cursor:pointer}.page-subscription h1,.page-subscription h2,.page-subscription h3{font-family:var(--font-display);text-wrap:balance}.page-subscription p{text-wrap:pretty}.page-subscription :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}.page-subscription .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-subscription .topnav{position:sticky;top:0;z-index:20;background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px);border-bottom:1px solid var(--border)}.page-subscription .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}.page-subscription .logo{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-display);font-size:23px;font-weight:700}.page-subscription .logo-mark{width:12px;height:12px;background:var(--fg);transform:rotate(45deg)}.page-subscription .nav-links{display:flex;align-items:center;gap:28px}.page-subscription .nav-links a{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}.page-subscription .nav-links a:hover{color:var(--fg)}.page-subscription .nav-tools{display:flex;align-items:center;gap:8px}.page-subscription .nav-login{min-height:44px;display:inline-flex;align-items:center;padding-inline:16px;border:1px solid var(--border)}.page-subscription .nav-login:hover{border-color:var(--fg);background:var(--fg-soft)}.page-subscription .icon-btn{width:46px;height:46px;display:grid;place-items:center;border:1px solid var(--fg);background:var(--fg);color:var(--surface)}.page-subscription .icon-btn:hover{background:var(--accent);border-color:var(--accent);color:var(--surface)}
.page-subscription .hero{padding-block:clamp(72px,11vw,140px);border-bottom:1px solid var(--border)}.page-subscription .hero-grid{display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(48px,9vw,110px);align-items:end}.page-subscription .eyebrow{font:11px/1.5 var(--font-mono);color:var(--accent);margin:0 0 20px}.page-subscription .hero .eyebrow{display:block;margin-bottom:34px}.page-subscription .hero h1{font-size:clamp(52px,8vw,102px);line-height:1.08;max-width:10ch;margin:0}.page-subscription .lead{font-size:clamp(19px,2vw,24px);line-height:1.7;color:var(--muted);max-width:52ch;margin:26px 0 0}.page-subscription .hero-note{border-top:1px solid var(--fg);padding-top:24px}.page-subscription .hero-note strong{font-family:var(--font-display);font-size:28px;display:block}.page-subscription .hero-note p{color:var(--muted);margin:8px 0 0}
.page-subscription .benefits{padding-block:90px;border-bottom:1px solid var(--border)}.page-subscription .section-head{display:flex;justify-content:space-between;align-items:end;gap:40px;margin-bottom:50px}.page-subscription .section-head h2{font-size:clamp(38px,5vw,60px);line-height:1.15;margin:0;max-width:13ch}.page-subscription .section-head p{color:var(--muted);max-width:46ch}.page-subscription .benefit-grid{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid var(--fg)}.page-subscription .benefit{padding:30px 0;border-bottom:1px solid var(--border)}.page-subscription .benefit+.benefit{margin-inline-start:32px}.page-subscription .benefit-index{font:12px/1.5 var(--font-mono);color:var(--accent)}.page-subscription .benefit h3{font-size:28px;margin:26px 0 10px}.page-subscription .benefit p{color:var(--muted);margin:0}
.page-subscription .guide-samples{padding-block:90px;background:var(--surface);border-bottom:1px solid var(--border)}.page-subscription .guide-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}.page-subscription .guide-card{display:flex;flex-direction:column;min-height:440px;border:1px solid var(--border);background:var(--bg)}.page-subscription .guide-cover{min-height:174px;padding:26px;display:flex;flex-direction:column;justify-content:space-between;background:var(--fg);color:var(--surface)}.page-subscription .guide-cover-light{background:var(--surface);color:var(--fg);border-bottom:1px solid var(--fg)}.page-subscription .guide-cover span:first-child{font:11px/1.5 var(--font-mono);}.page-subscription .guide-cover strong{font-family:var(--font-display);font-size:clamp(28px,3vw,40px);line-height:1.1;max-width:9ch}.page-subscription .guide-body{padding:26px;display:flex;flex:1;flex-direction:column}.page-subscription .guide-meta{display:flex;justify-content:space-between;gap:16px;color:var(--muted);font:12px/1.5 var(--font-mono)}.page-subscription .guide-body h3{font-size:25px;margin:24px 0 10px}.page-subscription .guide-body p{color:var(--muted);margin:0}.page-subscription .guide-output{margin:22px 0 0;padding:18px 0;border-block:1px solid var(--border)}.page-subscription .guide-output strong{display:block;font-size:13px;margin-bottom:6px}.page-subscription .guide-output span{color:var(--muted);font-size:14px}.page-subscription .guide-link{min-height:44px;display:flex;align-items:end;justify-content:space-between;margin-top:auto;padding-top:22px;font-weight:700}.page-subscription .guide-link:hover{color:var(--accent)}
.page-subscription .outcomes{padding-block:90px;border-bottom:1px solid var(--border)}.page-subscription .outcome-grid{display:grid;grid-template-columns:.9fr 1.1fr;gap:clamp(48px,9vw,110px);align-items:start}.page-subscription .outcome-grid h2{font-size:clamp(40px,5vw,64px);line-height:1.12;margin:0}.page-subscription .outcome-list{border-top:1px solid var(--fg)}.page-subscription .outcome-card{display:grid;grid-template-columns:72px 1fr;gap:22px;padding:28px 0;border-bottom:1px solid var(--border)}.page-subscription .outcome-card .benefit-index{padding-top:7px}.page-subscription .outcome-card h3{font-size:27px;margin:0 0 8px}.page-subscription .outcome-card p{color:var(--muted);margin:0}.page-subscription .team-contact{margin-top:34px;padding:30px;border:1px solid var(--fg);display:flex;align-items:center;justify-content:space-between;gap:28px}.page-subscription .team-contact h3{font-size:26px;margin:0 0 6px}.page-subscription .team-contact p{color:var(--muted);margin:0;max-width:48ch}.page-subscription .team-contact .btn{flex:0 0 auto}
.page-subscription .pricing{padding-block:90px;background:var(--fg);color:var(--surface)}.page-subscription .pricing-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(48px,9vw,110px);align-items:start}.page-subscription .pricing h2{font-size:clamp(40px,6vw,70px);line-height:1.08;margin:0}.page-subscription .pricing .lead{color:color-mix(in oklch,var(--surface) 70%,transparent)}.page-subscription .plan{background:var(--surface);color:var(--fg);padding:clamp(30px,5vw,54px)}.page-subscription .plan-head{display:flex;justify-content:space-between;align-items:start;gap:20px;padding-bottom:26px;border-bottom:1px solid var(--border)}.page-subscription .plan h3{font-size:32px;margin:0}.page-subscription .price{font-family:var(--font-display);font-size:58px;line-height:1;text-align:left}.page-subscription .price small{display:block;font:11px/1.5 var(--font-mono);color:var(--muted);margin-top:8px}.page-subscription .trial-label{display:inline-block;font:11px/1.5 var(--font-mono);color:var(--accent);margin-top:10px}.page-subscription .plan-list{list-style:none;margin:24px 0;padding:0}.page-subscription .plan-list li{display:flex;justify-content:space-between;gap:16px;padding:13px 0;border-bottom:1px solid var(--border)}.page-subscription .btn{min-height:50px;display:inline-flex;align-items:center;justify-content:center;padding:12px 20px;border:1px solid transparent;font-weight:700}.page-subscription .btn-primary{background:var(--accent);color:var(--surface);border-color:var(--accent)}.page-subscription .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black);color:var(--surface)}.page-subscription .plan .btn{width:100%}.page-subscription .plan-fine{font-size:12px;color:var(--muted);text-align:center;margin:12px 0 0}
.page-subscription .compare{padding-block:90px;border-bottom:1px solid var(--border)}.page-subscription .compare h2,.page-subscription .faq h2{font-size:clamp(38px,5vw,58px);margin:0 0 40px}.page-subscription .table-wrap{overflow-x:auto}.page-subscription .table{width:100%;border-collapse:collapse;min-width:620px}.page-subscription .table th,.page-subscription .table td{padding:16px 14px;border-bottom:1px solid var(--border);text-align:right}.page-subscription .table th{font:12px/1.5 var(--font-mono);color:var(--muted)}.page-subscription .table td:not(:first-child),.page-subscription .table th:not(:first-child){text-align:center}.page-subscription .member-col{border-inline:1px solid var(--accent)}
.page-subscription .faq{padding-block:90px}.page-subscription .faq-wrap{display:grid;grid-template-columns:.7fr 1.3fr;gap:clamp(46px,8vw,100px)}.page-subscription .faq-list{border-top:1px solid var(--fg)}.page-subscription .faq-item{border-bottom:1px solid var(--border)}.page-subscription .faq-button{width:100%;min-height:68px;border:0;background:transparent;color:var(--fg);display:flex;justify-content:space-between;align-items:center;gap:20px;text-align:right;font-weight:700}.page-subscription .faq-button:hover{color:var(--accent)}.page-subscription .faq-answer{display:none;color:var(--muted);padding:0 0 22px;max-width:64ch}.page-subscription .faq-item.open .faq-answer{display:block}.page-subscription .faq-item.open .faq-button span:last-child{transform:rotate(45deg)}
.page-subscription .pagefoot{padding-block:54px;border-top:1px solid var(--border);color:var(--muted)}.page-subscription .footer-grid{display:grid;grid-template-columns:1.4fr repeat(3,1fr);gap:44px}.page-subscription .footer-col strong{display:block;color:var(--fg);margin-bottom:12px}.page-subscription .footer-col a{display:block;min-height:36px}.page-subscription .footer-col a:hover{color:var(--fg);text-decoration:underline}.page-subscription .footer-bottom{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;padding-top:28px;margin-top:38px;border-top:1px solid var(--border);font-size:13px}
.page-subscription .menu-panel,.page-subscription .dialog{position:fixed;inset:0;z-index:60;visibility:hidden;pointer-events:none}.page-subscription .menu-panel.open,.page-subscription .dialog.open{visibility:visible;pointer-events:auto}.page-subscription .menu-scrim,.page-subscription .dialog-scrim{position:absolute;inset:0;border:0;background:color-mix(in oklch,var(--fg) 58%,transparent);opacity:0}.page-subscription .menu-panel.open .menu-scrim,.page-subscription .dialog.open .dialog-scrim{opacity:1}.page-subscription .menu-drawer{position:absolute;inset-block:0;inset-inline-start:0;width:min(520px,92vw);padding:28px clamp(24px,5vw,54px);background:var(--bg);border-inline-end:1px solid var(--fg);transform:translateX(105%);transition:transform .25s ease;display:flex;flex-direction:column}.page-subscription .menu-panel.open .menu-drawer{transform:translateX(0)}.page-subscription .menu-head{display:flex;justify-content:space-between;align-items:center;padding-bottom:24px;border-bottom:1px solid var(--border)}.page-subscription .menu-links{display:grid;margin-top:34px}.page-subscription .menu-links a{min-height:70px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);font-family:var(--font-display);font-size:clamp(27px,4vw,38px)}.page-subscription .menu-links a:hover{color:var(--accent)}.page-subscription .menu-foot{margin-top:auto;color:var(--muted)}.page-subscription .dialog-panel{position:relative;width:min(560px,calc(100% - 36px));margin:10vh auto 0;background:var(--surface);padding:clamp(30px,5vw,54px);border:1px solid var(--fg)}.page-subscription .dialog-panel h2{font-size:42px;margin:12px 0}.page-subscription .dialog-panel p{color:var(--muted)}.page-subscription .dialog-close{position:absolute;inset-inline-end:18px;top:18px;background:transparent;color:var(--fg);border-color:var(--border)}.page-subscription .dialog-actions{display:flex;gap:10px;margin-top:28px}.page-subscription .dialog-actions .btn{flex:1}.page-subscription .btn-secondary{background:transparent;color:var(--fg);border-color:var(--border)}.page-subscription .btn-secondary:hover{background:var(--fg);color:var(--surface);border-color:var(--fg)}
@media(max-width:920px){.page-subscription .nav-links{display:none}.page-subscription .hero-grid,.page-subscription .pricing-grid,.page-subscription .faq-wrap,.page-subscription .outcome-grid{grid-template-columns:1fr}.page-subscription .benefit-grid,.page-subscription .guide-grid{grid-template-columns:1fr}.page-subscription .benefit+.benefit{margin-inline-start:0}.page-subscription .guide-card{min-height:0}.page-subscription .footer-grid{grid-template-columns:1fr 1fr}.page-subscription .section-head{align-items:start;flex-direction:column}.page-subscription .hero-note{max-width:520px}}
@media(max-width:620px){.page-subscription .nav-login{display:none}.page-subscription .hero h1{font-size:50px}.page-subscription .benefits,.page-subscription .guide-samples,.page-subscription .outcomes,.page-subscription .pricing,.page-subscription .compare,.page-subscription .faq{padding-block:60px}.page-subscription .footer-grid{grid-template-columns:1fr;gap:28px}.page-subscription .plan-head,.page-subscription .team-contact{flex-direction:column;align-items:stretch}.page-subscription .team-contact .btn{width:100%}.page-subscription .outcome-card{grid-template-columns:48px 1fr}.page-subscription .price{text-align:right}.page-subscription .dialog-actions{flex-direction:column}}
@media(prefers-reduced-motion:reduce){.page-subscription *{scroll-behavior:auto!important;transition-duration:.01ms!important}}
/* QA-R01/R07: hero join CTA + price on the first fold. */
.page-subscription .hero-join{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-top:24px}
.page-subscription .hero-join .price{font-size:34px}
.page-subscription .hero-join .price small{display:block;font:11px/1.5 var(--font-mono);color:var(--muted);margin-top:8px}
.page-subscription .cta-pair{display:inline-flex;gap:10px;flex-wrap:wrap;align-items:center}
.page-subscription .trust-line{margin:14px 0 0;color:var(--muted);font-size:14px}
@@ -0,0 +1,114 @@
/* Page-scoped styles: .page-vip-onboarding. Shared tokens live in globals.css. */
.page-vip-onboarding *,.page-vip-onboarding *::before,.page-vip-onboarding *::after{box-sizing:border-box}
.page-vip-onboarding{-webkit-text-size-adjust:100%}
.page-vip-onboarding{margin:0;background:var(--bg);color:var(--fg);font:16px/1.75 var(--font-body);text-rendering:optimizeLegibility}
.page-vip-onboarding.locked{overflow:hidden}
.page-vip-onboarding a{color:inherit;text-decoration:none}
.page-vip-onboarding button,.page-vip-onboarding input{font:inherit}
.page-vip-onboarding button{cursor:pointer}
.page-vip-onboarding h1,.page-vip-onboarding h2,.page-vip-onboarding h3{font-family:var(--font-display);text-wrap:balance}
.page-vip-onboarding p{text-wrap:pretty}
.page-vip-onboarding :focus-visible{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-vip-onboarding .container{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.page-vip-onboarding .topnav{position:sticky;top:0;z-index:20;background:color-mix(in oklch,var(--bg) 92%,transparent);backdrop-filter:blur(14px);border-bottom:1px solid var(--border)}
.page-vip-onboarding .nav-inner{min-height:70px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.page-vip-onboarding .logo{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-display);font-size:23px;font-weight:700}
.page-vip-onboarding .nav-links{display:flex;align-items:center;gap:28px}
.page-vip-onboarding .nav-links a{min-height:44px;display:inline-flex;align-items:center;color:var(--muted)}
.page-vip-onboarding .nav-links a:hover{color:var(--fg)}
.page-vip-onboarding .nav-tools{display:flex;align-items:center;gap:8px}
.page-vip-onboarding .nav-account{min-height:44px;display:inline-flex;align-items:center;padding-inline:16px;border:1px solid var(--border);font-weight:700}
.page-vip-onboarding .nav-account:hover{border-color:var(--fg);background:var(--fg-soft)}
.page-vip-onboarding .icon-btn{width:46px;height:46px;display:grid;place-items:center;border:1px solid var(--border);background:transparent;color:var(--fg)}
.page-vip-onboarding .icon-btn:hover{border-color:var(--fg);background:var(--fg-soft)}
.page-vip-onboarding .member-strip{background:var(--fg);color:var(--surface);font:11px/1.6 var(--font-mono);padding:8px var(--gutter);text-align:center}
.page-vip-onboarding .onboarding-head{padding-block:clamp(48px,7vw,82px) 42px;border-bottom:1px solid var(--border)}
.page-vip-onboarding .eyebrow{font:11px/1.5 var(--font-mono);color:var(--accent);margin:0 0 18px}
.page-vip-onboarding .onboarding-head h1{font-size:clamp(42px,6vw,72px);line-height:1.1;margin:0;max-width:13ch}
.page-vip-onboarding .lead{font-size:clamp(18px,2vw,22px);color:var(--muted);max-width:58ch;margin:18px 0 0}
.page-vip-onboarding .progress-wrap{margin-top:34px;max-width:720px}
.page-vip-onboarding .progress-copy{display:flex;justify-content:space-between;gap:16px;color:var(--muted);font:12px/1.5 var(--font-mono);margin-bottom:10px}
.page-vip-onboarding .progress-track{height:4px;background:var(--border);overflow:hidden}
.page-vip-onboarding .progress-bar{height:100%;width:25%;background:var(--accent);transition:width .22s ease}
.page-vip-onboarding .onboarding-layout{display:grid;grid-template-columns:250px minmax(0,1fr);gap:clamp(42px,7vw,90px);align-items:start;padding-block:50px 90px}
.page-vip-onboarding .step-nav{position:sticky;top:100px;border-top:1px solid var(--fg)}
.page-vip-onboarding .step-nav button{width:100%;min-height:58px;display:grid;grid-template-columns:34px 1fr;align-items:center;gap:12px;padding:0 4px;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--muted);text-align:right}
.page-vip-onboarding .step-nav button[aria-current="step"]{color:var(--fg);font-weight:700}
.page-vip-onboarding .step-nav button:disabled{cursor:default}
.page-vip-onboarding .step-num{font:12px/1 var(--font-mono)}
.page-vip-onboarding .step-nav button[aria-current="step"] .step-num{color:var(--accent)}
.page-vip-onboarding .step-nav button.done{color:var(--fg)}
.page-vip-onboarding .step-panel{display:none;max-width:720px}
.page-vip-onboarding .step-panel.active{display:block}
.page-vip-onboarding .step-panel h2{font-size:clamp(32px,4vw,48px);line-height:1.2;margin:0 0 12px}
.page-vip-onboarding .step-intro{color:var(--muted);max-width:58ch;margin:0 0 32px}
.page-vip-onboarding .choice-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
.page-vip-onboarding .choice{position:relative;display:block}
.page-vip-onboarding .choice input{position:absolute;opacity:0;pointer-events:none}
.page-vip-onboarding .choice-body{min-height:150px;display:flex;flex-direction:column;padding:22px;border:1px solid var(--border);background:color-mix(in oklch,var(--surface) 90%,transparent);transition:border-color .15s,background .15s,transform .15s}
.page-vip-onboarding .choice-body:hover{border-color:var(--fg);transform:translateY(-1px)}
.page-vip-onboarding .choice input:checked + .choice-body{border-color:var(--fg);background:var(--fg-soft)}
.page-vip-onboarding .choice input:focus-visible + .choice-body{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-vip-onboarding .choice-title{font-size:19px;font-weight:700}
.page-vip-onboarding .choice-copy{color:var(--muted);font-size:14px;margin-top:8px}
.page-vip-onboarding .choice-mark{width:18px;height:18px;border:1px solid var(--muted);margin-top:auto;align-self:flex-end;display:grid;place-items:center}
.page-vip-onboarding .choice input:checked + .choice-body .choice-mark{background:var(--fg);border-color:var(--fg)}
.page-vip-onboarding .choice input:checked + .choice-body .choice-mark::after{content:"";width:6px;height:10px;border:solid var(--surface);border-width:0 2px 2px 0;transform:rotate(45deg) translate(-1px,-1px)}
.page-vip-onboarding .notice-card{border-block:1px solid var(--fg);padding-block:8px}
.page-vip-onboarding .notice-row{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 0;border-bottom:1px solid var(--border)}
.page-vip-onboarding .notice-row:last-child{border-bottom:0}
.page-vip-onboarding .notice-row strong{display:block;font-size:18px}
.page-vip-onboarding .notice-row p{color:var(--muted);font-size:14px;margin:4px 0 0;max-width:48ch}
.page-vip-onboarding .switch{position:relative;width:52px;min-height:32px;flex:none}
.page-vip-onboarding .switch input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}
.page-vip-onboarding .track{position:absolute;inset:0;border:1px solid var(--muted);background:var(--surface);pointer-events:none}
.page-vip-onboarding .track::after{content:"";position:absolute;top:4px;inset-inline-start:4px;width:22px;height:22px;background:var(--fg);transition:transform .15s}
.page-vip-onboarding .switch input:checked + .track{background:var(--fg);border-color:var(--fg)}
.page-vip-onboarding .switch input:checked + .track::after{transform:translateX(-20px);background:var(--surface)}
.page-vip-onboarding .switch input:focus-visible + .track{outline:3px solid color-mix(in oklch,var(--accent) 55%,transparent);outline-offset:3px}
.page-vip-onboarding .summary{border-top:1px solid var(--fg)}
.page-vip-onboarding .summary-row{display:grid;grid-template-columns:150px 1fr;gap:20px;padding:18px 0;border-bottom:1px solid var(--border)}
.page-vip-onboarding .summary-row dt{color:var(--muted);font:12px/1.7 var(--font-mono)}
.page-vip-onboarding .summary-row dd{margin:0;font-weight:700}
.page-vip-onboarding .start-card{margin-top:28px;padding:26px;border:1px solid var(--fg);background:var(--surface)}
.page-vip-onboarding .start-card h3{font-size:26px;margin:0 0 8px}
.page-vip-onboarding .start-card p{color:var(--muted);margin:0 0 22px;max-width:52ch}
.page-vip-onboarding .actions{display:flex;align-items:center;gap:12px;margin-top:34px}
.page-vip-onboarding .btn{min-height:50px;display:inline-flex;align-items:center;justify-content:center;padding:12px 22px;border:1px solid transparent;font-weight:700}
.page-vip-onboarding .btn-primary{background:var(--accent);color:var(--surface);border-color:var(--accent)}
.page-vip-onboarding .btn-primary:hover{background:color-mix(in oklch,var(--accent) 86%,black);color:var(--surface)}
.page-vip-onboarding .btn-secondary{background:transparent;color:var(--fg);border-color:var(--border)}
.page-vip-onboarding .btn-secondary:hover{border-color:var(--fg);background:var(--fg-soft)}
.page-vip-onboarding .btn[disabled]{opacity:.45;cursor:not-allowed;transform:none}
.page-vip-onboarding .status{min-height:26px;color:var(--muted);font-size:14px;margin:14px 0 0}
.page-vip-onboarding .pagefoot{padding-block:48px;border-top:1px solid var(--border);color:var(--muted)}
.page-vip-onboarding .footer-bottom{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;font-size:13px}
.page-vip-onboarding .menu-panel{position:fixed;inset:0;z-index:60;visibility:hidden;pointer-events:none}
.page-vip-onboarding .menu-panel.open{visibility:visible;pointer-events:auto}
.page-vip-onboarding .menu-scrim{position:absolute;inset:0;border:0;background:color-mix(in oklch,var(--bg) 72%,transparent)}
.page-vip-onboarding .menu-drawer{position:absolute;inset-block:0;inset-inline-start:0;width:min(520px,92vw);padding:28px clamp(24px,5vw,54px);background:var(--bg);border-inline-end:1px solid var(--fg);transform:translateX(105%);transition:transform .25s ease;display:flex;flex-direction:column}
.page-vip-onboarding .menu-panel.open .menu-drawer{transform:translateX(0)}
.page-vip-onboarding .menu-head{display:flex;justify-content:space-between;align-items:center;padding-bottom:24px;border-bottom:1px solid var(--border)}
.page-vip-onboarding .menu-links{display:flex;flex-direction:column;margin-top:24px}
.page-vip-onboarding .menu-links a{min-height:60px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);font-size:18px}
.page-vip-onboarding .menu-links a:hover{color:var(--fg);background:var(--fg-soft)}
@media(max-width:920px){
.page-vip-onboarding .onboarding-layout{grid-template-columns:1fr;padding-block:34px 70px}
.page-vip-onboarding .step-nav{position:static;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));overflow-x:auto;border-top:0;padding-bottom:4px}
.page-vip-onboarding .step-nav button{min-width:150px;border:1px solid var(--border);padding-inline:14px}
.page-vip-onboarding .choice-grid{grid-template-columns:1fr 1fr}
}
@media(max-width:620px){
.page-vip-onboarding .member-strip{text-align:right}
.page-vip-onboarding .onboarding-head{padding-block:42px 32px}
.page-vip-onboarding .onboarding-head h1{font-size:44px}
.page-vip-onboarding .choice-grid{grid-template-columns:1fr}
.page-vip-onboarding .choice-body{min-height:132px}
.page-vip-onboarding .notice-row{align-items:flex-start}
.page-vip-onboarding .summary-row{grid-template-columns:1fr;gap:4px}
.page-vip-onboarding .actions{align-items:stretch;flex-direction:column-reverse}
.page-vip-onboarding .actions .btn{width:100%}
.page-vip-onboarding .start-card{padding:22px}
.page-vip-onboarding .footer-bottom{flex-direction:column}
}
@media(prefers-reduced-motion:reduce){.page-vip-onboarding *{scroll-behavior:auto!important;transition-duration:.01ms!important}}
@@ -0,0 +1,29 @@
'use client'
import { useState } from 'react'
/* FAQ accordions — port of the subscription page's faq-button toggle. */
export default function Faq({ items }: { items: { q: string; a: string }[] }) {
const [openIdx, setOpenIdx] = useState<number | null>(0)
return (
<div className="faq-list">
{items.map((item, i) => {
const open = openIdx === i
return (
<article className={`faq-item${open ? ' open' : ''}`} key={i}>
<button
className="faq-button"
type="button"
aria-expanded={open}
onClick={() => setOpenIdx(open ? null : i)}
>
<span>{item.q}</span><span aria-hidden="true">{open ? '−' : '+'}</span>
</button>
<div className="faq-answer" hidden={!open}>{item.a}</div>
</article>
)
})}
</div>
)
}
@@ -0,0 +1,134 @@
import { getPayload } from 'payload'
import { headers } from 'next/headers'
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import SubscribeButton from './subscribe-button'
import Faq from './faq'
import configPromise from '@payload-config'
import Link from 'next/link'
import { formatNumber, guidesLabel, minutesLabel, MEMBERSHIP_PRICE } from '@/lib/format'
import type { Viewer } from '@/lib/viewer'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'العضوية' }
/* Member-library samples — real VIP guides, read from the CMS at request time. */
const SAMPLES = [
{ slug: 'agent-loop-stop-condition', cover: 'تشريح وكيل ناجح', variant: '', output: 'قالب حالة الوكيل + قائمة فحص قبل التشغيل' },
{ slug: 'systematic-model-eval', cover: 'قِس قبل أن تبدّل', variant: 'guide-cover-light', output: 'مجموعة صغيرة من اختبارات التقييم قابلة لإعادة التشغيل' },
{ slug: 'prototype-to-production', cover: 'من التجربة إلى الإنتاج', variant: '', output: 'قائمة قرارات ما قبل الإطلاق' },
]
const FAQ_ITEMS = [
{ q: 'هل أستطيع الإلغاء في أي وقت؟', a: 'نعم. يتوقف التجديد التالي، ويستمر وصولك حتى نهاية الفترة المدفوعة.' },
{ q: 'هل المحتوى مناسب للمبتدئ؟', a: 'كل دليل يوضح مستواه. ابدأ بالأدلة المجانية في مسار الأساسيات، ثم انتقل إلى الوكلاء والأتمتة.' },
{ q: 'هل توجد شهادة إكمال؟', a: 'لا حالياً. تركيز أنور بآي على التطبيق والملفات العملية، لا على شهادات الحضور.' },
{ q: 'هل يمكنني استخدام الملفات في عملي؟', a: 'نعم للاستخدام الشخصي والمهني داخل مشاريعك. إعادة بيع الملفات أو نشرها كما هي غير مشمول.' },
]
export default async function SubscriptionPage() {
const payload = await getPayload({ config: configPromise })
const { user } = await payload.auth({ headers: await headers() })
const role = (user?.role as string) || null
const entitled = role === 'vip' || role === 'admin'
const viewer: Viewer = user ? { email: user.email, role: role ?? 'free' } : null
const { docs: published } = await payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
depth: 0,
limit: 200,
select: { title: true, slug: true, dek: true, category: true, readingMinutes: true, access: true },
})
const freeCount = published.filter(doc => doc.access === 'free').length
const memberCount = published.length - freeCount
const samples = SAMPLES.flatMap(sample => {
const post = published.find(doc => doc.slug === sample.slug)
return post ? [{ ...sample, post }] : []
})
return (
<div id="site" className="site-page v2 v2-membership">
<Topnav viewer={viewer} />
<main id="content">
<section className="mem-hero" aria-labelledby="mem-title">
<div className="wrap mem-hero-grid">
<div className="mem-hero-copy">
<h1 id="mem-title">كل الأدلة كاملة، مع ملفات العمل.</h1>
<p className="lede">الأدلة المجانية تعرّفك بالمفاهيم. العضوية تفتح الأدلة المعمّقة التي تأخذك من التجربة إلى نظام يعمل، مع القوالب وقوائم الفحص التي نستخدمها فعلاً.</p>
<ul className="mem-points" role="list">
<li><strong>{guidesLabel(memberCount)} للأعضاء</strong><span>تُضاف أدلة جديدة كل شهر</span></li>
<li><strong>ملفات قابلة للتنزيل</strong><span>قوالب حالة، قوائم فحص، سجلات أخطاء</span></li>
<li><strong>نقاش مع الفريق</strong><span>اسأل في تعليقات أي دليل</span></li>
</ul>
</div>
<article className="plan" aria-labelledby="plan-title">
<h2 id="plan-title">عضوية فردية</h2>
<p className="plan-price"><bdi dir="ltr">{MEMBERSHIP_PRICE}</bdi><span>شهرياً</span></p>
<p className="plan-note">تلغي متى شئت، ويبقى حسابك المجاني ومحفوظاتك.</p>
<div className="plan-cta">
{entitled ? (
<a className="btn btn-primary" href="/account-settings">عضويتك فعّالة، أدر حسابك</a>
) : (
<SubscribeButton loggedIn={Boolean(user)} />
)}
</div>
<p className="fine">الدفع عبر Stripe. لا نحفظ بيانات بطاقتك.</p>
</article>
</div>
</section>
{samples.length > 0 && (
<section className="mem-samples" aria-labelledby="samples-title">
<div className="wrap">
<header className="section-head">
<h2 id="samples-title">من أدلة الأعضاء</h2>
<p>كل دليل يبدأ بمشكلة حقيقية وينتهي بشيء تشغّله. اقرأ المقدمة مجاناً قبل أن تقرر.</p>
</header>
<ul className="sample-grid" role="list">
{samples.map(({ slug, output, post }) => (
<li key={slug}>
<Link href={`/posts/${slug}`} className="sample">
<span className="meta-row"><span>{post.category}</span><span>{minutesLabel(post.readingMinutes)}</span></span>
<span className="sample-title">{post.title}</span>
<span className="sample-dek">{post.dek}</span>
<span className="sample-output"><span>ما تخرج به</span>{output}</span>
</Link>
</li>
))}
</ul>
</div>
</section>
)}
<section className="mem-compare" aria-labelledby="compare-title">
<div className="wrap narrow">
<h2 id="compare-title">الفرق بين الحساب المجاني والعضوية</h2>
<div className="table-wrap">
<table>
<thead><tr><th scope="col"><span className="sr-only">الميزة</span></th><th scope="col">حساب مجاني</th><th scope="col" className="is-member">العضوية</th></tr></thead>
<tbody>
<tr><th scope="row">الأدلة المجانية ({formatNumber(freeCount)})</th><td>كاملة</td><td className="is-member">كاملة</td></tr>
<tr><th scope="row">أدلة الأعضاء ({formatNumber(memberCount)})</th><td>المقدمة فقط</td><td className="is-member">كاملة</td></tr>
<tr><th scope="row">ملفات العمل</th><td>—</td><td className="is-member">تنزيل كامل</td></tr>
<tr><th scope="row">التعليق والنقاش</th><td>على الأدلة المجانية</td><td className="is-member">على كل الأدلة</td></tr>
<tr><th scope="row">حفظ الأدلة للعودة لاحقاً</th><td>—</td><td className="is-member">متاح</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section className="mem-faq" aria-labelledby="faq-title">
<div className="wrap faq-grid">
<div>
<h2 id="faq-title">أسئلة قبل الاشتراك</h2>
<p>لم تجد إجابتك؟ راسلنا على <a className="text-link" href="mailto:hello@anwarpy.ai" dir="ltr">hello@anwarpy.ai</a> ويرد عليك الفريق مباشرة.</p>
</div>
<Faq items={FAQ_ITEMS} />
</div>
</section>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,45 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
export default function SubscribeButton({ loggedIn }: { loggedIn: boolean }) {
const [pending, setPending] = useState(false)
const [comingSoon, setComingSoon] = useState(false)
async function subscribe() {
setPending(true)
const res = await fetch('/api/subscribe', { method: 'POST' })
if (res.ok) {
const data = await res.json()
if (data?.url) {
window.location.href = data.url as string
return
}
}
// Stripe pending mode (no keys configured) or failure
setComingSoon(true)
setPending(false)
}
if (comingSoon) {
return <p className="plan-soon" role="status">الدفع الإلكتروني متاح قريباً. راسلنا على <a href="mailto:hello@anwarpy.ai" dir="ltr">hello@anwarpy.ai</a> لتفعيل عضويتك يدوياً.</p>
}
if (!loggedIn) {
/* QA-R01/R07: anonymous visitors get explicit signup + login CTAs instead
of a silent redirect. Both preserve /subscription as the destination. */
return (
<span className="cta-pair">
<Link className="btn btn-primary" href="/signup?next=%2Fsubscription">أنشئ حساباً واشترك</Link>
<Link className="btn btn-quiet" href="/login?next=%2Fsubscription">لدي حساب، تسجيل الدخول</Link>
</span>
)
}
return (
<button className="btn btn-primary" onClick={subscribe} disabled={pending}>
{pending ? 'جارٍ التحويل…' : 'اشترك الآن'}
</button>
)
}
+35
View File
@@ -0,0 +1,35 @@
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import '../styles/privacy.css'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'شروط الاستخدام' }
export default function TermsPage() {
return (
<div id="site" className="site-page page-terms">
<Topnav />
<main id="content">
<section className="hero" data-od-id="terms-hero">
<div className="container">
<p className="eyebrow">شروط الاستخدام · مسودة ما قبل الإطلاق</p>
<h1 data-od-id="terms-title">شروط استخدام أنور بآي.</h1>
<p className="lead">توضح هذه المسودة القواعد الأساسية لاستخدام مكتبة أنور بآي والاشتراك فيها.</p>
<p className="notice" role="note">هذه مسودة للمراجعة القانونية وليست نصاً قانونياً نهائياً. يجب اعتماد النسخة النهائية قبل نشر الموقع.</p>
</div>
</section>
<div className="container policy" data-od-id="terms-content">
<section data-od-id="terms-service"><h2>ما هي الخدمة؟</h2><p>أنور بآي مكتبة محتوى عربية تقدم أدلة ومسارات تعليمية، بعضها مجاني وبعضها متاح لمشتركي العضوية. يمنحك الاشتراك وصولاً شخصياً إلى المحتوى المدفوع طوال فترة الاشتراك الفعّالة.</p></section>
<section data-od-id="terms-account"><h2>حسابك</h2><p>أنت مسؤول عن دقة بيانات التسجيل والاحتفاظ بكلمة المرور في سرية، وعن أي نشاط يجري عبر حسابك. يمكنك حذف حسابك في أي وقت بمراسلة <a href="mailto:hello@anwarpy.ai">hello@anwarpy.ai</a>، وسنحذف بياناتك وفق سياسة الخصوصية.</p></section>
<section data-od-id="terms-billing"><h2>الاشتراك والدفع</h2><p>العضوية بـ <bdi dir="ltr">$10</bdi> شهرياً وتُدفع عبر مزوّد الدفع (Stripe). يمكنك الإلغاء في أي وقت ويتوقف التجديد التالي فوراً مع بقاء وصولك حتى نهاية الفترة المدفوعة. السعر الحالي سعر تجريبي للمعاينة وقد يتغير قبل الإطلاق مع إشعار مسبق.</p></section>
<section data-od-id="terms-content"><h2>المحتوى والملكية</h2><p>حقوق المحتوى والملفات محفوظة لأصحابها. حصولك على الاشتراك يمنحك حق الاستخدام الشخصي والمهني داخل مشاريعك، ولا يشمل إعادة نشر المحتوى أو بيعه أو توزيعه كما هو.</p></section>
<section data-od-id="terms-conduct"><h2>الاستخدام المقبول</h2><p>لا يجوز استخدام الخدمة لاستخراج البيانات آلياً أو إعادة نشر المحتوى على نطاق واسع أو أي استخدام يضر بالمنصة أو بمستخدميها. قد نوقف الحسابات المخالفة بعد تنبيه صاحبها.</p></section>
<section data-od-id="terms-disclaimer"><h2>إخلاء المسؤولية</h2><p>المحتوى تعليمي ويُقدَّم «كما هو» دون ضمانات لنتائج محددة. أنت مسؤول عن قراراتك ومشاريعك، وينبغي اختبار أي قالب أو ملف قبل استخدامه في بيئة إنتاج.</p></section>
<section data-od-id="terms-changes"><h2>تعديل الشروط</h2><p>قد نحدّث هذه الشروط مع نمو الخدمة. عند أي تغيير جوهري سنبلّغ المستخدمين المسجلين عبر البريد الإلكتروني قبل سريان التعديل، ونحدّث تاريخ السريان أعلى الصفحة.</p></section>
<section data-od-id="terms-law"><h2>التواصل</h2><p>لأي سؤال عن هذه الشروط راسلنا على <a href="mailto:hello@anwarpy.ai">hello@anwarpy.ai</a>. القانون الحاكم واختصاص المحاكم يحدَّدان في النسخة القانونية النهائية بعد المراجعة.</p></section>
</div>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,60 @@
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import { getPayload } from 'payload'
import { headers } from 'next/headers'
import configPromise from '@payload-config'
import Link from 'next/link'
import VerifyActions from './verify-actions'
import '../styles/email-verification.css'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'تأكيد البريد الإلكتروني' }
type Props = { searchParams: Promise<{ invalid?: string }> }
/* Email verification step — design from email-verification.html. The account
was created; the user either clicks the emailed link (real SMTP at deploy)
or proceeds to onboarding via the CTA. */
export default async function VerifyEmailPage({ searchParams }: Props) {
const { invalid } = await searchParams
let email: string | null = null
let verified = false
try {
const payload = await getPayload({ config: configPromise })
const { user } = await payload.auth({ headers: await headers() })
if (user) {
email = user.email
verified = !!user.emailVerified
}
} catch {
/* no db / no session — page still renders */
}
return (
<div id="site" className="site-page page-verify-email">
<Topnav />
<main id="content" className="verification">
<section className="container" data-od-id="email-verification">
<div className="card">
<p className="eyebrow">الخطوة التالية · تأكيد البريد</p>
<h1 data-od-id="verification-title">افتح بريدك لتفعيل الحساب.</h1>
<p className="lead">أرسلنا رابط التأكيد إلى:<strong className="email" id="signupEmail">{email ?? 'بريدك الإلكتروني'}</strong></p>
{invalid === '1' && (
<p className="status" role="alert">رابط التأكيد غير صالح أو استُخدم من قبل. اطلب رسالة جديدة.</p>
)}
{verified && (
<p className="status" role="status">بريدك الإلكتروني مؤكَّد بالفعل ✓ — تابع إلى الإعداد.</p>
)}
<ol className="steps" aria-label="خطوات تأكيد البريد">
<li><span className="step-num">01</span><span>افتح رسالة أنور بآي في صندوق الوارد.</span></li>
<li><span className="step-num">02</span><span>اضغط رابط تأكيد البريد الإلكتروني داخل الرسالة.</span></li>
<li><span className="step-num">03</span><span>عد هنا لإعداد حسابك ومسارك الأول.</span></li>
</ol>
<VerifyActions verified={verified} />
</div>
</section>
</main>
<Footer />
</div>
)
}
@@ -0,0 +1,38 @@
'use client'
import Link from 'next/link'
import { useState } from 'react'
/* Resend button + status — real /api/auth/resend-verification endpoint.
Without SMTP configured the API answers 503 with a clear message. */
export default function VerifyActions({ verified }: { verified: boolean }) {
const [status, setStatus] = useState('')
return (
<>
<div className="actions">
<Link className="btn btn-primary" href="/onboarding" data-od-id="continue-onboarding-cta">متابعة إعداد الحساب</Link>
{!verified && (
<button className="btn btn-secondary" id="resendButton" type="button" data-od-id="resend-email-button"
onClick={async (e) => {
const button = e.currentTarget
button.disabled = true
setStatus('جارٍ الإرسال…')
const res = await fetch('/api/auth/resend-verification', { method: 'POST' })
const data = await res.json().catch(() => null)
if (res.ok || data?.already) {
setStatus('أرسلنا الرسالة من جديد — افحص صندوق الوارد.')
} else {
setStatus(data?.error || 'تعذّر الإرسال، جرّب بعد قليل.')
}
setTimeout(() => { button.disabled = false }, 3000)
}}
>
إعادة إرسال الرسالة
</button>
)}
</div>
<p className="status" id="resendStatus" role="status" aria-live="polite">{status}</p>
</>
)
}
@@ -0,0 +1,197 @@
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
/* 4-step VIP onboarding wizard — port of vip-onboarding.html step logic
(radio choice-grid, next/back, final summary + recommendation).
Selections persist in localStorage (faithful to the simulated original). */
const GOALS = [
{ value: 'بناء مشروع ذكاء اصطناعي', copy: 'حوّل فكرة إلى نموذج يعمل ويمكن اختباره مع مستخدمين.' },
{ value: 'تطوير مساري المهني', copy: 'ابنِ خبرة عملية ومشروعات تشرح قيمتك بوضوح.' },
{ value: 'أتمتة العمل', copy: 'حدد مهمة متكررة وابنِ لها سير عمل يمكن مراقبته.' },
{ value: 'فهم الوكلاء بعمق', copy: 'صمم التخطيط والتنفيذ والتحقق وحدود التوقف بأمان.' },
]
const LEVELS = [
{ value: 'مبتدئ', label: 'أبدأ من الأساسيات', copy: 'استخدمت أدوات الذكاء الاصطناعي، لكنني لم أبنِ مشروعاً بعد.' },
{ value: 'متوسط', label: 'بنيت تجارب بسيطة', copy: 'جرّبت واجهات برمجة التطبيقات (API) أو أتمتة أو تطبيقاً صغيراً.' },
{ value: 'متقدم', label: 'أعمل على أنظمة فعلية', copy: 'أحتاج أنماطاً أعمق للقياس والموثوقية والتشغيل.' },
]
const LABELS = ['الخطوة 1 من 4', 'الخطوة 2 من 4', 'الخطوة 3 من 4', 'الخطوة 4 من 4']
const PERCENTAGES = ['25٪', '50٪', '75٪', '100٪']
const STEP_TITLES = ['هدفك الآن', 'مستواك العملي', 'رسائلك', 'ابدأ مسارك']
type State = {
goal: string
level: string
weekly: boolean
trackUpdates: boolean
replies: boolean
step: number
}
function recommendation(goal: string): [string, string, string] {
if (goal === 'أتمتة العمل') return ['راقب قبل أن تؤتمت', 'ابدأ بتحديد إشارات النجاح والفشل قبل بناء سير العمل، ثم استخدم قائمة المراقبة في أول تجربة.', '/articles?topic=automation']
if (goal === 'بناء مشروع ذكاء اصطناعي') return ['من الفكرة إلى نموذج قابل للاختبار', 'اختر مشكلة واحدة ومستخدمًا واحدًا، ثم حوّل الفرضية إلى نموذج صغير يمكن اختباره هذا الأسبوع.', '/posts/prototype-to-production']
if (goal === 'تطوير مساري المهني') return ['تشريح وكيل ناجح', 'ابنِ نموذجاً صغيراً يوضح التخطيط والتنفيذ والتحقق، ثم وثّق قراراتك كدراسة حالة.', '/posts/agent-loop-stop-condition']
return ['تشريح وكيل ناجح', 'ابدأ بفصل التخطيط عن التنفيذ والتحقق، ثم استخدم قالب حالة الوكيل في مشروع صغير.', '/posts/agent-loop-stop-condition']
}
export default function OnboardingWizard({ viewerId }: { viewerId: number | null }) {
const [state, setState] = useState<State>({ goal: '', level: '', weekly: true, trackUpdates: true, replies: true, step: 0 })
const [goalStatus, setGoalStatus] = useState('')
const [levelStatus, setLevelStatus] = useState('')
const [restored, setRestored] = useState(false)
/* Backend persistence — goal/level/prefs live on the user record. */
useEffect(() => {
if (!viewerId || !state.goal || !state.level) return
fetch(`/api/users/${viewerId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
goal: state.goal,
level: state.level,
prefs: { weekly: state.weekly, trackUpdates: state.trackUpdates, replies: state.replies },
}),
}).catch(() => null)
}, [viewerId, state.goal, state.level, state.weekly, state.trackUpdates, state.replies])
useEffect(() => {
try {
const raw = localStorage.getItem('anwarpi:vip-onboarding')
if (raw) {
const s = JSON.parse(raw) as Partial<State>
setState(prev => ({
...prev,
goal: s.goal || '',
level: s.level && LEVELS.some(l => l.value === s.level) ? s.level : '',
weekly: typeof s.weekly === 'boolean' ? s.weekly : true,
trackUpdates: typeof s.trackUpdates === 'boolean' ? s.trackUpdates : true,
replies: typeof s.replies === 'boolean' ? s.replies : true,
step: Math.max(0, Math.min(3, Number(s.step) || 0)),
}))
}
} catch { /* ignore */ }
setRestored(true)
}, [])
useEffect(() => {
if (!restored) return
try { localStorage.setItem('anwarpi:vip-onboarding', JSON.stringify(state)) } catch { /* ignore */ }
}, [state, restored])
function next() {
if (state.step === 0 && !state.goal) { setGoalStatus('اختر هدفاً واحداً للمتابعة.'); return }
if (state.step === 1 && !state.level) { setLevelStatus('اختر مستوى واحداً للمتابعة.'); return }
setState(s => ({ ...s, step: Math.min(3, s.step + 1) }))
}
const notices = [
state.weekly && 'خلاصة الأحد',
state.trackUpdates && 'تحديثات المسار',
state.replies && 'الردود',
].filter(Boolean) as string[]
const rec = recommendation(state.goal)
return (
<>
<section className="onboarding-head" data-od-id="onboarding-hero">
<div className="container">
<p className="eyebrow">عضو جديد · إعداد سريع</p>
<h1 data-od-id="onboarding-title">مرحباً بك. لنبدأ بما يخدم هدفك.</h1>
<p className="lead">أجب عن سؤالين وحدد رسائلك المفضلة. سنقترح نقطة بداية واضحة بدلاً من تركك أمام مكتبة كبيرة.</p>
<div className="progress-wrap" aria-label="تقدم الإعداد">
<div className="progress-copy"><span id="progressLabel">{LABELS[state.step]}</span><span id="progressPercent">{PERCENTAGES[state.step]}</span></div>
<div className="progress-track"><div className="progress-bar" id="progressBar" style={{ width: `${(state.step + 1) * 25}%` }}></div></div>
</div>
</div>
</section>
<section className="container onboarding-layout" data-od-id="onboarding-flow">
<nav className="step-nav" aria-label="خطوات إعداد العضوية" data-od-id="onboarding-steps">
{STEP_TITLES.map((title, i) => (
<button type="button" key={i} aria-current={state.step === i ? 'step' : undefined} disabled className={i < state.step ? 'done' : ''}>
<span className="step-num">{'01020304'.slice(i * 2, i * 2 + 2)}</span><span>{title}</span>
</button>
))}
</nav>
<form id="onboardingForm" noValidate onSubmit={e => e.preventDefault()}>
<section className={`step-panel${state.step === 0 ? ' active' : ''}`} data-step={0} data-od-id="step-goal">
<h2>ما النتيجة التي تريدها أولاً؟</h2>
<p className="step-intro">اختر هدفاً واحداً الآن. يمكنك تغييره لاحقاً من إعدادات الحساب.</p>
<div className="choice-grid" role="radiogroup" aria-label="هدفك">
{GOALS.map(g => (
<label className="choice" key={g.value}>
<input type="radio" name="goal" value={g.value} required checked={state.goal === g.value} onChange={() => { setState(s => ({ ...s, goal: g.value })); setGoalStatus('') }} />
<span className="choice-body"><span className="choice-title">{g.value}</span><span className="choice-copy">{g.copy}</span><span className="choice-mark" aria-hidden="true"></span></span>
</label>
))}
</div>
<p className="status" id="goalStatus" role="status" aria-live="polite">{goalStatus}</p>
<div className="actions"><button className="btn btn-primary" type="button" onClick={next}>تابع إلى مستواك</button></div>
</section>
<section className={`step-panel${state.step === 1 ? ' active' : ''}`} data-step={1} data-od-id="step-level">
<h2>ما مستوى خبرتك العملية؟</h2>
<p className="step-intro">لا نختبرك. الإجابة تضبط ترتيب المواد وكمية الشرح في اقتراح البداية.</p>
<div className="choice-grid" role="radiogroup" aria-label="مستواك">
{LEVELS.map(l => (
<label className="choice" key={l.value}>
<input type="radio" name="level" value={l.value} required checked={state.level === l.value} onChange={() => { setState(s => ({ ...s, level: l.value })); setLevelStatus('') }} />
<span className="choice-body"><span className="choice-title">{l.label}</span><span className="choice-copy">{l.copy}</span><span className="choice-mark" aria-hidden="true"></span></span>
</label>
))}
</div>
<p className="status" id="levelStatus" role="status" aria-live="polite">{levelStatus}</p>
<div className="actions">
<button className="btn btn-secondary" type="button" onClick={() => setState(s => ({ ...s, step: 0 }))}>رجوع</button>
<button className="btn btn-primary" type="button" onClick={next}>تابع إلى الرسائل</button>
</div>
</section>
<section className={`step-panel${state.step === 2 ? ' active' : ''}`} data-step={2} data-od-id="step-notifications">
<h2>اختر ما يستحق الوصول إلى بريدك.</h2>
<p className="step-intro">كل الخيارات قابلة للتغيير. لا يؤثر إيقافها على وصولك إلى محتوى العضوية.</p>
<div className="notice-card">
<div className="notice-row"><div><strong>خلاصة الأحد</strong><p>أفضل ما نُشر هذا الأسبوع مع نقطة تطبيق واحدة.</p></div>
<label className="switch"><input type="checkbox" name="weekly" checked={state.weekly} onChange={e => setState(s => ({ ...s, weekly: e.target.checked }))} aria-label="تفعيل خلاصة الأحد" /><span className="track"></span></label></div>
<div className="notice-row"><div><strong>تحديثات المسار</strong><p>رسالة عندما نضيف جزءاً جديداً إلى المسار الذي تتبعه.</p></div>
<label className="switch"><input type="checkbox" name="trackUpdates" checked={state.trackUpdates} onChange={e => setState(s => ({ ...s, trackUpdates: e.target.checked }))} aria-label="تفعيل تحديثات المسار" /><span className="track"></span></label></div>
<div className="notice-row"><div><strong>الردود على تعليقاتك</strong><p>تنبيه فقط عندما يرد عضو أو فريق أنور على سؤالك.</p></div>
<label className="switch"><input type="checkbox" name="replies" checked={state.replies} onChange={e => setState(s => ({ ...s, replies: e.target.checked }))} aria-label="تفعيل إشعارات الردود" /><span className="track"></span></label></div>
</div>
<div className="actions">
<button className="btn btn-secondary" type="button" onClick={() => setState(s => ({ ...s, step: 1 }))}>رجوع</button>
<button className="btn btn-primary" type="button" onClick={() => setState(s => ({ ...s, step: 3 }))}>اعرض نقطة البداية</button>
</div>
</section>
<section className={`step-panel${state.step === 3 ? ' active' : ''}`} data-step={3} data-od-id="step-start">
<h2>مسارك جاهز.</h2>
<p className="step-intro">ابدأ بمادة واحدة، طبّق ملفها العملي، ثم عد إلى المكتبة. لا حاجة لفتح كل شيء اليوم.</p>
<dl className="summary">
<div className="summary-row"><dt>هدفك</dt><dd id="summaryGoal">{state.goal || '—'}</dd></div>
<div className="summary-row"><dt>مستواك</dt><dd id="summaryLevel">{state.level || '—'}</dd></div>
<div className="summary-row"><dt>الرسائل</dt><dd id="summaryNotices">{notices.length ? notices.join('، ') : 'لا رسائل بريدية'}</dd></div>
</dl>
<article className="start-card" data-od-id="recommended-first-guide">
<p className="eyebrow">نقطة البداية المقترحة</p>
<h3 id="recommendationTitle">{rec[0]}</h3>
<p id="recommendationCopy">{rec[1]}</p>
<Link className="btn btn-primary" href={rec[2]} id="startGuide" data-od-id="start-first-guide">ابدأ الدليل الأول</Link>
</article>
<div className="actions">
<button className="btn btn-secondary" type="button" onClick={() => setState(s => ({ ...s, step: 2 }))}>تعديل الاختيارات</button>
<Link className="btn btn-secondary" href="/articles">اذهب إلى المكتبة</Link>
</div>
</section>
</form>
</section>
</>
)
}
@@ -0,0 +1,25 @@
import Topnav from '@/components/topnav'
import Footer from '@/components/footer'
import OnboardingWizard from './onboarding-wizard'
import { redirect } from 'next/navigation'
import { getViewer } from '@/lib/viewer'
import '../styles/vip-onboarding.css'
export const dynamic = 'force-dynamic'
export const metadata = { title: 'إعداد العضوية' }
export default async function VipOnboardingPage() {
const viewer = await getViewer()
if (!viewer) redirect('/login?next=%2Fvip-onboarding')
if (viewer.role !== 'vip' && viewer.role !== 'admin') redirect('/subscription')
return (
<div id="site" className="site-page page-vip-onboarding">
<Topnav viewer={viewer} />
<main id="content">
<p className="member-strip" role="status">تم تفعيل عضويتك. خمس دقائق لإعداد مسارك الأول.</p>
<OnboardingWizard viewerId={viewer.id ?? null} />
</main>
<Footer />
</div>
)
}