1088 lines
28 KiB
Vue
1088 lines
28 KiB
Vue
<template>
|
||
<div>
|
||
<!-- Already a member -->
|
||
<template v-if="isAuthenticated">
|
||
<div class="full-section">
|
||
<h1>You're already a member</h1>
|
||
<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">
|
||
{{ formatContribution(memberData?.contributionAmount ?? 0, memberData?.billingCadence) }} CAD
|
||
</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: new layout -->
|
||
<template v-else>
|
||
<!-- HERO (split, matches about.vue) -->
|
||
<div class="join-hero">
|
||
<div class="join-hero-left">
|
||
<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>
|
||
<div class="join-hero-right">
|
||
<div class="section-label">What you get</div>
|
||
<ul>
|
||
<li>Full access to the knowledge commons</li>
|
||
<li>Free entry to all events and workshops</li>
|
||
<li>Slack invite in the next monthly onboarding wave</li>
|
||
<li>Equal access regardless of contribution</li>
|
||
<li>Pick your circle anytime after signup</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FORM + ASIDE -->
|
||
<div class="join-grid">
|
||
<div class="join-main">
|
||
<div class="join-main-inner">
|
||
<div v-if="errorMessage" class="error-box" role="alert">{{ errorMessage }}</div>
|
||
|
||
<form @submit.prevent="handleSubmit">
|
||
<!-- About you -->
|
||
<div class="form-block">
|
||
<h2>About you</h2>
|
||
<p class="form-block-intro">
|
||
We'll send a magic link to confirm your email. No passwords.
|
||
</p>
|
||
<div class="identity-grid">
|
||
<div class="field-row">
|
||
<label for="join-name">Name</label>
|
||
<input
|
||
id="join-name"
|
||
v-model="form.name"
|
||
class="form-input"
|
||
type="text"
|
||
required
|
||
:aria-invalid="!!fieldErrors.name"
|
||
aria-describedby="join-name-error"
|
||
@blur="validateName"
|
||
@input="fieldErrors.name && (fieldErrors.name = '')"
|
||
>
|
||
<p v-if="fieldErrors.name" id="join-name-error" class="field-error">
|
||
{{ fieldErrors.name }}
|
||
</p>
|
||
</div>
|
||
<div class="field-row">
|
||
<label for="join-email">Email</label>
|
||
<input
|
||
id="join-email"
|
||
v-model="form.email"
|
||
class="form-input"
|
||
type="email"
|
||
placeholder="you@example.com"
|
||
required
|
||
:aria-invalid="!!fieldErrors.email"
|
||
aria-describedby="join-email-error"
|
||
@blur="validateEmail"
|
||
@input="fieldErrors.email && (fieldErrors.email = '')"
|
||
>
|
||
<p v-if="fieldErrors.email" id="join-email-error" class="field-error">
|
||
{{ fieldErrors.email }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pay what you can -->
|
||
<div class="form-block">
|
||
<div class="pwyc-header">
|
||
<h2>Pay what you can</h2>
|
||
<div
|
||
class="cadence-toggle"
|
||
role="group"
|
||
aria-label="Billing cadence"
|
||
>
|
||
<button
|
||
type="button"
|
||
data-testid="cadence-monthly"
|
||
:class="{ active: cadence === 'monthly' }"
|
||
:aria-pressed="cadence === 'monthly'"
|
||
@click="onCadenceChange('monthly')"
|
||
>
|
||
Monthly
|
||
</button>
|
||
<button
|
||
type="button"
|
||
data-testid="cadence-annual"
|
||
:class="{ active: cadence === 'annual' }"
|
||
:aria-pressed="cadence === 'annual'"
|
||
@click="onCadenceChange('annual')"
|
||
>
|
||
Annual
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p class="form-block-intro">
|
||
Equal access for everyone. Pick what fits. These aren't
|
||
tiers.
|
||
</p>
|
||
|
||
<ul
|
||
class="pwyc-list"
|
||
aria-label="Contribution amount"
|
||
role="radiogroup"
|
||
>
|
||
<li
|
||
v-for="preset in presetRows"
|
||
:key="preset.baseAmount"
|
||
:class="[
|
||
'pwyc-row',
|
||
{ selected: isPresetSelected(preset) },
|
||
]"
|
||
>
|
||
<input
|
||
:id="`pwyc-${preset.baseAmount}`"
|
||
v-model.number="form.contributionAmount"
|
||
class="pwyc-radio"
|
||
type="radio"
|
||
name="pwyc-amount"
|
||
:value="preset.cadenceAmount"
|
||
@change="customAmount = ''"
|
||
>
|
||
<label
|
||
:for="`pwyc-${preset.baseAmount}`"
|
||
class="pwyc-row-content"
|
||
>
|
||
<span class="pwyc-amt">${{ preset.cadenceAmount }}</span>
|
||
<span class="pwyc-label">{{ preset.label }}</span>
|
||
<span v-if="preset.suggested" class="pwyc-tag"
|
||
>Suggested</span
|
||
>
|
||
</label>
|
||
</li>
|
||
<li :class="['pwyc-row', 'pwyc-custom', { selected: isCustomSelected }]">
|
||
<div class="pwyc-custom-input-wrap">
|
||
<span class="currency">$</span>
|
||
<input
|
||
v-model="customAmount"
|
||
class="pwyc-custom-input"
|
||
type="number"
|
||
min="0"
|
||
placeholder="100"
|
||
aria-label="Custom contribution amount"
|
||
@blur="commitCustomAmount"
|
||
>
|
||
</div>
|
||
<span class="pwyc-label"
|
||
>A different amount works for me</span
|
||
>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Submit row -->
|
||
<div class="submit-row">
|
||
<label class="agreement-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>
|
||
<button
|
||
class="submit-btn"
|
||
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>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="join-aside">
|
||
<div class="join-aside-inner">
|
||
<div class="section-label">About the money</div>
|
||
<ul class="trust-list">
|
||
<li>
|
||
<strong>Pay what you can.</strong> Equal access for everyone, no
|
||
matter the amount. If you can pay more, you're making room for
|
||
someone who can't.
|
||
</li>
|
||
<li>
|
||
<strong>Canadian charity.</strong> Baby Ghosts Studio
|
||
Development Fund is a registered Canadian charity. We'll help
|
||
you set up tax receipts after you join.
|
||
</li>
|
||
<li>
|
||
<strong>Secure payment.</strong> Card entry handled by Helcim;
|
||
we never see your card details.
|
||
</li>
|
||
<li>
|
||
<strong>Change anytime.</strong> Adjust your contribution or
|
||
cancel from your dashboard.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Flow overlay: covers the page from form submit through redirect.
|
||
Lives outside v-if/v-else so it survives the auth state flip that
|
||
fires after checkMemberStatus() at the end of createSubscription. -->
|
||
<SignupFlowOverlay
|
||
:state="flowState"
|
||
:summary="flowSummary"
|
||
:error-message="errorMessage"
|
||
@close="closeFlowOverlay"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { reactive, ref, computed, onMounted, onUnmounted } from "vue";
|
||
import {
|
||
requiresPayment,
|
||
formatContribution,
|
||
CONTRIBUTION_PRESETS,
|
||
} from "~/config/contributions";
|
||
|
||
useSiteMeta({
|
||
title: "Join",
|
||
description:
|
||
"Join Ghost Guild — a membership community for game developers exploring cooperative models. Everyone gets everything. Pay what you can, $0 to $50 per month.",
|
||
});
|
||
|
||
// Auth state
|
||
const { isAuthenticated, memberData, checkMemberStatus } = useAuth();
|
||
|
||
onMounted(async () => {
|
||
await checkMemberStatus();
|
||
});
|
||
|
||
// Form state — circle stays "community" and is submitted to the API unchanged.
|
||
// Picker is removed from the template; selection moves to a post-signup prompt.
|
||
const form = reactive({
|
||
email: "",
|
||
name: "",
|
||
circle: "community",
|
||
contributionAmount: 15,
|
||
agreedToGuidelines: false,
|
||
billingAddress: {
|
||
street: "",
|
||
city: "",
|
||
province: "",
|
||
postalCode: "",
|
||
country: "CA",
|
||
},
|
||
});
|
||
|
||
const isSubmitting = ref(false);
|
||
const errorMessage = ref("");
|
||
const successMessage = ref("");
|
||
const cadence = ref("monthly"); // 'monthly' | 'annual'
|
||
const customAmount = ref(""); // string; the raw <input type=number> value
|
||
|
||
const fieldErrors = reactive({ name: "", email: "" });
|
||
|
||
const validateName = () => {
|
||
fieldErrors.name = form.name.trim() ? "" : "Please enter your name.";
|
||
};
|
||
|
||
const validateEmail = () => {
|
||
const value = form.email.trim();
|
||
if (!value) {
|
||
fieldErrors.email = "";
|
||
return;
|
||
}
|
||
const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||
fieldErrors.email = ok ? "" : "Please enter a valid email address.";
|
||
};
|
||
|
||
const flowState = ref("idle");
|
||
|
||
const customerId = ref(null);
|
||
const customerCode = ref(null);
|
||
const subscriptionData = ref(null);
|
||
const paymentToken = ref(null);
|
||
|
||
// Preset rows with cadence-adjusted display amounts.
|
||
const presetRows = computed(() =>
|
||
CONTRIBUTION_PRESETS.map((p) => ({
|
||
baseAmount: p.amount,
|
||
cadenceAmount: cadence.value === "annual" ? p.amount * 12 : p.amount,
|
||
label: p.label,
|
||
suggested: p.amount === 15,
|
||
})),
|
||
);
|
||
|
||
const isCustomSelected = computed(() => customAmount.value !== "");
|
||
|
||
const isPresetSelected = (preset) =>
|
||
!isCustomSelected.value && form.contributionAmount === preset.cadenceAmount;
|
||
|
||
const onCadenceChange = (newCadence) => {
|
||
if (newCadence === cadence.value) return;
|
||
const next =
|
||
newCadence === "annual"
|
||
? form.contributionAmount * 12
|
||
: Math.floor(form.contributionAmount / 12);
|
||
form.contributionAmount = next;
|
||
if (customAmount.value !== "") {
|
||
customAmount.value = String(next);
|
||
}
|
||
cadence.value = newCadence;
|
||
};
|
||
|
||
// On blur of the custom-amount input: commit to form.contributionAmount.
|
||
// If the typed value matches a preset, snap the radio selection to that
|
||
// preset row and clear the custom input.
|
||
const commitCustomAmount = () => {
|
||
const raw = customAmount.value;
|
||
if (raw === "" || raw === null || raw === undefined) return;
|
||
const n = Math.max(0, Math.floor(Number(raw)));
|
||
if (!Number.isFinite(n)) {
|
||
customAmount.value = "";
|
||
return;
|
||
}
|
||
const matched = presetRows.value.find((p) => p.cadenceAmount === n);
|
||
if (matched) {
|
||
form.contributionAmount = matched.cadenceAmount;
|
||
customAmount.value = "";
|
||
} else {
|
||
form.contributionAmount = n;
|
||
}
|
||
};
|
||
|
||
const {
|
||
initializeHelcimPay,
|
||
verifyPayment,
|
||
cleanup: cleanupHelcimPay,
|
||
} = useHelcimPay();
|
||
|
||
const isFormValid = computed(() => {
|
||
return (
|
||
form.name &&
|
||
form.email &&
|
||
form.circle &&
|
||
Number.isInteger(form.contributionAmount) &&
|
||
form.contributionAmount >= 0 &&
|
||
form.agreedToGuidelines
|
||
);
|
||
});
|
||
|
||
const needsPayment = computed(() => {
|
||
return requiresPayment(form.contributionAmount);
|
||
});
|
||
|
||
const flowSummary = computed(() => {
|
||
const amount = form.contributionAmount || 0;
|
||
return {
|
||
name: form.name,
|
||
email: form.email,
|
||
circle: form.circle,
|
||
contribution: amount > 0 ? formatContribution(amount, cadence.value) : "$0",
|
||
};
|
||
});
|
||
|
||
const handleSubmit = async () => {
|
||
if (isSubmitting.value || !isFormValid.value) return;
|
||
|
||
isSubmitting.value = true;
|
||
errorMessage.value = "";
|
||
flowState.value = "creating-customer";
|
||
|
||
try {
|
||
const response = await $fetch("/api/helcim/customer", {
|
||
method: "POST",
|
||
body: {
|
||
name: form.name,
|
||
email: form.email,
|
||
circle: form.circle,
|
||
contributionAmount: form.contributionAmount,
|
||
cadence: cadence.value,
|
||
agreedToGuidelines: form.agreedToGuidelines,
|
||
billingAddress: form.billingAddress,
|
||
},
|
||
});
|
||
|
||
if (!response.success) {
|
||
throw new Error("Failed to create account.");
|
||
}
|
||
|
||
customerId.value = response.customerId;
|
||
customerCode.value = response.customerCode;
|
||
|
||
if (!needsPayment.value) {
|
||
flowState.value = "creating-subscription";
|
||
await createSubscription();
|
||
return;
|
||
}
|
||
|
||
flowState.value = "opening-payment";
|
||
await initializeHelcimPay(customerId.value, customerCode.value, 0);
|
||
|
||
const paymentResult = await verifyPayment();
|
||
if (!paymentResult?.success) {
|
||
throw new Error("Payment was not completed.");
|
||
}
|
||
paymentToken.value = paymentResult.cardToken;
|
||
|
||
flowState.value = "processing-payment";
|
||
await $fetch("/api/helcim/verify-payment", {
|
||
method: "POST",
|
||
body: {
|
||
cardToken: paymentResult.cardToken,
|
||
customerId: customerId.value,
|
||
},
|
||
});
|
||
|
||
flowState.value = "creating-subscription";
|
||
const subscriptionResult = await createSubscription(
|
||
paymentResult.cardToken,
|
||
);
|
||
|
||
if (!subscriptionResult || subscriptionResult.success === false) {
|
||
successMessage.value =
|
||
"Payment successful. Subscription setup may need manual completion.";
|
||
flowState.value = "success";
|
||
}
|
||
} catch (error) {
|
||
console.error("Join flow error:", error);
|
||
errorMessage.value =
|
||
error.data?.message ||
|
||
error.message ||
|
||
"Something went wrong. Please try again.";
|
||
flowState.value = "error";
|
||
} finally {
|
||
isSubmitting.value = false;
|
||
}
|
||
};
|
||
|
||
const createSubscription = async (cardToken = null) => {
|
||
try {
|
||
const response = await $fetch("/api/helcim/subscription", {
|
||
method: "POST",
|
||
body: {
|
||
customerId: customerId.value,
|
||
customerCode: customerCode.value,
|
||
contributionAmount: form.contributionAmount,
|
||
cadence: cadence.value,
|
||
cardToken: cardToken,
|
||
},
|
||
});
|
||
|
||
if (response.success) {
|
||
subscriptionData.value = response.subscription;
|
||
flowState.value = "success";
|
||
successMessage.value = "Your membership is active.";
|
||
} 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",
|
||
);
|
||
return {
|
||
success: false,
|
||
error:
|
||
error.data?.message || error.message || "Failed to create subscription",
|
||
};
|
||
}
|
||
};
|
||
|
||
const closeFlowOverlay = () => {
|
||
flowState.value = "idle";
|
||
errorMessage.value = "";
|
||
};
|
||
|
||
onUnmounted(() => {
|
||
cleanupHelcimPay();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ---- HERO (split, matches about.vue) ---- */
|
||
.join-hero {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0;
|
||
align-items: stretch;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.join-hero-left {
|
||
padding: 32px 32px 28px;
|
||
border-right: 1px dashed var(--border);
|
||
align-self: stretch;
|
||
}
|
||
.join-hero-left h1 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: var(--text-bright);
|
||
line-height: 1.15;
|
||
letter-spacing: -0.01em;
|
||
margin: 0 0 12px;
|
||
}
|
||
.join-hero-left p {
|
||
color: var(--text-dim);
|
||
line-height: 1.7;
|
||
font-size: 13px;
|
||
margin: 0;
|
||
max-width: 460px;
|
||
}
|
||
.join-hero-right {
|
||
padding: 32px;
|
||
align-self: stretch;
|
||
}
|
||
.join-hero-right .section-label {
|
||
margin-bottom: 10px;
|
||
}
|
||
.join-hero-right ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
.join-hero-right li {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
line-height: 1.7;
|
||
padding: 2px 0 2px 14px;
|
||
position: relative;
|
||
}
|
||
.join-hero-right li::before {
|
||
content: "›";
|
||
position: absolute;
|
||
left: 0;
|
||
color: var(--candle-faint);
|
||
}
|
||
|
||
/* ---- TWO-COL GRID ---- */
|
||
.join-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
align-items: stretch;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.join-main {
|
||
border-right: 1px dashed var(--border);
|
||
padding: 32px;
|
||
min-width: 0;
|
||
}
|
||
.join-aside {
|
||
padding: 32px;
|
||
min-width: 0;
|
||
position: relative;
|
||
}
|
||
.join-main-inner {
|
||
max-width: 560px;
|
||
}
|
||
.join-aside-inner {
|
||
max-width: 460px;
|
||
}
|
||
|
||
/* ---- SECTION LABEL (hero + aside + auth'd branch DashedBox) ---- */
|
||
.section-label {
|
||
font-size: 10px;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: var(--text-faint);
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* ---- FORM BLOCKS ---- */
|
||
.form-block {
|
||
margin-bottom: 32px;
|
||
}
|
||
.form-block:last-of-type {
|
||
margin-bottom: 0;
|
||
}
|
||
.form-block h2 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
margin: 0 0 4px;
|
||
}
|
||
.form-block-intro {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
margin: 0 0 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* ---- IDENTITY GRID ---- */
|
||
.identity-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 14px;
|
||
}
|
||
.field-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.field-row label {
|
||
font-size: 10px;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: var(--text-faint);
|
||
}
|
||
.form-input {
|
||
background: var(--input-bg);
|
||
border: 1px dashed var(--border);
|
||
color: var(--text-bright);
|
||
font-family: "Commit Mono", monospace;
|
||
font-size: 13px;
|
||
padding: 10px 14px;
|
||
width: 100%;
|
||
outline: none;
|
||
transition: border-color 0.15s;
|
||
}
|
||
.form-input:focus {
|
||
border-color: var(--candle);
|
||
border-style: solid;
|
||
}
|
||
.form-input::placeholder {
|
||
color: var(--text-faint);
|
||
}
|
||
.field-error {
|
||
font-size: 11px;
|
||
color: var(--ember);
|
||
margin: 0;
|
||
}
|
||
|
||
/* ---- PWYC ---- */
|
||
.pwyc-header {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
margin-bottom: 4px;
|
||
gap: 12px;
|
||
}
|
||
.cadence-toggle {
|
||
display: inline-flex;
|
||
border: 1px dashed var(--border);
|
||
}
|
||
.cadence-toggle button {
|
||
background: transparent;
|
||
border: none;
|
||
padding: 7px 12px;
|
||
font-family: "Commit Mono", monospace;
|
||
font-size: 10px;
|
||
color: var(--text-faint);
|
||
cursor: pointer;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
}
|
||
.cadence-toggle button.active {
|
||
background: var(--parch);
|
||
color: var(--parch-text);
|
||
}
|
||
|
||
.pwyc-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
border-top: 1px dashed var(--border);
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.pwyc-row {
|
||
position: relative;
|
||
border-bottom: 1px dashed var(--border);
|
||
transition: background 0.12s;
|
||
}
|
||
.pwyc-row:last-of-type {
|
||
border-bottom: none;
|
||
}
|
||
.pwyc-row:hover {
|
||
background: var(--surface);
|
||
}
|
||
.pwyc-row.selected {
|
||
background: var(--input-bg);
|
||
}
|
||
.pwyc-row.selected::before {
|
||
content: "›";
|
||
position: absolute;
|
||
left: 12px;
|
||
color: var(--candle);
|
||
font-size: 14px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
line-height: 1;
|
||
}
|
||
.pwyc-radio {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
pointer-events: none;
|
||
}
|
||
.pwyc-radio:focus-visible + .pwyc-row-content {
|
||
outline: 1px solid var(--candle);
|
||
outline-offset: -2px;
|
||
}
|
||
.pwyc-row-content {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
padding: 8px 14px 8px 28px;
|
||
cursor: pointer;
|
||
}
|
||
.pwyc-amt {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text-bright);
|
||
min-width: 36px;
|
||
flex-shrink: 0;
|
||
}
|
||
.pwyc-row.selected .pwyc-amt {
|
||
color: var(--candle);
|
||
}
|
||
.pwyc-label {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
line-height: 1.5;
|
||
flex: 1;
|
||
}
|
||
.pwyc-row.selected .pwyc-label {
|
||
color: var(--text-bright);
|
||
}
|
||
.pwyc-tag {
|
||
font-size: 9px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--candle);
|
||
border: 1px dashed var(--candle-faint);
|
||
padding: 1px 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Custom row */
|
||
.pwyc-custom {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
padding: 6px 14px 6px 28px;
|
||
cursor: default;
|
||
}
|
||
.pwyc-custom:hover {
|
||
background: transparent;
|
||
}
|
||
.pwyc-custom.selected {
|
||
background: var(--input-bg);
|
||
}
|
||
.pwyc-custom-input-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
border: 1px dashed var(--border);
|
||
background: var(--input-bg);
|
||
height: 26px;
|
||
flex-shrink: 0;
|
||
}
|
||
.pwyc-custom-input-wrap .currency {
|
||
padding: 0 2px 0 6px;
|
||
color: var(--text-faint);
|
||
font-weight: 600;
|
||
font-size: 12px;
|
||
}
|
||
.pwyc-custom-input {
|
||
border: none;
|
||
background: transparent;
|
||
font-family: "Commit Mono", monospace;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-bright);
|
||
padding: 0 6px 0 0;
|
||
width: 56px;
|
||
outline: none;
|
||
-moz-appearance: textfield;
|
||
}
|
||
.pwyc-custom-input::-webkit-outer-spin-button,
|
||
.pwyc-custom-input::-webkit-inner-spin-button {
|
||
-webkit-appearance: none;
|
||
margin: 0;
|
||
}
|
||
|
||
/* ---- SUBMIT ROW ---- */
|
||
.submit-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-top: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.agreement-label {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: flex-start;
|
||
font-size: 12px;
|
||
color: var(--text);
|
||
line-height: 1.55;
|
||
cursor: pointer;
|
||
flex: 1;
|
||
min-width: 240px;
|
||
}
|
||
.agreement-label input {
|
||
margin-top: 3px;
|
||
flex-shrink: 0;
|
||
}
|
||
.agreement-label :deep(a) {
|
||
color: var(--candle);
|
||
text-decoration: underline;
|
||
}
|
||
.submit-btn {
|
||
display: inline-block;
|
||
background: var(--parch);
|
||
color: var(--parch-accent);
|
||
font-family: "Commit Mono", monospace;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.04em;
|
||
border: 1px solid var(--parch);
|
||
padding: 12px 24px;
|
||
cursor: pointer;
|
||
transition: background-color 0.15s, border-color 0.15s, color 0.15s;
|
||
flex-shrink: 0;
|
||
}
|
||
.submit-btn:hover {
|
||
background: var(--parch-hover);
|
||
color: var(--parch-text);
|
||
}
|
||
.submit-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* ---- TRUST LIST (aside) ---- */
|
||
.trust-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
.trust-list li {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
line-height: 1.65;
|
||
padding: 0 0 14px 14px;
|
||
position: relative;
|
||
}
|
||
.trust-list li:last-child {
|
||
padding-bottom: 0;
|
||
}
|
||
.trust-list li::before {
|
||
content: "›";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
color: var(--candle-faint);
|
||
}
|
||
.trust-list li :deep(strong) {
|
||
color: var(--text-bright);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* ---- ERROR BOX ---- */
|
||
.error-box {
|
||
border: 1px dashed var(--ember);
|
||
color: var(--ember);
|
||
padding: 12px 16px;
|
||
font-size: 12px;
|
||
margin-bottom: 20px;
|
||
max-width: 560px;
|
||
}
|
||
|
||
/* ---- AUTH'D MEMBER BRANCH ---- */
|
||
.full-section {
|
||
padding: 32px;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.full-section h1 {
|
||
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;
|
||
}
|
||
.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;
|
||
}
|
||
.button-row {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
margin-top: 20px;
|
||
}
|
||
.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: background-color 0.2s, border-color 0.2s, color 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;
|
||
}
|
||
.parchment-link {
|
||
color: var(--candle-faint);
|
||
font-size: 12px;
|
||
}
|
||
.parchment-link:hover {
|
||
color: var(--candle-dim);
|
||
}
|
||
.capitalize {
|
||
text-transform: capitalize;
|
||
}
|
||
|
||
/* ---- PARCHMENT LIST (only used in auth'd branch ParchmentInset) ---- */
|
||
: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;
|
||
}
|
||
|
||
/* ---- RESPONSIVE ---- */
|
||
@media (max-width: 1024px) {
|
||
.join-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.join-main {
|
||
border-right: none;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
}
|
||
@media (max-width: 768px) {
|
||
.join-hero {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.join-hero-left {
|
||
border-right: none;
|
||
border-bottom: 1px dashed var(--border);
|
||
padding: 24px 20px 20px;
|
||
}
|
||
.join-hero-right {
|
||
padding: 24px 20px;
|
||
}
|
||
.join-hero-left h1 {
|
||
font-size: 26px;
|
||
}
|
||
.identity-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.join-main,
|
||
.join-aside {
|
||
padding: 24px 20px;
|
||
}
|
||
.full-section {
|
||
padding: 24px 20px;
|
||
}
|
||
.member-info-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
@media (max-width: 480px) {
|
||
.submit-row {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
.submit-btn {
|
||
width: 100%;
|
||
}
|
||
.button-row {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
}
|
||
</style>
|