ghostguild-org/app/pages/admin/events/create.vue
Jennie Robinson Faber 9858316b30 fix(admin-events): preserve datetime on edit round-trip
Editing an event was pulling its UTC startDate/endDate, slicing off the
"Z" with toISOString().slice(0, 16), and then handing the bare digits to
a datetime-local input. The input reinterprets them as local time, so
each save shifted the time by the browser's UTC offset. Same pattern
for registrationDeadline and earlyBirdDeadline.

Format the value using local-time components instead so the round-trip
matches what the admin sees.
2026-05-18 17:56:17 +01:00

1223 lines
34 KiB
Vue

<template>
<div class="create-form">
<div class="page-header">
<div class="header-row">
<NuxtLink to="/admin/events" class="back-link">&larr; Events</NuxtLink>
<h1>{{ editingEvent ? "Edit Event" : "Create Event" }}</h1>
<p>Fill out the form below to create or update an event</p>
</div>
</div>
<div class="form-body">
<!-- Error Summary -->
<div v-if="formErrors.length > 0" class="error-box">
<h3>Please fix the following errors:</h3>
<ul>
<li v-for="error in formErrors" :key="error">{{ error }}</li>
</ul>
</div>
<!-- Success Message -->
<div v-if="showSuccessMessage" class="success-box">
{{
editingEvent
? "Event updated successfully!"
: "Event created successfully!"
}}
</div>
<form @submit.prevent="saveEvent">
<!-- Basic Information -->
<div class="form-section">
<h2 class="section-heading">Basic Information</h2>
<div class="field">
<label> Event Title <span class="required">*</span> </label>
<UInput
v-model="eventForm.title"
placeholder="Enter a clear, descriptive event title"
required
:color="fieldErrors.title ? 'error' : undefined"
class="w-full"
/>
<p v-if="fieldErrors.title" class="field-error">
{{ fieldErrors.title }}
</p>
</div>
<div class="field">
<label>Feature Image</label>
<ImageUpload v-model="eventForm.featureImage" />
<p class="help-text">
Upload a high-quality image (1200x630px recommended) to represent
your event
</p>
</div>
<div class="field">
<label> Event Description <span class="required">*</span> </label>
<UTextarea
v-model="eventForm.description"
placeholder="Provide a clear description of what attendees can expect from this event"
required
:rows="4"
:color="fieldErrors.description ? 'error' : undefined"
class="w-full"
/>
<p v-if="fieldErrors.description" class="field-error">
{{ fieldErrors.description }}
</p>
<p class="help-text">
This will be displayed on the event listing and detail pages
</p>
</div>
<div class="field">
<label>Additional Content</label>
<UTextarea
v-model="eventForm.content"
placeholder="Add detailed information, agenda, requirements, or other important details"
:rows="6"
class="w-full"
/>
<p class="help-text">
Optional: Provide additional context, agenda items, or detailed
requirements
</p>
</div>
</div>
<!-- Event Details -->
<div class="form-section">
<h2 class="section-heading">Event Details</h2>
<div class="form-grid">
<div class="field">
<label> Event Type <span class="required">*</span> </label>
<USelect
v-model="eventForm.eventType"
aria-label="Event type"
:items="[
{ label: 'Community Meetup', value: 'community' },
{ label: 'Workshop', value: 'workshop' },
{ label: 'Social Event', value: 'social' },
{ label: 'Showcase', value: 'showcase' },
]"
class="w-full"
/>
<p class="help-text">
Choose the category that best describes your event
</p>
</div>
<div class="field">
<label> Location <span class="required">*</span> </label>
<UInput
v-model="eventForm.location"
placeholder="e.g., https://zoom.us/j/123... or #channel-name"
required
:color="fieldErrors.location ? 'error' : undefined"
class="w-full"
/>
<p v-if="fieldErrors.location" class="field-error">
{{ fieldErrors.location }}
</p>
<p class="help-text">
Enter a video conference link or Slack channel (starting with #)
</p>
</div>
<div class="field">
<label> Start Date & Time <span class="required">*</span> </label>
<NaturalDateInput
v-model="eventForm.startDate"
placeholder="e.g., 'tomorrow at 3pm', 'next Friday at 9am'"
:required="true"
/>
<p v-if="fieldErrors.startDate" class="field-error">
{{ fieldErrors.startDate }}
</p>
</div>
<div class="field">
<label> End Date & Time <span class="required">*</span> </label>
<NaturalDateInput
v-model="eventForm.endDate"
placeholder="e.g., 'tomorrow at 5pm', 'next Friday at 11am'"
:required="true"
/>
<p v-if="fieldErrors.endDate" class="field-error">
{{ fieldErrors.endDate }}
</p>
</div>
<div class="field">
<label>Max Attendees</label>
<UInput
v-model="eventForm.maxAttendees"
type="number"
min="1"
placeholder="Leave blank for unlimited"
class="w-full"
/>
<p class="help-text">
Set a maximum number of attendees (optional)
</p>
</div>
<div class="field">
<label>Registration Deadline</label>
<NaturalDateInput
v-model="eventForm.registrationDeadline"
placeholder="e.g., 'tomorrow at noon', '1 hour before event'"
/>
<p class="help-text">
When should registration close? (optional)
</p>
</div>
</div>
</div>
<!-- Target Audience -->
<div class="form-section">
<h2 class="section-heading">Target Audience</h2>
<div class="field">
<label>Target Circles</label>
<div class="check-group">
<label class="check-label">
<input
v-model="eventForm.targetCircles"
value="community"
type="checkbox"
/>
<div>
<strong>Community Circle</strong>
<span class="help-text">
New members and those exploring the community
</span>
</div>
</label>
<label class="check-label">
<input
v-model="eventForm.targetCircles"
value="founder"
type="checkbox"
/>
<div>
<strong>Founder Circle</strong>
<span class="help-text">
Entrepreneurs and business leaders
</span>
</div>
</label>
<label class="check-label">
<input
v-model="eventForm.targetCircles"
value="practitioner"
type="checkbox"
/>
<div>
<strong>Practitioner Circle</strong>
<span class="help-text">
Experts and professionals sharing knowledge
</span>
</div>
</label>
</div>
<p class="help-text">
Select which circles this event is most relevant for (leave blank
for all circles)
</p>
</div>
</div>
<!-- Tags -->
<div class="form-section">
<h2 class="section-heading">Tags</h2>
<div class="field">
<label>Event Tags</label>
<USelectMenu
v-model="eventForm.tags"
:items="tagOptions"
value-key="value"
multiple
placeholder="Select tags..."
class="w-full"
/>
<p class="help-text">
Tag this event to help with discovery and recommendations
</p>
</div>
</div>
<!-- Ticketing -->
<div class="form-section">
<h2 class="section-heading">Ticketing</h2>
<label class="check-label">
<input v-model="eventForm.tickets.enabled" type="checkbox" />
<div>
<strong>Enable Ticketing</strong>
<span class="help-text"> Allow ticket sales for this event </span>
</div>
</label>
<div v-if="eventForm.tickets.enabled" class="nested-section">
<label class="check-label">
<input
v-model="eventForm.tickets.public.available"
type="checkbox"
/>
<div>
<strong>Public Tickets Available</strong>
<span class="help-text">
Allow non-members to purchase tickets
</span>
</div>
</label>
<div v-if="eventForm.tickets.public.available">
<div class="form-grid">
<div class="field">
<label>Ticket Name</label>
<UInput
v-model="eventForm.tickets.public.name"
placeholder="e.g., General Admission"
class="w-full"
/>
</div>
<div class="field">
<label>Price (CAD)</label>
<UInput
v-model="eventForm.tickets.public.price"
type="number"
min="0"
step="0.01"
placeholder="0.00"
class="w-full"
/>
<p class="help-text">Set to 0 for free public events</p>
</div>
</div>
<div class="field">
<label>Ticket Description</label>
<UTextarea
v-model="eventForm.tickets.public.description"
placeholder="What's included with this ticket..."
:rows="2"
class="w-full"
/>
</div>
<div class="form-grid">
<div class="field">
<label>Quantity Available</label>
<UInput
v-model="eventForm.tickets.public.quantity"
type="number"
min="1"
placeholder="Leave blank for unlimited"
class="w-full"
/>
</div>
<div class="field">
<label>Early Bird Price (Optional)</label>
<UInput
v-model="eventForm.tickets.public.earlyBirdPrice"
type="number"
min="0"
step="0.01"
placeholder="0.00"
class="w-full"
/>
</div>
</div>
<div
v-if="eventForm.tickets.public.earlyBirdPrice > 0"
class="field"
>
<label>Early Bird Deadline</label>
<NaturalDateInput
v-model="eventForm.tickets.public.earlyBirdDeadline"
placeholder="e.g., '1 week before event', 'next Monday'"
/>
<p class="help-text">
Price increases to regular price after this date
</p>
</div>
</div>
</div>
<div class="note-box">
<strong>Note:</strong> Members always get free access to all events
regardless of ticket settings.
</div>
</div>
<!-- Series Management -->
<div class="form-section">
<h2 class="section-heading">Series Management</h2>
<label class="check-label">
<input v-model="eventForm.series.isSeriesEvent" type="checkbox" />
<div>
<strong>Part of Event Series</strong>
<span class="help-text">
This event is part of a multi-event series
</span>
</div>
</label>
<div v-if="eventForm.series.isSeriesEvent" class="nested-section">
<div class="field">
<label> Select Series <span class="required">*</span> </label>
<div class="series-select-row">
<USelect
v-model="selectedSeriesId"
aria-label="Select series"
@update:model-value="onSeriesSelect"
:items="
availableSeries.map((series) => ({
label: `${series.title} (${series.eventCount || 0} events)`,
value: series.id,
}))
"
placeholder="Choose existing series or create new..."
value-key="value"
class="w-full"
/>
<NuxtLink to="/admin/series/create" class="btn btn-primary">
New Series
</NuxtLink>
</div>
<p class="help-text">
Select an existing series or create a new one
</p>
</div>
<div v-if="selectedSeriesId || eventForm.series.id">
<div class="field">
<label> Series Title <span class="required">*</span> </label>
<UInput
v-model="eventForm.series.title"
placeholder="e.g., Cooperative Game Development Fundamentals"
required
:readonly="selectedSeriesId"
class="w-full"
/>
<p class="help-text">
{{
selectedSeriesId
? "From selected series"
: "Descriptive name for the entire series"
}}
</p>
</div>
<div class="field">
<label>
Series Description <span class="required">*</span>
</label>
<UTextarea
v-model="eventForm.series.description"
placeholder="Describe what the series covers and its goals"
required
:rows="3"
:readonly="selectedSeriesId"
class="w-full"
/>
<p class="help-text">
{{
selectedSeriesId
? "From selected series"
: "Describe what the series covers and its goals"
}}
</p>
</div>
<div v-if="selectedSeriesId" class="note-box">
<strong>Note:</strong> This event will be added to the existing
"{{ eventForm.series.title }}" series.
</div>
</div>
</div>
</div>
<!-- Event Agenda -->
<div class="form-section">
<h2 class="section-heading">Event Agenda</h2>
<div class="agenda-items">
<div
v-for="(item, index) in eventForm.agenda"
:key="index"
class="agenda-row"
>
<UInput
v-model="eventForm.agenda[index]"
placeholder="Enter agenda item (e.g., 'Introduction and welcome - 10 mins')"
class="w-full"
/>
<button
type="button"
@click="removeAgendaItem(index)"
class="link-btn link-btn-danger"
>
<Icon name="heroicons:trash" class="w-4 h-4" />
</button>
</div>
<button
type="button"
@click="addAgendaItem"
class="btn add-agenda-btn"
>
+ Add Agenda Item
</button>
</div>
<p class="help-text">
Add agenda items to help attendees know what to expect during the
event
</p>
</div>
<!-- Event Settings -->
<div class="form-section">
<h2 class="section-heading">Event Settings</h2>
<div class="form-grid">
<div class="check-group">
<label class="check-label">
<input v-model="eventForm.isOnline" type="checkbox" />
<div>
<strong>Online Event</strong>
<span class="help-text">
Event will be conducted virtually
</span>
</div>
</label>
<label class="check-label">
<input
v-model="eventForm.registrationRequired"
type="checkbox"
/>
<div>
<strong>Registration Required</strong>
<span class="help-text">
Attendees must register before attending
</span>
</div>
</label>
</div>
<div class="check-group">
<label class="check-label">
<input v-model="eventForm.isVisible" type="checkbox" />
<div>
<strong>Visible on Public Calendar</strong>
<span class="help-text">
Event will appear on the public events page
</span>
</div>
</label>
<label class="check-label">
<input v-model="eventForm.isCancelled" type="checkbox" />
<div>
<strong>Event Cancelled</strong>
<span class="help-text"> Mark this event as cancelled </span>
</div>
</label>
</div>
</div>
</div>
<!-- Cancellation Message (conditional) -->
<div v-if="eventForm.isCancelled" class="form-section">
<div class="field">
<label>Cancellation Message</label>
<UTextarea
v-model="eventForm.cancellationMessage"
placeholder="Explain why the event was cancelled and any next steps..."
:rows="3"
color="error"
class="w-full"
/>
<p class="help-text">
This message will be displayed to users viewing the event page
</p>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<NuxtLink to="/admin/events" class="btn"> Cancel </NuxtLink>
<div class="form-actions-right">
<button
v-if="!editingEvent"
type="button"
@click="saveAndCreateAnother"
:disabled="creating"
class="btn"
>
{{ creating ? "Saving..." : "Save & Create Another" }}
</button>
<button type="submit" :disabled="creating" class="btn btn-primary">
{{
creating
? "Saving..."
: editingEvent
? "Update Event"
: "Create Event"
}}
</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script setup>
definePageMeta({
layout: "admin",
middleware: "admin",
});
const route = useRoute();
const router = useRouter();
const creating = ref(false);
const editingEvent = ref(null);
const showSuccessMessage = ref(false);
const formErrors = ref([]);
const fieldErrors = ref({});
const selectedSeriesId = ref(null);
const availableSeries = ref([]);
const availableTags = ref([]);
const tagOptions = computed(() =>
availableTags.value.map((t) => ({ label: t.label, value: t.slug }))
);
const eventForm = reactive({
title: "",
description: "",
content: "",
featureImage: null,
startDate: "",
endDate: "",
eventType: "community",
location: "",
isOnline: true,
isVisible: true,
isCancelled: false,
cancellationMessage: "",
targetCircles: [],
tags: [],
maxAttendees: "",
registrationRequired: false,
registrationDeadline: "",
agenda: [],
tickets: {
enabled: false,
public: {
available: false,
name: "Public Ticket",
description: "",
price: 0,
quantity: null,
earlyBirdPrice: null,
earlyBirdDeadline: "",
},
},
series: {
isSeriesEvent: false,
id: "",
title: "",
description: "",
},
});
// Format a Date/ISO value into a datetime-local string using local-time components.
// `toISOString().slice(0,16)` drifts by the browser's UTC offset on edit round-trip.
const formatForDatetimeLocal = (value) => {
if (!value) return "";
const d = new Date(value);
if (isNaN(d.getTime())) return "";
const pad = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
// Agenda management functions
const addAgendaItem = () => {
eventForm.agenda.push("");
};
const removeAgendaItem = (index) => {
eventForm.agenda.splice(index, 1);
};
// Load available series and tags
onMounted(async () => {
try {
const [seriesResponse, tagsResponse] = await Promise.all([
$fetch("/api/admin/series"),
$fetch("/api/tags"),
]);
availableSeries.value = seriesResponse;
availableTags.value = tagsResponse.tags || [];
} catch (error) {
console.error("Failed to load form data:", error);
}
});
// Handle series selection
const onSeriesSelect = () => {
console.log(
"onSeriesSelect called, selectedSeriesId:",
selectedSeriesId.value,
);
console.log("availableSeries:", availableSeries.value);
if (selectedSeriesId.value) {
const series = availableSeries.value.find(
(s) => s.id === selectedSeriesId.value,
);
console.log("Found series:", series);
if (series) {
eventForm.series.id = series.id;
eventForm.series.title = series.title;
eventForm.series.description = series.description;
console.log("Updated eventForm.series:", eventForm.series);
}
} else {
// Reset series form when no series is selected
eventForm.series.id = "";
eventForm.series.title = "";
eventForm.series.description = "";
}
};
// Check if we're editing an event
if (route.query.edit) {
try {
const response = await $fetch(`/api/admin/events/${route.query.edit}`);
const event = response.data;
if (event) {
editingEvent.value = event;
Object.assign(eventForm, {
title: event.title,
description: event.description,
content: event.content || "",
featureImage: event.featureImage || null,
startDate: formatForDatetimeLocal(event.startDate),
endDate: formatForDatetimeLocal(event.endDate),
eventType: event.eventType,
location: event.location || "",
isOnline: event.isOnline,
isVisible: event.isVisible !== undefined ? event.isVisible : true,
isCancelled: event.isCancelled || false,
cancellationMessage: event.cancellationMessage || "",
targetCircles: event.targetCircles || [],
tags: event.tags || [],
maxAttendees: event.maxAttendees || "",
registrationRequired: event.registrationRequired,
registrationDeadline: formatForDatetimeLocal(event.registrationDeadline),
agenda: event.agenda || [],
tickets: event.tickets || {
enabled: false,
public: {
available: false,
name: "Public Ticket",
description: "",
price: 0,
quantity: null,
earlyBirdPrice: null,
earlyBirdDeadline: "",
},
},
series: event.series || {
isSeriesEvent: false,
id: "",
title: "",
description: "",
},
});
if (event.tickets?.public?.earlyBirdDeadline) {
eventForm.tickets.public.earlyBirdDeadline = formatForDatetimeLocal(
event.tickets.public.earlyBirdDeadline,
);
}
}
} catch (error) {
console.error("Failed to load event for editing:", error);
}
}
// Check if we're duplicating an event
if (route.query.duplicate && process.client) {
const duplicateData = sessionStorage.getItem("duplicateEventData");
if (duplicateData) {
try {
const data = JSON.parse(duplicateData);
Object.assign(eventForm, data);
sessionStorage.removeItem("duplicateEventData");
} catch (error) {
console.error("Failed to load duplicate event data:", error);
}
}
}
// Check if we're creating a series event
if (route.query.series && process.client) {
const seriesData = sessionStorage.getItem("seriesEventData");
if (seriesData) {
try {
const data = JSON.parse(seriesData);
Object.assign(eventForm, data);
sessionStorage.removeItem("seriesEventData");
} catch (error) {
console.error("Failed to load series event data:", error);
}
}
}
const validateForm = () => {
formErrors.value = [];
fieldErrors.value = {};
// Required field validation
if (!eventForm.title.trim()) {
formErrors.value.push("Event title is required");
fieldErrors.value.title = "Please enter an event title";
}
if (!eventForm.description.trim()) {
formErrors.value.push("Event description is required");
fieldErrors.value.description =
"Please provide a description for your event";
}
if (!eventForm.startDate) {
formErrors.value.push("Start date and time is required");
fieldErrors.value.startDate = "Please select when the event starts";
}
if (!eventForm.endDate) {
formErrors.value.push("End date and time is required");
fieldErrors.value.endDate = "Please select when the event ends";
}
if (!eventForm.location.trim()) {
formErrors.value.push("Location is required");
fieldErrors.value.location =
"Please enter a location (URL or Slack channel)";
}
// Date validation
if (eventForm.startDate && eventForm.endDate) {
const startDate = new Date(eventForm.startDate);
const endDate = new Date(eventForm.endDate);
if (startDate >= endDate) {
formErrors.value.push("End date must be after start date");
fieldErrors.value.endDate = "End date must be after the start date";
}
if (startDate < new Date()) {
formErrors.value.push("Start date cannot be in the past");
fieldErrors.value.startDate = "Event cannot start in the past";
}
}
// Location format validation
if (eventForm.location.trim()) {
const urlPattern = /^https?:\/\/.+/;
const slackPattern = /^#[a-zA-Z0-9-_]+$/;
if (
!urlPattern.test(eventForm.location) &&
!slackPattern.test(eventForm.location)
) {
formErrors.value.push(
"Location must be a valid URL or Slack channel (starting with #)",
);
fieldErrors.value.location =
"Enter a video conference link (https://...) or Slack channel (#channel-name)";
}
}
// Registration deadline validation
if (eventForm.registrationDeadline && eventForm.startDate) {
const regDeadline = new Date(eventForm.registrationDeadline);
const startDate = new Date(eventForm.startDate);
if (regDeadline >= startDate) {
formErrors.value.push(
"Registration deadline must be before the event starts",
);
fieldErrors.value.registrationDeadline =
"Registration must close before the event starts";
}
}
return formErrors.value.length === 0;
};
const saveEvent = async (redirect = true) => {
if (!validateForm()) {
// Scroll to top to show errors
window.scrollTo({ top: 0, behavior: "smooth" });
return false;
}
creating.value = true;
try {
// If this is a series event and not using an existing series, create the standalone series first
if (eventForm.series.isSeriesEvent && selectedSeriesId.value) {
// Series will be handled by the selected series
} else if (eventForm.series.isSeriesEvent) {
// For now, series creation requires selecting an existing series
// Individual series creation is handled through the series management page
}
if (editingEvent.value) {
await $fetch(`/api/admin/events/${editingEvent.value._id}`, {
method: "PUT",
body: eventForm,
});
} else {
await $fetch("/api/admin/events", {
method: "POST",
body: eventForm,
});
}
showSuccessMessage.value = true;
setTimeout(() => {
showSuccessMessage.value = false;
}, 5000);
if (redirect) {
setTimeout(() => {
router.push("/admin/events");
}, 1500);
}
return true;
} catch (error) {
console.error("Failed to save event:", error);
formErrors.value = [
`Failed to ${editingEvent.value ? "update" : "create"} event: ${error.data?.statusMessage || error.message}`,
];
window.scrollTo({ top: 0, behavior: "smooth" });
return false;
} finally {
creating.value = false;
}
};
const saveAndCreateAnother = async () => {
const success = await saveEvent(false);
if (success) {
// Reset form for new event
Object.assign(eventForm, {
title: "",
description: "",
content: "",
featureImage: null,
startDate: "",
endDate: "",
eventType: "community",
location: "",
isOnline: true,
isVisible: true,
isCancelled: false,
cancellationMessage: "",
targetCircles: [],
tags: [],
maxAttendees: "",
registrationRequired: false,
registrationDeadline: "",
agenda: [],
tickets: {
enabled: false,
public: {
available: false,
name: "Public Ticket",
description: "",
price: 0,
quantity: null,
earlyBirdPrice: null,
earlyBirdDeadline: "",
},
},
series: {
isSeriesEvent: false,
id: "",
title: "",
description: "",
},
});
// Clear any existing errors
formErrors.value = [];
fieldErrors.value = {};
// Scroll to top
window.scrollTo({ top: 0, behavior: "smooth" });
}
};
</script>
<style scoped>
.create-form {
max-width: 800px;
}
.page-header {
padding: 28px 28px 20px;
border-bottom: 1px dashed var(--border);
}
.page-header h1 {
font-family: "Brygada 1918", serif;
font-size: 24px;
font-weight: 500;
color: var(--text-bright);
line-height: 1.2;
margin-bottom: 4px;
}
.page-header p {
font-size: 12px;
color: var(--text-dim);
}
.back-link {
font-size: 11px;
color: var(--text-faint);
text-decoration: none;
margin-bottom: 8px;
display: inline-block;
letter-spacing: 0.02em;
}
.back-link:hover {
color: var(--candle);
text-decoration: none;
}
.form-body {
padding: 24px 28px;
}
.section-heading {
font-family: "Brygada 1918", serif;
font-size: 16px;
font-weight: 500;
color: var(--text-bright);
padding-bottom: 10px;
border-bottom: 1px dashed var(--border);
margin-bottom: 16px;
}
.error-box {
padding: 16px 20px;
border: 1px dashed var(--ember);
margin-bottom: 20px;
font-size: 12px;
color: var(--ember);
}
.error-box h3 {
font-weight: 500;
margin-bottom: 8px;
}
.error-box ul {
list-style: none;
padding: 0;
margin: 0;
}
.error-box li::before {
content: "\2022 ";
}
.success-box {
padding: 16px 20px;
border: 1px dashed var(--candle);
margin-bottom: 20px;
font-size: 12px;
color: var(--candle);
}
.help-text {
font-size: 11px;
color: var(--text-faint);
margin-top: 4px;
}
.field-error {
font-size: 11px;
color: var(--ember);
margin-top: 4px;
}
.required {
color: var(--ember);
}
.check-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.check-label {
display: flex;
gap: 10px;
align-items: flex-start;
cursor: pointer;
margin-bottom: 8px;
}
.check-label input[type="checkbox"] {
width: auto;
flex-shrink: 0;
margin-top: 2px;
accent-color: var(--candle);
}
.check-label strong {
display: block;
font-size: 13px;
color: var(--text);
font-weight: 500;
}
.check-label .help-text {
margin-top: 0;
}
.form-section {
margin-bottom: 32px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20px;
border-top: 1px dashed var(--border);
}
.form-actions-right {
display: flex;
gap: 8px;
}
.nested-section {
padding: 16px 20px;
border: 1px dashed var(--border);
margin-top: 8px;
}
.note-box {
padding: 12px 16px;
border: 1px dashed var(--candle);
font-size: 12px;
color: var(--candle);
margin-top: 12px;
}
.series-select-row {
display: flex;
gap: 8px;
align-items: flex-start;
}
.series-select-row .w-full {
flex: 1;
}
.agenda-items {
display: flex;
flex-direction: column;
gap: 8px;
}
.agenda-row {
display: flex;
gap: 8px;
align-items: center;
}
.agenda-row .w-full {
flex: 1;
}
.link-btn {
background: none;
border: none;
color: var(--candle);
cursor: pointer;
font-family: 'Commit Mono', monospace;
font-size: 11px;
padding: 2px 6px;
}
.link-btn:hover {
text-decoration: underline;
}
.link-btn-danger {
color: var(--ember);
}
.add-agenda-btn {
align-self: flex-start;
color: var(--candle);
border-color: var(--candle);
border-style: dashed;
}
.btn:disabled,
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 768px) {
.page-header {
padding: 24px 20px 16px;
}
.form-body {
padding: 20px;
}
.form-grid {
grid-template-columns: 1fr;
}
.series-select-row {
flex-direction: column;
}
}
</style>