gigafibre-fsm/apps/dispatch/src/pages/ContractorPage.vue
louispaulb 7da22ff132 merge: import dispatch-app into apps/dispatch/ (17 commits preserved)
Integrates the Dispatch PWA (Vue/Quasar) into the gigafibre-fsm monorepo.
Full git history accessible via `git log -- apps/dispatch/`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 08:08:51 -04:00

717 lines
26 KiB
Vue

<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { registerContractor } from 'src/api/contractor'
const router = useRouter()
const ALL_SERVICES = [
{ id: 'informatique', icon: '💻', label: 'Informatique' },
{ id: 'formatage', icon: '🖥️', label: 'Formatage PC' },
{ id: 'nettoyage', icon: '🧹', label: 'Nettoyage' },
{ id: 'camera', icon: '📷', label: 'Caméras sécurité' },
{ id: 'plomberie', icon: '🔧', label: 'Plomberie' },
{ id: 'electricite', icon: '⚡', label: 'Électricité' },
{ id: 'climatisation', icon: '❄️', label: 'Climatisation' },
{ id: 'telephone', icon: '📱', label: 'Téléphones' },
{ id: 'serrurerie', icon: '🔒', label: 'Serrurerie' },
{ id: 'peinture', icon: '🎨', label: 'Peinture' },
{ id: 'jardinage', icon: '🌿', label: 'Entretien extérieur' },
{ id: 'autre', icon: '🔨', label: 'Autre' },
]
const DAYS = [
{ id: 'mon', label: 'Lun' },
{ id: 'tue', label: 'Mar' },
{ id: 'wed', label: 'Mer' },
{ id: 'thu', label: 'Jeu' },
{ id: 'fri', label: 'Ven' },
{ id: 'sat', label: 'Sam' },
{ id: 'sun', label: 'Dim' },
]
const TOTAL_STEPS = 4
const step = ref(1)
// ── Step 1 — Profil ──────────────────────────────────────────────────────────
const profile = ref({
firstname: '',
lastname: '',
phone: '',
email: '',
company: '',
license: '',
})
// ── Step 2 — Services ────────────────────────────────────────────────────────
// selectedServices: { [id]: { rate: '', rateType: 'hourly' } }
const selectedServices = ref({})
function toggleService (svc) {
if (selectedServices.value[svc.id]) {
const copy = { ...selectedServices.value }
delete copy[svc.id]
selectedServices.value = copy
} else {
selectedServices.value = {
...selectedServices.value,
[svc.id]: { rate: '', rateType: 'hourly' },
}
}
}
function isSelected (id) { return !!selectedServices.value[id] }
const selectedServiceList = computed(() =>
ALL_SERVICES
.filter(s => selectedServices.value[s.id])
.map(s => ({
...s,
rate: selectedServices.value[s.id].rate,
rateType: selectedServices.value[s.id].rateType,
}))
)
// ── Step 3 — Zone & disponibilité ────────────────────────────────────────────
const availability = ref({
city: '',
radius: '25km',
days: ['mon','tue','wed','thu','fri'],
urgent: false,
})
function toggleDay (id) {
const days = availability.value.days
if (days.includes(id)) {
availability.value.days = days.filter(d => d !== id)
} else {
availability.value.days = [...days, id]
}
}
// ── Submit ───────────────────────────────────────────────────────────────────
const submitting = ref(false)
const submitError = ref('')
const contractorRef = ref('')
async function submit () {
submitting.value = true
submitError.value = ''
try {
const ref = await registerContractor({
profile: profile.value,
services: selectedServiceList.value,
availability: availability.value,
})
contractorRef.value = ref
step.value = 5
} catch (e) {
submitError.value = e.message || 'Erreur lors de la soumission.'
} finally {
submitting.value = false
}
}
// ── Navigation ───────────────────────────────────────────────────────────────
const canNext = computed(() => {
if (step.value === 1) {
const p = profile.value
return p.firstname.trim().length >= 2
&& p.lastname.trim().length >= 2
&& p.phone.replace(/\D/g, '').length >= 10
&& p.email.includes('@')
}
if (step.value === 2) return selectedServiceList.value.length >= 1
&& selectedServiceList.value.every(s => s.rate.trim() !== '')
if (step.value === 3) return availability.value.city.trim().length >= 2
&& availability.value.days.length >= 1
return true
})
function next () { if (canNext.value && step.value < TOTAL_STEPS) step.value++ }
function prev () { if (step.value > 1) step.value-- }
</script>
<template>
<div class="ct-root">
<!-- Header -->
<div class="ct-header">
<button class="btn-back" @click="router.push('/')"> Retour</button>
<div class="ct-brand">Dispatch</div>
<div v-if="step <= TOTAL_STEPS" class="step-dots">
<span
v-for="i in TOTAL_STEPS"
:key="i"
class="dot"
:class="{ active: step === i, done: step > i }"
/>
</div>
</div>
<!-- ── Hero intro (before step 1) ── not shown, header serves this role -->
<!-- ── Body ───────────────────────────────────────────────────────────── -->
<div class="ct-body">
<!-- Step 1 — Profil -->
<div v-if="step === 1" class="step-panel">
<div class="step-eyebrow">Étape 1 sur {{ TOTAL_STEPS }}</div>
<h1 class="step-title">Votre profil</h1>
<p class="step-sub">
Rejoignez notre réseau de techniciens et sous-traitants.<br>
Nous vous contactons sous 24h après révision de votre profil.
</p>
<div class="form-grid">
<div class="field">
<label>Prénom *</label>
<input v-model="profile.firstname" type="text" placeholder="Jean" />
</div>
<div class="field">
<label>Nom *</label>
<input v-model="profile.lastname" type="text" placeholder="Tremblay" />
</div>
<div class="field">
<label>Téléphone *</label>
<input v-model="profile.phone" type="tel" placeholder="514-555-0123" />
</div>
<div class="field">
<label>Courriel *</label>
<input v-model="profile.email" type="email" placeholder="jean@exemple.com" />
</div>
<div class="field span2">
<label>Entreprise (optionnel)</label>
<input v-model="profile.company" type="text" placeholder="Technologies XYZ inc." />
</div>
<div class="field span2">
<label>Numéro RBQ / Licence (optionnel)</label>
<input v-model="profile.license" type="text" placeholder="8301-1234-56" />
<span class="field-hint">Requis pour plomberie, électricité et certains travaux de construction</span>
</div>
</div>
</div>
<!-- Step 2 — Services -->
<div v-if="step === 2" class="step-panel">
<div class="step-eyebrow">Étape 2 sur {{ TOTAL_STEPS }}</div>
<h1 class="step-title">Vos services et tarifs</h1>
<p class="step-sub">Sélectionnez les services que vous offrez et indiquez votre tarif pour chacun</p>
<div class="service-grid">
<button
v-for="s in ALL_SERVICES"
:key="s.id"
class="service-chip"
:class="{ selected: isSelected(s.id) }"
@click="toggleService(s)"
>
<span>{{ s.icon }}</span>
<span class="chip-label">{{ s.label }}</span>
<span v-if="isSelected(s.id)" class="chip-check">✓</span>
</button>
</div>
<!-- Rate inputs for selected services -->
<div v-if="selectedServiceList.length" class="rates-section">
<div class="rates-title">Tarifs pour les services sélectionnés</div>
<div
v-for="s in selectedServiceList"
:key="s.id"
class="rate-row"
>
<div class="rate-svc">
<span class="rate-icon">{{ s.icon }}</span>
<span class="rate-label">{{ s.label }}</span>
</div>
<div class="rate-inputs">
<input
v-model="selectedServices[s.id].rate"
type="number"
min="0"
placeholder="75"
class="rate-amount"
/>
<span class="rate-currency">$</span>
<select v-model="selectedServices[s.id].rateType" class="rate-type">
<option value="hourly">/ heure</option>
<option value="flat">forfait</option>
</select>
</div>
</div>
</div>
<div v-if="!selectedServiceList.length" class="hint-box">
Sélectionnez au moins un service ci-dessus
</div>
</div>
<!-- Step 3 — Zone & disponibilité -->
<div v-if="step === 3" class="step-panel">
<div class="step-eyebrow">Étape 3 sur {{ TOTAL_STEPS }}</div>
<h1 class="step-title">Zone et disponibilité</h1>
<p class="step-sub">Définissez où vous opérez et quand vous êtes disponible</p>
<div class="zone-section">
<div class="field">
<label>Ville principale *</label>
<input v-model="availability.city" type="text" placeholder="Montréal, Laval, Longueuil…" />
</div>
<div class="field">
<label>Rayon d'intervention</label>
<div class="radius-group">
<button
v-for="r in ['10km','25km','50km','Province']"
:key="r"
class="radius-btn"
:class="{ selected: availability.radius === r }"
@click="availability.radius = r"
>{{ r }}</button>
</div>
</div>
<div class="field">
<label>Jours disponibles *</label>
<div class="days-group">
<button
v-for="d in DAYS"
:key="d.id"
class="day-btn"
:class="{ selected: availability.days.includes(d.id) }"
@click="toggleDay(d.id)"
>{{ d.label }}</button>
</div>
</div>
<label class="urgent-row">
<input type="checkbox" v-model="availability.urgent" />
<span>Disponible pour les urgences (interventions rapides)</span>
</label>
</div>
</div>
<!-- Step 4 — Révision -->
<div v-if="step === 4" class="step-panel">
<div class="step-eyebrow">Étape 4 sur {{ TOTAL_STEPS }} — Révision</div>
<h1 class="step-title">Confirmer votre inscription</h1>
<p class="step-sub">Vérifiez vos informations avant de soumettre</p>
<div class="review-card">
<div class="review-section">
<div class="review-section-title">Profil</div>
<div class="review-row"><span>Nom</span><strong>{{ profile.firstname }} {{ profile.lastname }}</strong></div>
<div class="review-row"><span>Téléphone</span><strong>{{ profile.phone }}</strong></div>
<div class="review-row"><span>Courriel</span><strong>{{ profile.email }}</strong></div>
<div v-if="profile.company" class="review-row"><span>Entreprise</span><strong>{{ profile.company }}</strong></div>
<div v-if="profile.license" class="review-row"><span>Licence</span><strong>{{ profile.license }}</strong></div>
</div>
<div class="review-section">
<div class="review-section-title">Services offerts</div>
<div v-for="s in selectedServiceList" :key="s.id" class="review-row">
<span>{{ s.icon }} {{ s.label }}</span>
<strong>{{ s.rate }} $ / {{ s.rateType === 'hourly' ? 'heure' : 'forfait' }}</strong>
</div>
</div>
<div class="review-section">
<div class="review-section-title">Zone et disponibilité</div>
<div class="review-row"><span>Ville</span><strong>{{ availability.city }}</strong></div>
<div class="review-row"><span>Rayon</span><strong>{{ availability.radius }}</strong></div>
<div class="review-row">
<span>Jours</span>
<strong>
{{ DAYS.filter(d => availability.days.includes(d.id)).map(d => d.label).join(', ') }}
</strong>
</div>
<div v-if="availability.urgent" class="review-row">
<span>Urgences</span><strong>Disponible</strong>
</div>
</div>
</div>
<div v-if="submitError" class="submit-error">{{ submitError }}</div>
</div>
<!-- Step 5 — Confirmation -->
<div v-if="step === 5" class="step-panel step-confirm">
<div class="confirm-anim">🎉</div>
<h1 class="step-title">Candidature reçue !</h1>
<p class="step-sub">
Votre profil est en cours de révision.<br>
Un responsable vous contactera sous 24h.
</p>
<div class="confirm-ref">
Référence : <strong>{{ contractorRef }}</strong>
</div>
<div class="next-steps">
<div class="next-step-title">Prochaines étapes</div>
<div class="next-step-item">
<span class="ns-num">1</span>
<span>Vérification de votre profil et de vos certifications</span>
</div>
<div class="next-step-item">
<span class="ns-num">2</span>
<span>Entretien téléphonique avec notre équipe</span>
</div>
<div class="next-step-item">
<span class="ns-num">3</span>
<span>Activation de votre compte et réception de vos premiers jobs</span>
</div>
</div>
<button class="btn-primary-lg" @click="router.push('/')">Retour à l'accueil</button>
</div>
</div><!-- /ct-body -->
<!-- ── Footer nav ──────────────────────────────────────────────────────── -->
<div v-if="step <= TOTAL_STEPS" class="ct-footer">
<button v-if="step > 1" class="btn-prev" @click="prev">← Précédent</button>
<div v-else class="footer-spacer" />
<div class="footer-progress">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: ((step - 1) / TOTAL_STEPS * 100) + '%' }" />
</div>
</div>
<button
v-if="step < TOTAL_STEPS"
class="btn-next"
:disabled="!canNext"
@click="next"
>
Suivant
</button>
<button
v-else
class="btn-submit"
:disabled="!canNext || submitting"
@click="submit"
>
{{ submitting ? 'Envoi…' : 'Soumettre mon profil' }}
</button>
</div>
</div>
</template>
<style scoped>
/* ── Root ── */
.ct-root {
min-height: 100vh;
background: var(--bg, #0f1117);
color: var(--text-primary, #f1f5f9);
display: flex; flex-direction: column;
font-family: 'Inter', sans-serif;
}
/* ── Header ── */
.ct-header {
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; gap: 1rem;
padding: 0.9rem 1.5rem;
background: rgba(15,17,23,0.92);
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
backdrop-filter: blur(12px);
}
.ct-brand {
font-size: 1rem; font-weight: 800;
color: #10b981; flex: 1;
}
.btn-back {
background: none; border: 1px solid var(--border, rgba(255,255,255,0.08));
color: var(--text-secondary, #94a3b8);
border-radius: 6px; padding: 0.3rem 0.75rem;
cursor: pointer; font-size: 0.8rem; font-weight: 600;
transition: color 0.15s, border-color 0.15s;
}
.btn-back:hover { color: var(--text-primary, #f1f5f9); border-color: #10b981; }
.step-dots { display: flex; gap: 6px; align-items: center; }
.dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--border, rgba(255,255,255,0.12));
transition: all 0.25s;
}
.dot.active { background: #10b981; width: 22px; border-radius: 4px; }
.dot.done { background: #10b981; opacity: 0.5; }
/* ── Body ── */
.ct-body {
flex: 1; overflow-y: auto;
padding: 2rem 1.5rem 6rem;
max-width: 680px; margin: 0 auto; width: 100%;
}
.step-panel { animation: fadeUp 0.25s ease; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.step-eyebrow {
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.06em; color: #10b981; margin-bottom: 0.5rem;
}
.step-title { font-size: 1.6rem; font-weight: 800; margin: 0 0 0.4rem; line-height: 1.2; }
.step-sub {
color: var(--text-secondary, #94a3b8);
font-size: 0.92rem; margin: 0 0 1.75rem; line-height: 1.5;
}
/* ── Step 1 — Form grid ── */
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.span2 { grid-column: span 2; }
/* ── Step 2 — Service chips ── */
.service-grid {
display: flex; flex-wrap: wrap; gap: 0.5rem;
margin-bottom: 1.5rem;
}
.service-chip {
display: flex; align-items: center; gap: 0.4rem;
padding: 0.5rem 0.85rem; border-radius: 99px;
background: var(--card-bg, rgba(255,255,255,0.04));
border: 2px solid var(--border, rgba(255,255,255,0.08));
cursor: pointer; font-size: 0.82rem;
transition: all 0.18s; color: var(--text-primary, #f1f5f9);
}
.service-chip:hover { border-color: rgba(16,185,129,0.4); }
.service-chip.selected {
border-color: #10b981;
background: rgba(16,185,129,0.1);
}
.chip-label { font-weight: 600; }
.chip-check { color: #10b981; font-weight: 700; font-size: 0.7rem; }
.rates-section {
background: var(--card-bg, rgba(255,255,255,0.04));
border: 1px solid var(--border, rgba(255,255,255,0.08));
border-radius: 12px; overflow: hidden;
}
.rates-title {
padding: 0.65rem 1rem;
background: var(--sidebar-bg, #161b27);
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--text-secondary, #94a3b8);
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
}
.rate-row {
display: flex; align-items: center; justify-content: space-between;
gap: 1rem; padding: 0.7rem 1rem;
border-bottom: 1px solid var(--border, rgba(255,255,255,0.05));
}
.rate-row:last-child { border-bottom: none; }
.rate-svc { display: flex; align-items: center; gap: 0.5rem; }
.rate-icon { font-size: 1rem; }
.rate-label { font-size: 0.85rem; font-weight: 600; }
.rate-inputs { display: flex; align-items: center; gap: 0.35rem; }
.rate-amount {
width: 72px; background: var(--bg, #0f1117);
border: 1px solid var(--border, rgba(255,255,255,0.08));
border-radius: 6px; color: var(--text-primary, #f1f5f9);
padding: 0.35rem 0.5rem; font-size: 0.85rem; text-align: right;
outline: none; transition: border-color 0.15s;
}
.rate-amount:focus { border-color: #10b981; }
.rate-currency { font-size: 0.82rem; color: var(--text-secondary, #94a3b8); }
.rate-type {
background: var(--bg, #0f1117);
border: 1px solid var(--border, rgba(255,255,255,0.08));
border-radius: 6px; color: var(--text-primary, #f1f5f9);
padding: 0.35rem 0.5rem; font-size: 0.8rem; cursor: pointer;
outline: none;
}
.hint-box {
text-align: center; padding: 2rem;
color: var(--text-secondary, #64748b);
font-size: 0.88rem; font-style: italic;
}
/* ── Step 3 — Zone ── */
.zone-section { display: flex; flex-direction: column; gap: 1.25rem; }
.radius-group { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.radius-btn {
padding: 0.45rem 1rem; border-radius: 8px;
background: var(--card-bg, rgba(255,255,255,0.04));
border: 2px solid var(--border, rgba(255,255,255,0.08));
color: var(--text-primary, #f1f5f9); cursor: pointer;
font-size: 0.82rem; font-weight: 600; transition: all 0.15s;
}
.radius-btn:hover { border-color: rgba(16,185,129,0.4); }
.radius-btn.selected { border-color: #10b981; background: rgba(16,185,129,0.1); }
.days-group { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.day-btn {
width: 44px; height: 44px; border-radius: 8px;
background: var(--card-bg, rgba(255,255,255,0.04));
border: 2px solid var(--border, rgba(255,255,255,0.08));
color: var(--text-primary, #f1f5f9); cursor: pointer;
font-size: 0.8rem; font-weight: 700; transition: all 0.15s;
}
.day-btn:hover { border-color: rgba(16,185,129,0.4); }
.day-btn.selected { border-color: #10b981; background: rgba(16,185,129,0.1); color: #10b981; }
.urgent-row {
display: flex; align-items: center; gap: 0.65rem;
cursor: pointer; font-size: 0.88rem; color: var(--text-secondary, #94a3b8);
}
.urgent-row input { accent-color: #10b981; width: 16px; height: 16px; cursor: pointer; }
/* ── Step 4 — Review ── */
.review-card {
background: var(--card-bg, rgba(255,255,255,0.04));
border: 1px solid var(--border, rgba(255,255,255,0.08));
border-radius: 12px; overflow: hidden;
}
.review-section { border-bottom: 1px solid var(--border, rgba(255,255,255,0.08)); }
.review-section:last-child { border-bottom: none; }
.review-section-title {
padding: 0.65rem 1rem;
background: var(--sidebar-bg, #161b27);
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--text-secondary, #94a3b8);
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
}
.review-row {
display: flex; justify-content: space-between; align-items: center;
padding: 0.6rem 1rem; font-size: 0.82rem;
border-bottom: 1px solid var(--border, rgba(255,255,255,0.05));
}
.review-row:last-child { border-bottom: none; }
.review-row span { color: var(--text-secondary, #94a3b8); }
.review-row strong { color: var(--text-primary, #f1f5f9); }
/* ── Step 5 — Confirm ── */
.step-confirm { text-align: center; padding-top: 2rem; }
.confirm-anim {
font-size: 4rem; margin-bottom: 1rem;
animation: popIn 0.4s cubic-bezier(0.34,1.56,0.64,1);
}
@keyframes popIn {
from { transform: scale(0.4); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.confirm-ref {
display: inline-block; margin: 1.5rem auto;
background: var(--card-bg, rgba(255,255,255,0.04));
border: 1px solid var(--border, rgba(255,255,255,0.08));
border-radius: 8px; padding: 0.65rem 1.25rem;
font-size: 0.88rem; color: var(--text-secondary, #94a3b8);
}
.confirm-ref strong { color: #10b981; font-size: 1rem; }
.next-steps {
text-align: left; margin: 1.5rem 0 2rem;
background: var(--card-bg, rgba(255,255,255,0.04));
border: 1px solid var(--border, rgba(255,255,255,0.08));
border-radius: 12px; overflow: hidden;
}
.next-step-title {
padding: 0.65rem 1rem;
background: var(--sidebar-bg, #161b27);
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--text-secondary, #94a3b8);
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
}
.next-step-item {
display: flex; align-items: flex-start; gap: 0.9rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border, rgba(255,255,255,0.05));
font-size: 0.85rem; color: var(--text-primary, #f1f5f9);
}
.next-step-item:last-child { border-bottom: none; }
.ns-num {
flex-shrink: 0; width: 22px; height: 22px;
background: #10b981; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 0.7rem; font-weight: 700; color: white;
}
.submit-error {
margin-top: 0.75rem; padding: 0.65rem 0.9rem;
background: rgba(244,63,94,0.08);
border: 1px solid rgba(244,63,94,0.25);
border-radius: 8px; font-size: 0.82rem; color: #f43f5e;
}
/* ── Footer ── */
.ct-footer {
position: fixed; bottom: 0; left: 0; right: 0;
display: flex; align-items: center; gap: 1rem;
padding: 0.9rem 1.5rem;
background: rgba(15,17,23,0.96);
border-top: 1px solid var(--border, rgba(255,255,255,0.08));
backdrop-filter: blur(12px);
}
.footer-spacer { flex: 0 0 80px; }
.footer-progress { flex: 1; }
.progress-bar {
height: 3px; background: var(--border, rgba(255,255,255,0.08));
border-radius: 2px; overflow: hidden;
}
.progress-fill {
height: 100%; background: #10b981;
border-radius: 2px; transition: width 0.35s ease;
}
.btn-prev {
flex: 0 0 auto; background: none;
border: 1px solid var(--border, rgba(255,255,255,0.08));
color: var(--text-secondary, #94a3b8);
border-radius: 8px; padding: 0.55rem 1rem;
cursor: pointer; font-size: 0.82rem; font-weight: 600;
transition: all 0.15s;
}
.btn-prev:hover { color: var(--text-primary, #f1f5f9); border-color: rgba(255,255,255,0.2); }
.btn-next {
flex: 0 0 auto;
background: #10b981; border: none; color: white;
border-radius: 8px; padding: 0.55rem 1.25rem;
cursor: pointer; font-size: 0.88rem; font-weight: 700;
transition: opacity 0.15s;
}
.btn-next:hover:not(:disabled) { opacity: 0.85; }
.btn-next:disabled { opacity: 0.35; cursor: not-allowed; }
.btn-submit {
flex: 0 0 auto;
background: #10b981; border: none; color: white;
border-radius: 8px; padding: 0.6rem 1.5rem;
cursor: pointer; font-size: 0.88rem; font-weight: 700;
transition: opacity 0.15s;
}
.btn-submit:hover:not(:disabled) { opacity: 0.85; }
.btn-submit:disabled { opacity: 0.35; cursor: not-allowed; }
.btn-primary-lg {
background: #10b981; border: none; color: white;
border-radius: 10px; padding: 0.75rem 2rem;
cursor: pointer; font-size: 0.95rem; font-weight: 700;
transition: opacity 0.15s;
}
.btn-primary-lg:hover { opacity: 0.85; }
/* ── Shared field styles ── */
.field { display: flex; flex-direction: column; gap: 0.3rem; }
.field label {
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.04em; color: var(--text-secondary, #94a3b8);
}
.field input, .field select, .field textarea {
background: var(--bg, #0f1117);
border: 1px solid var(--border, rgba(255,255,255,0.08));
border-radius: 8px; color: var(--text-primary, #f1f5f9);
padding: 0.6rem 0.85rem; font-size: 0.88rem;
font-family: inherit; outline: none;
transition: border-color 0.15s;
}
.field input:focus, .field select:focus { border-color: #10b981; }
.field-hint { font-size: 0.7rem; color: var(--text-secondary, #64748b); }
@media (max-width: 480px) {
.form-grid { grid-template-columns: 1fr; }
.span2 { grid-column: span 1; }
.step-title { font-size: 1.3rem; }
}
</style>