ghostguild-org/app/pages/join.vue

1029 lines
28 KiB
Vue

<template>
<div>
<!-- HERO -->
<div class="hero">
<h1>Join Ghost Guild</h1>
<p>Resources, events, and a community of people figuring it out. Everyone gets everything. Pay what you can.</p>
</div>
<!-- Already a member -->
<template v-if="isAuthenticated">
<div class="full-section">
<h2>You're already a member</h2>
<p class="section-intro">
Welcome back, {{ memberData?.name || 'member' }}. You're part of Ghost Guild in the
<span class="capitalize">{{ memberData?.circle || 'community' }}</span> circle.
</p>
<div class="member-info-grid">
<DashedBox :hoverable="false">
<div class="section-label">Circle</div>
<div class="info-value capitalize">{{ memberData?.circle || 'Community' }}</div>
</DashedBox>
<DashedBox :hoverable="false">
<div class="section-label">Contribution</div>
<div class="info-value">${{ memberData?.contributionTier || '0' }} CAD/month</div>
</DashedBox>
</div>
<div class="button-row">
<NuxtLink to="/member/dashboard" class="form-submit">Go to Dashboard</NuxtLink>
<NuxtLink to="/member/profile" class="btn">Edit Profile</NuxtLink>
</div>
</div>
<ParchmentInset>
<h2>Want to change your circle or contribution?</h2>
<p>You can update your circle and adjust your monthly contribution at any time from your profile settings.</p>
<NuxtLink to="/member/profile" class="parchment-link">Update Membership Settings</NuxtLink>
</ParchmentInset>
</template>
<!-- Not authenticated: show full join page -->
<template v-else>
<!-- HOW MEMBERSHIP WORKS -->
<ParchmentInset>
<h2>How membership works</h2>
<ul>
<li>Full access to the knowledge commons, events, Slack community, and peer support</li>
<li>One member, one vote in all decisions</li>
<li>Your circle is where you are in your journey, not rank</li>
<li>Your contribution is what you can afford ($0--50+/month, separate from your circle)</li>
<li>Higher contributions create solidarity spots for those who need them</li>
</ul>
</ParchmentInset>
<!-- THREE CIRCLES -->
<div class="content-row">
<div class="content-block">
<div class="section-label" style="color: var(--c-community);">Community</div>
<h2>Exploring</h2>
<p>For game workers curious about cooperatives and people exploring alternative work models. You might be a solo developer, a student, a researcher, or just someone who heard about this and wants to know more. Start here.</p>
<p class="circle-not-sure">Not sure where you fit? Start with Community. You can always move later.</p>
</div>
<div class="content-block">
<div class="section-label" style="color: var(--c-founder);">Founder</div>
<h2>Building</h2>
<p>For people actively building cooperative studios. You have a team, or you are forming one. You are working through governance, legal structure, revenue sharing, and all the hard parts. You want structured support and peers doing the same thing.</p>
</div>
<div class="content-block">
<div class="section-label" style="color: var(--c-practitioner);">Practitioner</div>
<h2>Practicing</h2>
<p>For those already running cooperative studios or with deep experience in cooperative business. You are here to teach, advise, mentor, and help shape the program itself. Peer Accelerator alumni land here.</p>
</div>
</div>
<!-- CONTRIBUTION TIERS -->
<div class="full-section">
<div class="section-label" style="margin-bottom: 12px;">Monthly Contribution</div>
<h2>Pay what you can</h2>
<div class="tier-row">
<div class="tier-card">
<div class="tier-amount">$0</div>
<div class="tier-freq">/month</div>
<div class="tier-desc">Access is a right</div>
</div>
<div class="tier-card">
<div class="tier-amount">$5</div>
<div class="tier-freq">/month</div>
<div class="tier-desc">A small gesture</div>
</div>
<div class="tier-card suggested">
<div class="tier-amount">$15</div>
<div class="tier-freq">/month</div>
<div class="tier-desc">Sustaining</div>
<div class="tier-badge">suggested</div>
</div>
<div class="tier-card">
<div class="tier-amount">$30</div>
<div class="tier-freq">/month</div>
<div class="tier-desc">Supporting</div>
</div>
<div class="tier-card">
<div class="tier-amount">$50</div>
<div class="tier-freq">/month</div>
<div class="tier-desc">Solidarity</div>
</div>
</div>
<p class="solidarity-note">Every dollar above $0 goes to the Solidarity Fund, which covers membership for people who need it. Higher tiers directly sponsor other members. Your contribution is never a gate -- it is a gift.</p>
</div>
<!-- SIGN UP FORM -->
<div v-if="currentStep === 1" class="form-section">
<h2>Become a member</h2>
<p class="form-intro">We will send you a magic link to confirm your email. No passwords, no fuss.</p>
<!-- Error Message -->
<div v-if="errorMessage" class="error-box">
{{ errorMessage }}
</div>
<form @submit.prevent="handleSubmit">
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="join-name">Full Name</label>
<input
id="join-name"
v-model="form.name"
class="form-input"
type="text"
placeholder="Your name"
required
/>
</div>
<div class="form-group">
<label class="form-label" for="join-email">Email Address</label>
<input
id="join-email"
v-model="form.email"
class="form-input"
type="email"
placeholder="you@example.com"
required
/>
</div>
<div class="form-group full-width">
<label class="form-label">Circle</label>
<div class="circle-radios">
<div class="circle-radio community">
<input
id="circle-community"
v-model="form.circle"
type="radio"
name="circle"
value="community"
/>
<label for="circle-community">
<span class="circle-label-name" style="color: var(--c-community);">Community</span>
<span class="circle-label-desc">Exploring</span>
</label>
</div>
<div class="circle-radio founder">
<input
id="circle-founder"
v-model="form.circle"
type="radio"
name="circle"
value="founder"
/>
<label for="circle-founder">
<span class="circle-label-name" style="color: var(--c-founder);">Founder</span>
<span class="circle-label-desc">Building</span>
</label>
</div>
<div class="circle-radio practitioner">
<input
id="circle-practitioner"
v-model="form.circle"
type="radio"
name="circle"
value="practitioner"
/>
<label for="circle-practitioner">
<span class="circle-label-name" style="color: var(--c-practitioner);">Practitioner</span>
<span class="circle-label-desc">Practicing</span>
</label>
</div>
</div>
</div>
<div class="form-group full-width">
<label class="form-label" for="join-contribution">Monthly Contribution</label>
<select
id="join-contribution"
v-model="form.contributionTier"
class="form-select"
>
<option value="0">$0/mo -- Access is a right</option>
<option value="5">$5/mo -- A small gesture</option>
<option value="15">$15/mo -- Sustaining (suggested)</option>
<option value="30">$30/mo -- Supporting</option>
<option value="50">$50/mo -- Solidarity</option>
</select>
</div>
<div class="form-group">
<button
class="form-submit"
type="submit"
:disabled="!isFormValid || isSubmitting"
>
<span v-if="isSubmitting">Processing...</span>
<span v-else-if="needsPayment">Continue to Payment</span>
<span v-else>Become a Member</span>
</button>
</div>
</div>
<p class="form-note">By joining you agree to our <NuxtLink to="/guidelines">community guidelines</NuxtLink>. You can change your circle or contribution at any time from your dashboard. Payment is handled securely through <a href="https://www.helcim.com" target="_blank" rel="noopener">Helcim</a>.</p>
</form>
</div>
<!-- Step 2: Payment -->
<div v-if="currentStep === 2" class="form-section">
<h2>Payment Information</h2>
<p class="form-intro">
You're signing up for the {{ selectedTier.label }} plan --
${{ selectedTier.amount }} CAD / month
</p>
<!-- Error Message -->
<div v-if="errorMessage" class="error-box">
{{ errorMessage }}
</div>
<DashedBox :hoverable="false">
<p class="payment-instruction">Click "Complete Payment" below to open the secure payment modal and verify your payment method.</p>
</DashedBox>
<div class="button-row" style="margin-top: 24px;">
<button
class="btn"
:disabled="isSubmitting"
@click="goBack"
>
Back
</button>
<button
class="form-submit"
:disabled="isSubmitting"
@click="processPayment"
>
<span v-if="isSubmitting">Processing...</span>
<span v-else>Complete Payment</span>
</button>
</div>
</div>
<!-- Step 3: Confirmation -->
<div v-if="currentStep === 3" class="form-section">
<h2>Welcome to Ghost Guild!</h2>
<div v-if="successMessage" class="success-box">
{{ successMessage }}
</div>
<DashedBox :hoverable="false">
<div class="section-label" style="margin-bottom: 12px;">Membership Details</div>
<dl class="details-list">
<div class="details-row">
<dt>Name</dt>
<dd>{{ form.name }}</dd>
</div>
<div class="details-row">
<dt>Email</dt>
<dd>{{ form.email }}</dd>
</div>
<div class="details-row">
<dt>Circle</dt>
<dd class="capitalize">{{ form.circle }}</dd>
</div>
<div class="details-row">
<dt>Contribution</dt>
<dd>{{ selectedTier.label }}</dd>
</div>
<div v-if="customerCode" class="details-row">
<dt>Member ID</dt>
<dd>{{ customerCode }}</dd>
</div>
</dl>
</DashedBox>
<p class="form-note" style="margin-top: 20px;">
We've sent a confirmation email to {{ form.email }} with your membership details.
</p>
<DashedBox :hoverable="false" style="margin-top: 16px;">
<p class="redirect-note">You will be automatically redirected to your dashboard in a few seconds...</p>
</DashedBox>
<div class="button-row" style="margin-top: 24px;">
<NuxtLink to="/member/dashboard" class="form-submit">Go to Dashboard Now</NuxtLink>
<button class="btn" @click="resetForm">Register Another Member</button>
</div>
</div>
</template>
</div>
</template>
<script setup>
import { reactive, ref, computed, onMounted, onUnmounted } from "vue";
import { getCircleOptions } from "~/config/circles";
import {
getContributionOptions,
requiresPayment,
getContributionTierByValue,
} from "~/config/contributions";
// Auth state
const { isAuthenticated, memberData, checkMemberStatus } = useAuth();
// Check authentication status on mount
onMounted(async () => {
await checkMemberStatus();
});
// Form state
const form = reactive({
email: "",
name: "",
circle: "community",
contributionTier: "15",
billingAddress: {
street: "",
city: "",
province: "",
postalCode: "",
country: "CA",
},
});
// UI state
const isSubmitting = ref(false);
const currentStep = ref(1); // 1: Info, 2: Billing (paid only), 3: Payment, 4: Confirmation
const errorMessage = ref("");
const successMessage = ref("");
// Helcim state
const customerId = ref(null);
const customerCode = ref(null);
const subscriptionData = ref(null);
const paymentToken = ref(null);
// Circle options from central config
const circleOptions = getCircleOptions();
// Contribution options from central config
const contributionOptions = getContributionOptions();
// Initialize composables
const {
initializeHelcimPay,
verifyPayment,
cleanup: cleanupHelcimPay,
} = useHelcimPay();
// Form validation
const isFormValid = computed(() => {
return form.name && form.email && form.circle && form.contributionTier;
});
// Check if payment is required
const needsPayment = computed(() => {
return requiresPayment(form.contributionTier);
});
// Get selected tier info
const selectedTier = computed(() => {
return getContributionTierByValue(form.contributionTier);
});
// Step 1: Create customer
const handleSubmit = async () => {
if (isSubmitting.value || !isFormValid.value) return;
isSubmitting.value = true;
errorMessage.value = "";
try {
// Create customer in Helcim
const response = await $fetch("/api/helcim/customer", {
method: "POST",
body: {
name: form.name,
email: form.email,
circle: form.circle,
contributionTier: form.contributionTier,
billingAddress: form.billingAddress,
},
});
if (response.success) {
customerId.value = response.customerId;
customerCode.value = response.customerCode;
// Token is now set as httpOnly cookie by the server
// No need to manually set cookie on client side
// Move to next step
if (needsPayment.value) {
currentStep.value = 2;
// Initialize HelcimPay.js session for card verification
await initializeHelcimPay(customerId.value, customerCode.value, 0);
} else {
// For free tier, create subscription directly
await createSubscription();
// Check member status to ensure user is properly authenticated
await checkMemberStatus();
// Automatically redirect to welcome page after a short delay
setTimeout(() => {
navigateTo("/welcome");
}, 3000); // 3 second delay to show success message
}
}
} catch (error) {
console.error("Error creating customer:", error);
errorMessage.value =
error.data?.message || "Failed to create account. Please try again.";
} finally {
isSubmitting.value = false;
}
};
// Step 2: Process payment
const processPayment = async () => {
if (isSubmitting.value) return;
isSubmitting.value = true;
errorMessage.value = "";
try {
// Verify payment through HelcimPay.js
const paymentResult = await verifyPayment();
if (paymentResult.success) {
paymentToken.value = paymentResult.cardToken;
// Verify payment on server
const verifyResult = await $fetch("/api/helcim/verify-payment", {
method: "POST",
body: {
cardToken: paymentResult.cardToken,
customerId: customerId.value,
},
});
// Create subscription (don't let subscription errors prevent form progression)
const subscriptionResult = await createSubscription(
paymentResult.cardToken,
);
if (!subscriptionResult || !subscriptionResult.success) {
console.warn(
"Subscription creation failed but payment succeeded:",
subscriptionResult?.error,
);
// Still progress to success page since payment worked
currentStep.value = 3;
successMessage.value =
"Payment successful! Subscription setup may need manual completion.";
}
}
} catch (error) {
console.error("Payment process error:", error);
errorMessage.value =
error.message || "Payment verification failed. Please try again.";
} finally {
isSubmitting.value = false;
}
};
// Create subscription
const createSubscription = async (cardToken = null) => {
try {
const response = await $fetch("/api/helcim/subscription", {
method: "POST",
body: {
customerId: customerId.value,
customerCode: customerCode.value,
contributionTier: form.contributionTier,
cardToken: cardToken,
},
});
if (response.success) {
subscriptionData.value = response.subscription;
currentStep.value = 3;
successMessage.value = "Your membership has been activated successfully!";
// Check member status to ensure user is properly authenticated
await checkMemberStatus();
// Automatically redirect to welcome page after a short delay
setTimeout(() => {
navigateTo("/welcome");
}, 3000); // 3 second delay to show success message
} else {
throw new Error("Subscription creation failed - response not successful");
}
} catch (error) {
console.error("Subscription creation error:", error);
console.error("Error details:", {
message: error.message,
statusCode: error.statusCode,
statusMessage: error.statusMessage,
data: error.data,
});
console.error(
"Subscription creation completely failed, but payment was successful",
);
// Don't throw error - let the calling function handle progression
return {
success: false,
error:
error.data?.message || error.message || "Failed to create subscription",
};
}
};
// Go back to previous step
const goBack = () => {
if (currentStep.value > 1) {
currentStep.value--;
errorMessage.value = "";
}
};
// Reset form
const resetForm = () => {
currentStep.value = 1;
customerId.value = null;
customerCode.value = null;
subscriptionData.value = null;
paymentToken.value = null;
errorMessage.value = "";
successMessage.value = "";
form.email = "";
form.name = "";
form.circle = "community";
form.contributionTier = "15";
};
// Cleanup on unmount
onUnmounted(() => {
cleanupHelcimPay();
});
</script>
<style scoped>
/* ---- HERO ---- */
.hero {
padding: 48px 32px;
border-bottom: 1px dashed var(--border);
}
.hero h1 {
font-family: 'Brygada 1918', serif;
font-size: 36px;
font-weight: 600;
color: var(--text-bright);
line-height: 1.15;
letter-spacing: -0.01em;
margin-bottom: 16px;
max-width: 540px;
}
.hero p {
color: var(--text-dim);
max-width: 460px;
line-height: 1.7;
}
/* ---- PARCHMENT LIST STYLES ---- */
:deep(.parchment-inset ul) {
list-style: none;
max-width: 560px;
padding: 0;
}
:deep(.parchment-inset ul li) {
font-size: 13px;
color: #c8bea8;
line-height: 1.75;
padding: 4px 0;
padding-left: 16px;
position: relative;
}
:deep(.parchment-inset ul li::before) {
content: "--";
position: absolute;
left: 0;
color: var(--candle-dim);
opacity: 0.5;
}
.parchment-link {
color: var(--candle-faint);
font-size: 12px;
}
.parchment-link:hover {
color: var(--candle-dim);
}
/* ---- CONTENT ROW (three circles) ---- */
.content-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
border-bottom: 1px dashed var(--border);
}
.content-block {
padding: 24px 28px;
border-right: 1px dashed var(--border);
min-width: 0;
overflow-wrap: break-word;
}
.content-block:last-child {
border-right: none;
}
.content-block h2 {
font-family: 'Brygada 1918', serif;
font-size: 18px;
font-weight: 500;
color: var(--text-bright);
margin-bottom: 8px;
}
.content-block p {
color: var(--text-dim);
font-size: 12px;
line-height: 1.65;
}
.circle-not-sure {
font-size: 11px;
color: var(--text-faint);
margin-top: 10px;
line-height: 1.6;
}
/* ---- FULL-WIDTH SECTION ---- */
.full-section {
padding: 32px;
border-bottom: 1px dashed var(--border);
}
.full-section h2 {
font-family: 'Brygada 1918', serif;
font-size: 20px;
font-weight: 500;
color: var(--text-bright);
margin-bottom: 16px;
}
.section-intro {
font-size: 13px;
color: var(--text-dim);
line-height: 1.65;
margin-bottom: 20px;
}
/* ---- TIER CARDS ---- */
.tier-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
margin: 16px 0;
}
.tier-card {
border: 1px dashed var(--border);
padding: 20px 16px;
text-align: center;
transition: border-color 0.2s;
}
.tier-card:hover {
border-color: var(--candle-faint);
}
.tier-card.suggested {
border-color: var(--candle-dim);
border-style: solid;
}
.tier-card.suggested:hover {
border-color: var(--candle);
}
.tier-amount {
font-family: 'Brygada 1918', serif;
font-size: 22px;
font-weight: 600;
color: var(--text-bright);
margin-bottom: 2px;
}
.tier-card.suggested .tier-amount {
color: var(--candle);
}
.tier-freq {
font-size: 11px;
color: var(--text-faint);
margin-bottom: 10px;
}
.tier-desc {
font-size: 11px;
color: var(--text-faint);
line-height: 1.5;
}
.tier-badge {
display: inline-block;
font-size: 9px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--candle-dim);
border: 1px dashed var(--candle-faint);
padding: 1px 6px;
margin-top: 8px;
}
.solidarity-note {
font-size: 12px;
color: var(--text-dim);
line-height: 1.65;
margin-top: 16px;
max-width: 560px;
}
/* ---- FORM SECTION ---- */
.form-section {
padding: 32px;
border-bottom: 1px dashed var(--border);
}
.form-section h2 {
font-family: 'Brygada 1918', serif;
font-size: 20px;
font-weight: 500;
color: var(--text-bright);
margin-bottom: 4px;
}
.form-intro {
font-size: 12px;
color: var(--text-dim);
margin-bottom: 24px;
line-height: 1.65;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
max-width: 600px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group.full-width {
grid-column: 1 / -1;
}
.form-label {
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-faint);
}
.form-input {
background: var(--surface);
border: 1px dashed var(--border);
color: var(--text-bright);
font-family: 'Commit Mono', monospace;
font-size: 13px;
padding: 10px 14px;
transition: border-color 0.2s;
outline: none;
width: 100%;
}
.form-input:focus {
border-color: var(--candle-dim);
border-style: solid;
}
.form-input::placeholder {
color: var(--text-faint);
}
/* ---- CIRCLE RADIOS ---- */
.circle-radios {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.circle-radio {
position: relative;
}
.circle-radio input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.circle-radio label {
display: block;
border: 1px dashed var(--border);
padding: 14px 12px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.circle-radio label:hover {
border-color: var(--candle-faint);
}
.circle-radio input:checked + label {
border-style: solid;
}
.circle-radio input:checked + label .circle-label-name {
color: var(--text-bright);
}
.circle-radio.community input:checked + label {
border-color: var(--c-community);
}
.circle-radio.founder input:checked + label {
border-color: var(--c-founder);
}
.circle-radio.practitioner input:checked + label {
border-color: var(--c-practitioner);
}
.circle-label-name {
font-size: 12px;
color: var(--text-dim);
display: block;
margin-bottom: 2px;
}
.circle-label-desc {
font-size: 10px;
color: var(--text-faint);
}
/* ---- CONTRIBUTION SELECT ---- */
.form-select {
background: var(--surface);
border: 1px dashed var(--border);
color: var(--text-bright);
font-family: 'Commit Mono', monospace;
font-size: 13px;
padding: 10px 14px;
transition: border-color 0.2s;
outline: none;
width: 100%;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238a7e6a' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
cursor: pointer;
}
.form-select:focus {
border-color: var(--candle-dim);
border-style: solid;
}
.form-select option {
background: var(--surface);
color: var(--text-bright);
}
/* ---- SUBMIT BUTTON ---- */
.form-submit {
display: inline-block;
background: var(--parch);
color: var(--candle-faint);
font-family: 'Commit Mono', monospace;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
border: 1px solid var(--parch);
padding: 12px 28px;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
text-align: center;
}
.form-submit:hover {
background: #3a3025;
border-color: #3a3025;
color: var(--candle-dim);
text-decoration: none;
}
.form-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ---- FORM NOTE ---- */
.form-note {
font-size: 11px;
color: var(--text-faint);
line-height: 1.6;
margin-top: 16px;
max-width: 460px;
}
.form-note a,
.form-note :deep(a) {
color: var(--candle-dim);
}
/* ---- ERROR & SUCCESS BOXES ---- */
.error-box {
border: 1px dashed var(--ember);
color: var(--ember);
padding: 12px 16px;
font-size: 12px;
margin-bottom: 20px;
max-width: 600px;
}
.success-box {
border: 1px dashed var(--green, var(--candle));
color: var(--green, var(--candle));
padding: 12px 16px;
font-size: 12px;
margin-bottom: 20px;
max-width: 600px;
}
/* ---- DETAILS LIST (confirmation) ---- */
.details-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.details-row {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 13px;
}
.details-row dt {
color: var(--text-faint);
}
.details-row dd {
color: var(--text-bright);
font-weight: 500;
}
/* ---- PAYMENT INSTRUCTION ---- */
.payment-instruction {
font-size: 13px;
color: var(--text-dim);
line-height: 1.65;
}
/* ---- REDIRECT NOTE ---- */
.redirect-note {
font-size: 12px;
color: var(--text-dim);
text-align: center;
}
/* ---- BUTTON ROW ---- */
.button-row {
display: flex;
gap: 12px;
align-items: center;
margin-top: 20px;
}
/* ---- MEMBER INFO GRID ---- */
.member-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
max-width: 500px;
margin-bottom: 8px;
}
.info-value {
font-family: 'Brygada 1918', serif;
font-size: 18px;
font-weight: 500;
color: var(--text-bright);
margin-top: 4px;
}
/* ---- UTILITY ---- */
.capitalize {
text-transform: capitalize;
}
/* ---- RESPONSIVE ---- */
@media (max-width: 768px) {
.content-row {
grid-template-columns: 1fr;
}
.content-block {
border-right: none;
border-bottom: 1px dashed var(--border);
}
.content-block:last-child {
border-bottom: none;
}
.tier-row {
grid-template-columns: repeat(3, 1fr);
}
.form-grid {
grid-template-columns: 1fr;
}
.form-group.full-width {
grid-column: auto;
}
.circle-radios {
grid-template-columns: 1fr;
}
.member-info-grid {
grid-template-columns: 1fr;
}
.hero {
padding: 32px 20px;
}
.hero h1 {
font-size: 28px;
}
.full-section,
.form-section {
padding: 24px 20px;
}
.content-block {
padding: 20px;
}
}
@media (max-width: 480px) {
.tier-row {
grid-template-columns: repeat(2, 1fr);
}
.button-row {
flex-direction: column;
align-items: stretch;
}
}
</style>