ghostguild-org/app/pages/join.vue
Jennie Robinson Faber c5e901ed24 feat(signup): community guidelines agreement and policies routes
Introduces /community-guidelines and /policies/{privacy,terms,[slug]} pages,
swaps the signup/invite checkbox from agreedToTerms to agreedToGuidelines,
adds Member.agreement.acceptedAt, and stamps the field when a Helcim
customer is created.
2026-04-18 17:06:10 +01:00

1130 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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, Slack, and peer support</li>
<li>Free access to all Ghost Guild events</li>
<li>Equal access for every member, regardless of contribution</li>
<li>Your circle reflects where you are, not rank</li>
<li>Pay what you can ($0&ndash;$50+/month, separate from circle)</li>
<li>Higher contributions create solidarity spots for others</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>
</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 practice. You are here to teach, advise,
mentor, and help shape the program itself. Alumni.
</p>
</div>
</div>
<!-- CONTRIBUTION + SIGN UP (two columns) -->
<div v-if="currentStep === 1" class="join-two-col">
<!-- Left: Monthly Contribution -->
<div class="join-col">
<div class="section-label" style="margin-bottom: 12px">
Monthly Contribution
</div>
<h2>Pay what you can</h2>
<ul class="tier-list">
<li><span class="tier-amt">$0</span> I need support right now</li>
<li><span class="tier-amt">$5</span> I can contribute</li>
<li>
<span class="tier-amt">$15</span> I can sustain the community
(suggested)
</li>
<li><span class="tier-amt">$30</span> I can support others too</li>
<li>
<span class="tier-amt">$50</span> I want to sponsor multiple
members
</li>
</ul>
<p class="solidarity-note">
Pay what you can. If you can pay more, you're making room for
someone who can't.
</p>
<p class="circle-not-sure">
Not sure where you fit? Start with Community. You can always move
later.
</p>
</div>
<!-- Right: Become a member -->
<div class="join-col">
<h2>Become a member</h2>
<p class="form-intro">
You'll get a magic link to confirm your email. No passwords.
</p>
<!-- Error Message -->
<div v-if="errorMessage" class="error-box">
{{ errorMessage }}
</div>
<form @submit.prevent="handleSubmit">
<div class="form-stack">
<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">
<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">
<label class="form-label" for="join-contribution"
>Monthly Contribution</label
>
<USelectMenu
id="join-contribution"
v-model="form.contributionTier"
:items="contributionItems"
value-key="value"
:search-input="false"
class="zine-select"
:ui="{
content: 'tz-content',
item: 'tz-item',
}"
/>
</div>
<div class="form-group full-width">
<label class="checkbox-label">
<input
v-model="form.agreedToGuidelines"
type="checkbox"
>
<span>
I agree to the Ghost Guild
<NuxtLink to="/community-guidelines" target="_blank"
>Community Guidelines</NuxtLink
>.
</span>
</label>
</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">
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>
</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",
agreedToGuidelines: false,
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();
// Minimal labels for the dropdown (tier descriptions live in the left column).
const contributionItems = [
{ value: "0", label: "$0/mo" },
{ value: "5", label: "$5/mo" },
{ value: "15", label: "$15/mo (suggested)" },
{ value: "30", label: "$30/mo" },
{ value: "50", label: "$50/mo" },
];
// Initialize composables
const {
initializeHelcimPay,
verifyPayment,
cleanup: cleanupHelcimPay,
} = useHelcimPay();
// Form validation
const isFormValid = computed(() => {
return (
form.name &&
form.email &&
form.circle &&
form.contributionTier &&
form.agreedToGuidelines
);
});
// 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,
agreedToGuidelines: form.agreedToGuidelines,
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 is active.";
// 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: var(--parch-text-dim);
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-faint);
font-size: 14px;
line-height: 1.4;
}
.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));
align-items: stretch;
border-bottom: 1px dashed var(--border);
}
.content-block {
padding: 24px 28px;
border-right: 1px dashed var(--border);
min-width: 0;
overflow-wrap: break-word;
align-self: stretch;
}
.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;
}
/* ---- TWO-COLUMN JOIN LAYOUT ---- */
.join-two-col {
display: grid;
grid-template-columns: 1fr 1fr;
border-bottom: 1px dashed var(--border);
}
.join-col {
padding: 32px;
}
.join-col:first-child {
border-right: 1px dashed var(--border);
}
.join-col h2 {
font-family: "Brygada 1918", serif;
font-size: 20px;
font-weight: 500;
color: var(--text-bright);
margin-bottom: 16px;
}
/* ---- 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 LIST (matches about page) ---- */
.tier-list {
list-style: none;
padding: 0;
}
.tier-list li {
padding: 5px 0;
font-size: 12px;
color: var(--text-dim);
border-bottom: 1px dashed var(--border);
display: flex;
gap: 12px;
}
.tier-list li:last-child {
border-bottom: none;
}
.tier-amt {
color: var(--text-bright);
font-weight: 600;
min-width: 36px;
}
.solidarity-note {
font-size: 12px;
color: var(--text-dim);
line-height: 1.65;
margin-top: 16px;
}
/* ---- 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-stack {
display: flex;
flex-direction: column;
gap: 20px;
max-width: 600px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.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(--parch-accent);
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: var(--parch-hover);
border-color: var(--parch-hover);
color: var(--parch-text);
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);
}
/* ---- CHECKBOX ---- */
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 8px;
cursor: pointer;
font-size: 12px;
color: var(--text-dim);
line-height: 1.5;
}
.checkbox-label input {
margin-top: 3px;
flex-shrink: 0;
}
.checkbox-label a,
.checkbox-label :deep(a) {
color: var(--candle);
}
/* ---- 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;
}
.join-two-col {
grid-template-columns: 1fr;
}
.join-col:first-child {
border-right: none;
border-bottom: 1px dashed var(--border);
}
.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) {
.button-row {
flex-direction: column;
align-items: stretch;
}
}
</style>