Use a unified card with stepper, clearer payment options, and consistent padding on both steps.
953 lines
25 KiB
Vue
953 lines
25 KiB
Vue
<template>
|
||
<div class="registration-page">
|
||
<div class="container">
|
||
<header class="page-head">
|
||
<RouterLink to="/" class="back-link">
|
||
<span aria-hidden="true">→</span>
|
||
بازگشت به صفحه اصلی
|
||
</RouterLink>
|
||
<p class="page-eyebrow">{{ isClassRequest ? 'درخواست تشکیل کلاس' : 'ثبتنام در کلاس' }}</p>
|
||
<h1>{{ pageTitle }}</h1>
|
||
<p v-if="cls" class="page-lead">{{ cls.course?.title || cls.name }}</p>
|
||
</header>
|
||
|
||
<div v-if="loading" class="state-card loading-block">
|
||
<div class="spinner" aria-label="در حال بارگذاری"></div>
|
||
</div>
|
||
|
||
<div v-else-if="loadError" class="state-card alert" role="alert">{{ loadError }}</div>
|
||
|
||
<div v-else-if="cls" class="registration-card">
|
||
<aside class="summary-panel">
|
||
<div class="summary-inner">
|
||
<p class="summary-label">خلاصه کلاس</p>
|
||
<h2 class="summary-title">{{ cls.name }}</h2>
|
||
|
||
<dl class="summary-list">
|
||
<div v-if="cls.professor" class="summary-item">
|
||
<dt>استاد</dt>
|
||
<dd>{{ cls.professor.name }} {{ cls.professor.surname || '' }}</dd>
|
||
</div>
|
||
<div v-if="cls.startDate" class="summary-item">
|
||
<dt>شروع</dt>
|
||
<dd>{{ formatJalali(cls.startDate) }}</dd>
|
||
</div>
|
||
<div class="summary-item">
|
||
<dt>شهریه</dt>
|
||
<dd>{{ formatPrice(pricing.tuitionFee) }}</dd>
|
||
</div>
|
||
</dl>
|
||
|
||
<div v-if="step === 2" class="summary-total">
|
||
<span>مبلغ این مرحله</span>
|
||
<strong>{{ formatPrice(activePricing.amountDueNow) }}</strong>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="form-panel">
|
||
<nav class="stepper" aria-label="مراحل ثبتنام">
|
||
<div class="stepper-item" :class="{ active: step === 1, done: step > 1 }">
|
||
<span class="stepper-dot">{{ step > 1 ? '✓' : '۱' }}</span>
|
||
<span class="stepper-label">اطلاعات شما</span>
|
||
</div>
|
||
<span class="stepper-line" aria-hidden="true"></span>
|
||
<div class="stepper-item" :class="{ active: step === 2 }">
|
||
<span class="stepper-dot">۲</span>
|
||
<span class="stepper-label">پرداخت</span>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Step 1 -->
|
||
<div v-if="step === 1" class="step-content">
|
||
<div class="step-header">
|
||
<h2>اطلاعات تماس</h2>
|
||
<p>
|
||
برای ادامه، مشخصات خود را وارد کنید. اگر حساب کاربری ندارید، بهصورت خودکار ساخته
|
||
میشود و نام کاربری و رمز عبور از طریق پیامک برایتان ارسال میگردد.
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="formError" class="alert" role="alert">{{ formError }}</div>
|
||
|
||
<form class="reg-form" novalidate @submit.prevent="handleInfoSubmit">
|
||
<div class="fields-grid">
|
||
<div class="field">
|
||
<label for="reg-name">نام <span class="req">*</span></label>
|
||
<input
|
||
id="reg-name"
|
||
v-model.trim="form.name"
|
||
type="text"
|
||
autocomplete="given-name"
|
||
placeholder="نام"
|
||
/>
|
||
<span v-if="errors.name" class="field-error">{{ errors.name }}</span>
|
||
</div>
|
||
<div class="field">
|
||
<label for="reg-surname">نام خانوادگی <span class="req">*</span></label>
|
||
<input
|
||
id="reg-surname"
|
||
v-model.trim="form.surname"
|
||
type="text"
|
||
autocomplete="family-name"
|
||
placeholder="نام خانوادگی"
|
||
/>
|
||
<span v-if="errors.surname" class="field-error">{{ errors.surname }}</span>
|
||
</div>
|
||
<div class="field">
|
||
<label for="reg-national-id">کد ملی <span class="req">*</span></label>
|
||
<input
|
||
id="reg-national-id"
|
||
v-model.trim="form.nationalIdCode"
|
||
type="text"
|
||
inputmode="numeric"
|
||
maxlength="10"
|
||
autocomplete="off"
|
||
placeholder="۱۰ رقم"
|
||
dir="ltr"
|
||
class="input-ltr"
|
||
/>
|
||
<span v-if="errors.nationalIdCode" class="field-error">{{ errors.nationalIdCode }}</span>
|
||
</div>
|
||
<div class="field">
|
||
<label for="reg-phone">شماره موبایل <span class="req">*</span></label>
|
||
<input
|
||
id="reg-phone"
|
||
v-model.trim="form.phoneNumber"
|
||
type="tel"
|
||
inputmode="tel"
|
||
autocomplete="tel"
|
||
placeholder="۰۹۱۲۱۲۳۴۵۶۷"
|
||
dir="ltr"
|
||
class="input-ltr"
|
||
/>
|
||
<span v-if="errors.phoneNumber" class="field-error">{{ errors.phoneNumber }}</span>
|
||
</div>
|
||
<div class="field field-wide">
|
||
<label for="reg-email">ایمیل <span class="optional">(اختیاری)</span></label>
|
||
<input
|
||
id="reg-email"
|
||
v-model.trim="form.email"
|
||
type="email"
|
||
autocomplete="email"
|
||
placeholder="example@email.com"
|
||
dir="ltr"
|
||
class="input-ltr"
|
||
/>
|
||
<span v-if="errors.email" class="field-error">{{ errors.email }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button class="btn btn-ink btn-block" type="submit" :disabled="infoSubmitting">
|
||
<span v-if="infoSubmitting" class="spinner" aria-hidden="true"></span>
|
||
{{ infoSubmitting ? 'در حال ثبت…' : 'ادامه به پرداخت' }}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Step 2 -->
|
||
<div v-else class="step-content">
|
||
<div class="step-header">
|
||
<h2>{{ isClassRequest ? 'پرداخت بیعانه' : 'انتخاب روش پرداخت' }}</h2>
|
||
<p v-if="!isClassRequest">
|
||
یکی از روشهای زیر را انتخاب کنید. پرداخت یکجا شامل
|
||
{{ formatPrice(FULL_PAYMENT_DISCOUNT) }} تخفیف است.
|
||
</p>
|
||
<p v-else>
|
||
برای ثبت درخواست تشکیل کلاس جدید، بیعانه پرداخت کنید. این مبلغ تا زمان تأیید
|
||
نهایی قطعی نیست.
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="!isClassRequest" class="plan-options" role="radiogroup" aria-label="روش پرداخت">
|
||
<label class="plan-card" :class="{ selected: paymentPlan === 'full' }">
|
||
<input v-model="paymentPlan" type="radio" value="full" />
|
||
<span class="plan-radio" aria-hidden="true"></span>
|
||
<div class="plan-content">
|
||
<div class="plan-top">
|
||
<strong>پرداخت یکجا</strong>
|
||
<span class="plan-badge">پیشنهادی</span>
|
||
</div>
|
||
<p>
|
||
با پرداخت کامل شهریه،
|
||
<span class="highlight">{{ formatPrice(FULL_PAYMENT_DISCOUNT) }}</span>
|
||
تخفیف دریافت میکنید.
|
||
</p>
|
||
<div class="plan-footer">
|
||
<span class="plan-price">{{ formatPrice(fullPricing.totalAmount) }}</span>
|
||
</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="plan-card" :class="{ selected: paymentPlan === 'installments' }">
|
||
<input v-model="paymentPlan" type="radio" value="installments" />
|
||
<span class="plan-radio" aria-hidden="true"></span>
|
||
<div class="plan-content">
|
||
<div class="plan-top">
|
||
<strong>پرداخت دو قسطی</strong>
|
||
</div>
|
||
<p>
|
||
شهریه کامل بدون تخفیف. نیمی از مبلغ اکنون و قسط دوم در
|
||
{{ secondInstallmentDueLabel }} سررسید میشود.
|
||
</p>
|
||
<div class="plan-footer">
|
||
<span class="plan-price">{{ formatPrice(installmentPricing.amountDueNow) }}</span>
|
||
<span class="plan-note">اکنون · باقیمانده {{ formatPrice(installmentPricing.secondInstallmentAmount) }}</span>
|
||
</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div v-else class="terms-box">
|
||
<h3>شرایط درخواست</h3>
|
||
<ul>
|
||
<li>
|
||
بیعانه معادل
|
||
{{ toPersianDigits(Math.round(CLASS_REQUEST_DEPOSIT_RATE * 100)) }}٪ شهریه:
|
||
<strong>{{ formatPrice(requestPricing.amountDueNow) }}</strong>
|
||
</li>
|
||
<li>اگر تا یک ماه آینده کلاس تشکیل نشود، کل بیعانه بدون کسر بازگردانده میشود.</li>
|
||
<li>در صورت انصراف شخصی، بخشی از بیعانه (طبق قوانین آموزشگاه) کسر میشود.</li>
|
||
<li>تا زمان تأیید نهایی توسط آموزشگاه، ثبتنام قطعی نیست.</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="checkout-box">
|
||
<div class="checkout-row checkout-row--total">
|
||
<span>مبلغ قابل پرداخت</span>
|
||
<strong>{{ formatPrice(activePricing.amountDueNow) }}</strong>
|
||
</div>
|
||
<div v-if="!isClassRequest && paymentPlan === 'full'" class="checkout-row">
|
||
<span>تخفیف پرداخت یکجا</span>
|
||
<span class="checkout-discount">− {{ formatPrice(activePricing.paymentDiscount) }}</span>
|
||
</div>
|
||
<div v-if="!isClassRequest && paymentPlan === 'installments'" class="checkout-row">
|
||
<span>قسط دوم · {{ secondInstallmentDueLabel }}</span>
|
||
<span>{{ formatPrice(activePricing.secondInstallmentAmount) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="gateway-banner">
|
||
<span class="gateway-icon" aria-hidden="true">⏳</span>
|
||
<p>درگاه پرداخت بهزودی فعال میشود. پس از پرداخت، درخواست شما برای بررسی ثبت میشود.</p>
|
||
</div>
|
||
|
||
<div class="form-actions form-actions--split">
|
||
<button class="btn btn-ghost" type="button" @click="step = 1">بازگشت</button>
|
||
<button
|
||
class="btn btn-ink"
|
||
type="button"
|
||
disabled
|
||
title="درگاه پرداخت بهزودی فعال میشود"
|
||
>
|
||
پرداخت {{ formatPrice(activePricing.amountDueNow) }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { RouterLink, useRoute } from 'vue-router'
|
||
import { registrationApi } from '@/api/registrationApi'
|
||
import {
|
||
CLASS_REQUEST_DEPOSIT_RATE,
|
||
FULL_PAYMENT_DISCOUNT,
|
||
calculateClassRequestPricing,
|
||
calculateEnrollmentPricing
|
||
} from '@/utils/registrationPricing'
|
||
import { formatJalali, formatPrice, getApiErrorMessage, toPersianDigits } from '@/utils/format'
|
||
|
||
const route = useRoute()
|
||
|
||
const isClassRequest = computed(() => route.meta.registrationType === 'class_request')
|
||
const pageTitle = computed(() =>
|
||
isClassRequest.value ? 'درخواست تشکیل کلاس جدید' : 'ثبتنام در کلاس پیشرو'
|
||
)
|
||
|
||
const cls = ref(null)
|
||
const loading = ref(true)
|
||
const loadError = ref('')
|
||
const step = ref(1)
|
||
const paymentPlan = ref('full')
|
||
const userId = ref(null)
|
||
const infoSubmitting = ref(false)
|
||
const formError = ref('')
|
||
|
||
const form = reactive({
|
||
name: '',
|
||
surname: '',
|
||
nationalIdCode: '',
|
||
phoneNumber: '',
|
||
email: ''
|
||
})
|
||
const errors = reactive({
|
||
name: '',
|
||
surname: '',
|
||
nationalIdCode: '',
|
||
phoneNumber: '',
|
||
email: ''
|
||
})
|
||
|
||
const fullPricing = computed(() =>
|
||
calculateEnrollmentPricing(cls.value || {}, cls.value?.course || {}, 'full')
|
||
)
|
||
const installmentPricing = computed(() =>
|
||
calculateEnrollmentPricing(cls.value || {}, cls.value?.course || {}, 'installments')
|
||
)
|
||
const requestPricing = computed(() => calculateClassRequestPricing(cls.value || {}))
|
||
|
||
const activePricing = computed(() => {
|
||
if (isClassRequest.value) return requestPricing.value
|
||
return paymentPlan.value === 'installments' ? installmentPricing.value : fullPricing.value
|
||
})
|
||
|
||
const pricing = computed(() => activePricing.value)
|
||
|
||
const secondInstallmentDueLabel = computed(() => {
|
||
const due = installmentPricing.value.secondInstallmentDueDate
|
||
return due ? formatJalali(due) : 'میانه دوره'
|
||
})
|
||
|
||
function normalizePhone(value) {
|
||
const phone = String(value || '').replace(/[\s\-()]/g, '').trim()
|
||
if (phone.startsWith('+98')) return `0${phone.slice(3)}`
|
||
if (/^9\d{9}$/.test(phone)) return `0${phone}`
|
||
return phone
|
||
}
|
||
|
||
function validateInfo() {
|
||
errors.name = form.name ? '' : 'نام الزامی است'
|
||
errors.surname = form.surname ? '' : 'نام خانوادگی الزامی است'
|
||
errors.nationalIdCode = form.nationalIdCode ? '' : 'کد ملی الزامی است'
|
||
errors.phoneNumber = form.phoneNumber ? '' : 'شماره موبایل الزامی است'
|
||
|
||
if (form.nationalIdCode && !/^\d{10}$/.test(form.nationalIdCode)) {
|
||
errors.nationalIdCode = 'کد ملی باید ۱۰ رقم باشد'
|
||
}
|
||
const phone = normalizePhone(form.phoneNumber)
|
||
if (form.phoneNumber && !/^09\d{9}$/.test(phone)) {
|
||
errors.phoneNumber = 'شماره موبایل معتبر نیست'
|
||
}
|
||
if (form.email && !/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(form.email)) {
|
||
errors.email = 'ایمیل معتبر نیست'
|
||
}
|
||
|
||
return !Object.values(errors).some(Boolean)
|
||
}
|
||
|
||
async function handleInfoSubmit() {
|
||
formError.value = ''
|
||
if (!validateInfo()) return
|
||
|
||
infoSubmitting.value = true
|
||
try {
|
||
const res = await registrationApi.ensureUser({
|
||
name: form.name,
|
||
surname: form.surname,
|
||
nationalIdCode: form.nationalIdCode,
|
||
phoneNumber: normalizePhone(form.phoneNumber),
|
||
email: form.email || undefined
|
||
})
|
||
userId.value = res.data?.data?.user?._id || res.data?.user?._id
|
||
step.value = 2
|
||
} catch (error) {
|
||
const details = error?.response?.data?.error?.details
|
||
if (details && typeof details === 'object') {
|
||
Object.entries(details).forEach(([key, value]) => {
|
||
if (key in errors) errors[key] = String(value)
|
||
})
|
||
}
|
||
formError.value = getApiErrorMessage(error, 'ثبت اطلاعات ناموفق بود.')
|
||
} finally {
|
||
infoSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
const classId = route.params.classId
|
||
if (!classId) {
|
||
loadError.value = 'کلاس یافت نشد.'
|
||
loading.value = false
|
||
return
|
||
}
|
||
|
||
try {
|
||
const res = await registrationApi.getClass(classId)
|
||
cls.value = res.data?.data || res.data
|
||
} catch (error) {
|
||
loadError.value = getApiErrorMessage(error, 'بارگذاری اطلاعات کلاس ناموفق بود.')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.registration-page {
|
||
min-height: 100vh;
|
||
padding: calc(var(--header-h) + 2rem) 0 3.5rem;
|
||
background:
|
||
radial-gradient(circle at 12% 0%, rgba(45, 106, 79, 0.1), transparent 42%),
|
||
radial-gradient(circle at 88% 8%, rgba(232, 163, 23, 0.1), transparent 38%),
|
||
linear-gradient(180deg, var(--canvas) 0%, var(--canvas-deep) 100%);
|
||
}
|
||
|
||
.page-head {
|
||
max-width: 42rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.back-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.35rem;
|
||
margin-bottom: 1rem;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: var(--ink-soft);
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
.back-link:hover {
|
||
color: var(--leaf);
|
||
}
|
||
|
||
.page-eyebrow {
|
||
font-size: 0.8rem;
|
||
font-weight: 800;
|
||
letter-spacing: 0.02em;
|
||
color: var(--saffron-deep);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.page-head h1 {
|
||
font-family: var(--font-display);
|
||
font-size: clamp(1.75rem, 3.2vw, 2.25rem);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.page-lead {
|
||
font-size: 1.05rem;
|
||
color: var(--ink-soft);
|
||
}
|
||
|
||
.state-card {
|
||
background: var(--chalk);
|
||
border: 1px solid rgba(20, 53, 43, 0.08);
|
||
border-radius: 1.25rem;
|
||
padding: 2.5rem;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.registration-card {
|
||
display: grid;
|
||
grid-template-columns: minmax(240px, 300px) 1fr;
|
||
gap: 0;
|
||
max-width: 960px;
|
||
margin-inline: auto;
|
||
border-radius: 1.5rem;
|
||
overflow: hidden;
|
||
border: 1px solid rgba(20, 53, 43, 0.08);
|
||
box-shadow: var(--shadow);
|
||
background: var(--chalk);
|
||
}
|
||
|
||
.summary-panel {
|
||
background:
|
||
linear-gradient(165deg, rgba(45, 106, 79, 0.08) 0%, rgba(45, 106, 79, 0.02) 100%);
|
||
border-left: 1px solid rgba(20, 53, 43, 0.06);
|
||
}
|
||
|
||
.summary-inner {
|
||
padding: 2rem 1.75rem;
|
||
position: sticky;
|
||
top: calc(var(--header-h) + 1.5rem);
|
||
}
|
||
|
||
.summary-label {
|
||
font-size: 0.75rem;
|
||
font-weight: 800;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
color: var(--leaf);
|
||
margin-bottom: 0.65rem;
|
||
}
|
||
|
||
.summary-title {
|
||
font-size: 1.15rem;
|
||
line-height: 1.45;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.summary-list {
|
||
margin: 0;
|
||
display: grid;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.summary-item {
|
||
display: grid;
|
||
gap: 0.2rem;
|
||
}
|
||
|
||
.summary-item dt {
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
color: var(--ink-soft);
|
||
}
|
||
|
||
.summary-item dd {
|
||
margin: 0;
|
||
font-size: 0.95rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.summary-total {
|
||
margin-top: 1.75rem;
|
||
padding-top: 1.25rem;
|
||
border-top: 1px solid rgba(20, 53, 43, 0.1);
|
||
display: grid;
|
||
gap: 0.35rem;
|
||
}
|
||
|
||
.summary-total span {
|
||
font-size: 0.82rem;
|
||
color: var(--ink-soft);
|
||
}
|
||
|
||
.summary-total strong {
|
||
font-size: 1.2rem;
|
||
color: var(--leaf);
|
||
}
|
||
|
||
.form-panel {
|
||
padding: 2rem 2.25rem 2.25rem;
|
||
}
|
||
|
||
.stepper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
margin-bottom: 2rem;
|
||
padding-bottom: 1.5rem;
|
||
border-bottom: 1px solid rgba(20, 53, 43, 0.08);
|
||
}
|
||
|
||
.stepper-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.55rem;
|
||
color: var(--ink-soft);
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.stepper-item.active {
|
||
color: var(--ink);
|
||
}
|
||
|
||
.stepper-item.done {
|
||
color: var(--leaf);
|
||
}
|
||
|
||
.stepper-dot {
|
||
width: 1.75rem;
|
||
height: 1.75rem;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
font-size: 0.78rem;
|
||
font-weight: 800;
|
||
background: rgba(20, 53, 43, 0.06);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.stepper-item.active .stepper-dot {
|
||
background: var(--leaf);
|
||
color: var(--chalk);
|
||
}
|
||
|
||
.stepper-item.done .stepper-dot {
|
||
background: rgba(45, 106, 79, 0.15);
|
||
color: var(--leaf);
|
||
}
|
||
|
||
.stepper-line {
|
||
flex: 1;
|
||
height: 2px;
|
||
background: rgba(20, 53, 43, 0.1);
|
||
border-radius: 1px;
|
||
}
|
||
|
||
.step-header {
|
||
margin-bottom: 1.75rem;
|
||
}
|
||
|
||
.step-header h2 {
|
||
font-size: 1.25rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.step-header p {
|
||
color: var(--ink-soft);
|
||
font-size: 0.925rem;
|
||
line-height: 1.75;
|
||
max-width: 36rem;
|
||
}
|
||
|
||
.fields-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 1.25rem 1.5rem;
|
||
}
|
||
|
||
.field-wide {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.field label {
|
||
display: block;
|
||
font-size: 0.875rem;
|
||
font-weight: 700;
|
||
margin-bottom: 0.45rem;
|
||
color: var(--ink);
|
||
}
|
||
|
||
.req {
|
||
color: var(--saffron-deep);
|
||
}
|
||
|
||
.optional {
|
||
font-weight: 500;
|
||
color: var(--ink-soft);
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.field input {
|
||
width: 100%;
|
||
border: 1px solid rgba(20, 53, 43, 0.14);
|
||
border-radius: 0.75rem;
|
||
padding: 0.75rem 1rem;
|
||
background: #fff;
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.field input:focus {
|
||
outline: none;
|
||
border-color: var(--leaf);
|
||
box-shadow: 0 0 0 3px rgba(45, 106, 79, 0.12);
|
||
}
|
||
|
||
.input-ltr {
|
||
text-align: left;
|
||
}
|
||
|
||
.field-error {
|
||
display: block;
|
||
margin-top: 0.35rem;
|
||
font-size: 0.8rem;
|
||
color: var(--danger);
|
||
}
|
||
|
||
.form-actions {
|
||
margin-top: 2rem;
|
||
padding-top: 0.25rem;
|
||
}
|
||
|
||
.form-actions--split {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.plan-options {
|
||
display: grid;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.plan-card {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 1rem;
|
||
padding: 1.25rem 1.35rem;
|
||
border: 2px solid rgba(20, 53, 43, 0.1);
|
||
border-radius: 1rem;
|
||
cursor: pointer;
|
||
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.plan-card:hover {
|
||
border-color: rgba(45, 106, 79, 0.35);
|
||
}
|
||
|
||
.plan-card.selected {
|
||
border-color: var(--leaf);
|
||
background: rgba(45, 106, 79, 0.04);
|
||
box-shadow: 0 0 0 1px rgba(45, 106, 79, 0.15);
|
||
}
|
||
|
||
.plan-card input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.plan-radio {
|
||
width: 1.25rem;
|
||
height: 1.25rem;
|
||
border-radius: 50%;
|
||
border: 2px solid rgba(20, 53, 43, 0.25);
|
||
flex-shrink: 0;
|
||
margin-top: 0.15rem;
|
||
position: relative;
|
||
}
|
||
|
||
.plan-card.selected .plan-radio {
|
||
border-color: var(--leaf);
|
||
}
|
||
|
||
.plan-card.selected .plan-radio::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 3px;
|
||
border-radius: 50%;
|
||
background: var(--leaf);
|
||
}
|
||
|
||
.plan-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.plan-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
.plan-top strong {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.plan-badge {
|
||
font-size: 0.7rem;
|
||
font-weight: 800;
|
||
padding: 0.15rem 0.5rem;
|
||
border-radius: 999px;
|
||
background: rgba(232, 163, 23, 0.18);
|
||
color: var(--saffron-deep);
|
||
}
|
||
|
||
.plan-content p {
|
||
margin: 0 0 0.85rem;
|
||
font-size: 0.875rem;
|
||
color: var(--ink-soft);
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.highlight {
|
||
color: var(--saffron-deep);
|
||
font-weight: 800;
|
||
}
|
||
|
||
.plan-footer {
|
||
display: flex;
|
||
align-items: baseline;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem 1rem;
|
||
}
|
||
|
||
.plan-price {
|
||
font-size: 1.05rem;
|
||
font-weight: 800;
|
||
color: var(--ink);
|
||
}
|
||
|
||
.plan-note {
|
||
font-size: 0.8rem;
|
||
color: var(--ink-soft);
|
||
}
|
||
|
||
.terms-box {
|
||
margin-bottom: 1.5rem;
|
||
padding: 1.25rem 1.35rem;
|
||
border-radius: 1rem;
|
||
background: rgba(232, 163, 23, 0.07);
|
||
border: 1px solid rgba(232, 163, 23, 0.2);
|
||
}
|
||
|
||
.terms-box h3 {
|
||
font-size: 0.95rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.terms-box ul {
|
||
margin: 0;
|
||
padding-right: 1.15rem;
|
||
color: var(--ink-soft);
|
||
font-size: 0.875rem;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.checkout-box {
|
||
padding: 1.25rem 1.35rem;
|
||
border-radius: 1rem;
|
||
background: rgba(20, 53, 43, 0.03);
|
||
border: 1px solid rgba(20, 53, 43, 0.08);
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.checkout-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
padding: 0.4rem 0;
|
||
font-size: 0.9rem;
|
||
color: var(--ink-soft);
|
||
}
|
||
|
||
.checkout-row--total {
|
||
padding-bottom: 0.75rem;
|
||
margin-bottom: 0.5rem;
|
||
border-bottom: 1px dashed rgba(20, 53, 43, 0.12);
|
||
font-size: 1rem;
|
||
color: var(--ink);
|
||
}
|
||
|
||
.checkout-row--total strong {
|
||
font-size: 1.15rem;
|
||
color: var(--leaf);
|
||
}
|
||
|
||
.checkout-discount {
|
||
color: var(--saffron-deep);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.gateway-banner {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
padding: 1rem 1.15rem;
|
||
margin-bottom: 1.75rem;
|
||
border-radius: 0.85rem;
|
||
background: rgba(45, 106, 79, 0.06);
|
||
border: 1px solid rgba(45, 106, 79, 0.12);
|
||
}
|
||
|
||
.gateway-icon {
|
||
font-size: 1.1rem;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.gateway-banner p {
|
||
font-size: 0.85rem;
|
||
color: var(--ink-soft);
|
||
line-height: 1.65;
|
||
}
|
||
|
||
.alert {
|
||
margin-bottom: 1.25rem;
|
||
padding: 0.85rem 1.1rem;
|
||
border-radius: 0.75rem;
|
||
background: rgba(180, 35, 24, 0.08);
|
||
color: #7a1a12;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.loading-block {
|
||
display: grid;
|
||
place-items: center;
|
||
min-height: 10rem;
|
||
}
|
||
|
||
@media (max-width: 820px) {
|
||
.registration-card {
|
||
grid-template-columns: 1fr;
|
||
max-width: 520px;
|
||
}
|
||
|
||
.summary-panel {
|
||
border-left: none;
|
||
border-bottom: 1px solid rgba(20, 53, 43, 0.08);
|
||
}
|
||
|
||
.summary-inner {
|
||
position: static;
|
||
padding: 1.5rem 1.75rem 1.25rem;
|
||
}
|
||
|
||
.summary-title {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.summary-list {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.summary-total {
|
||
margin-top: 1.25rem;
|
||
padding-top: 1rem;
|
||
}
|
||
|
||
.form-panel {
|
||
padding: 1.75rem 1.5rem 2rem;
|
||
}
|
||
|
||
.stepper-label {
|
||
font-size: 0.8rem;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 560px) {
|
||
.registration-page {
|
||
padding-top: calc(var(--header-h) + 1.25rem);
|
||
}
|
||
|
||
.page-head {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.fields-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 1.1rem;
|
||
}
|
||
|
||
.summary-list {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.form-actions--split {
|
||
flex-direction: column-reverse;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.form-actions--split .btn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.stepper {
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.stepper-label {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|