Files
bibdle/src/lib/components/SubmitVerse.svelte
T

996 lines
25 KiB
Svelte

<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { fly } from "svelte/transition";
import { bibleBooks } from "$lib/types/bible";
let {
isLoggedIn = false,
anonymousId = "",
localDate = "",
}: {
isLoggedIn?: boolean;
anonymousId?: string;
localDate?: string;
} = $props();
// ── Status (logged-in state) ───────────────────────────────────────────
type Status = {
canSubmit: boolean;
cooldownEndsAt: number | null;
lastSubmission: { scheduledDate: string; reference: string } | null;
upcoming: { scheduledDate: string; reference: string }[];
};
let status = $state<Status | null>(null);
let statusError = $state<string | null>(null);
// ── Bible structure (Book → chapters → verse counts) ──────────────────
type Structure = { bookId: string; chapters: number[] }[];
let structure = $state<Structure | null>(null);
// ── Selector state ─────────────────────────────────────────────────────
let selectedBookId = $state<string>("");
let selectedChapter = $state<number>(0);
let selectedVerse = $state<number>(0);
// ── Preview state ──────────────────────────────────────────────────────
type Preview = {
windowVerses: string[];
reference: string;
bookId: string;
selectedVerse: number;
};
let preview = $state<Preview | null>(null);
let previewLoading = $state(false);
// ── Submission state ───────────────────────────────────────────────────
let submitting = $state(false);
let submitError = $state<string | null>(null);
let submitResult = $state<{ scheduledDate: string; reference: string } | null>(
null,
);
// ── UI state ───────────────────────────────────────────────────────────
let expanded = $state(false);
let countdownText = $state("");
let countdownId: number | null = null;
// Book list grouped by Testament for the <select> optgroups.
const oldBooks = $derived(bibleBooks.filter((b) => b.testament === "old"));
const newBooks = $derived(bibleBooks.filter((b) => b.testament === "new"));
// Chapter list for the currently selected book (1..N).
const chapterCount = $derived(
structure && selectedBookId
? structure.find((b) => b.bookId === selectedBookId)?.chapters.length ??
0
: 0,
);
// Verse count for the currently selected chapter.
const verseCount = $derived(
structure && selectedBookId && selectedChapter
? structure.find((b) => b.bookId === selectedBookId)?.chapters[
selectedChapter - 1
] ?? 0
: 0,
);
// Whether the user is currently on cooldown (only meaningful when logged in).
const onCooldown = $derived(
!!status && !status.canSubmit && status.cooldownEndsAt !== null,
);
// Debounce handle for the live preview fetch.
let previewTimer: ReturnType<typeof setTimeout> | null = null;
function trackEvent(name: string) {
try {
(window as any).rybbit?.event?.(name);
} catch {
/* noop */
}
}
function fmtDateLong(dateStr: string): string {
// dateStr is YYYY-MM-DD; format as "August 24, 2026" in UTC to avoid drift.
const d = new Date(dateStr + "T00:00:00Z");
return d.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC",
});
}
function fmtDateVague(dateStr: string): string {
// dateStr is YYYY-MM-DD. Return a deliberately vague timeframe instead of
// revealing the exact scheduled date.
const scheduled = new Date(dateStr + "T00:00:00Z");
const now = new Date();
const today = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()),
);
const diffDays = Math.round(
(scheduled.getTime() - today.getTime()) / (1000 * 60 * 60 * 24),
);
if (diffDays <= 14) return "a few days from now";
if (diffDays <= 60) return "a few weeks from now";
return "within the next few months";
}
async function loadStatus() {
if (!isLoggedIn || !localDate) return;
try {
const res = await fetch(
`/api/submit-verse/status?localDate=${encodeURIComponent(localDate)}`,
);
if (res.status === 401) {
status = null;
return;
}
if (!res.ok) {
statusError = "Couldn't load submission status.";
return;
}
status = await res.json();
} catch {
statusError = "Couldn't load submission status.";
}
}
async function loadStructure() {
if (structure) return;
try {
const res = await fetch("/api/bible/structure");
if (!res.ok) return;
const data = await res.json();
structure = data;
// Default selection: first book, chapter 1, verse 1.
if (data.length > 0 && !selectedBookId) {
selectedBookId = data[0].bookId;
selectedChapter = 1;
selectedVerse = 1;
}
} catch {
/* noop */
}
}
function fetchPreview() {
if (previewTimer) clearTimeout(previewTimer);
previewTimer = setTimeout(async () => {
if (!selectedBookId || !selectedChapter || !selectedVerse) {
preview = null;
return;
}
previewLoading = true;
try {
const res = await fetch(
`/api/verse-window?bookId=${encodeURIComponent(
selectedBookId,
)}&chapter=${selectedChapter}&verse=${selectedVerse}`,
);
if (res.ok) {
preview = await res.json();
}
} catch {
/* noop */
} finally {
previewLoading = false;
}
}, 250);
}
function onBookChange() {
selectedChapter = 1;
selectedVerse = 1;
submitError = null;
fetchPreview();
}
function onChapterChange() {
selectedVerse = 1;
submitError = null;
fetchPreview();
}
function onVerseChange() {
submitError = null;
fetchPreview();
}
async function handleSubmit() {
if (!isLoggedIn) return;
submitting = true;
submitError = null;
try {
const res = await fetch("/api/submit-verse", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
bookId: selectedBookId,
chapter: selectedChapter,
verse: selectedVerse,
localDate,
}),
});
const data = await res.json();
if (!res.ok) {
if (res.status === 429 && data?.cooldownEndsAt) {
// Refresh status so the cooldown UI kicks in.
await loadStatus();
submitError = "You're on cooldown — try again later.";
} else {
submitError = data?.error ?? "Submission failed.";
}
return;
}
submitResult = {
scheduledDate: data.scheduledDate,
reference: data.reference,
};
trackEvent("Submit a verse");
// Refresh status so the cooldown reflects the new submission.
await loadStatus();
} catch {
submitError = "Network error — please try again.";
} finally {
submitting = false;
}
}
function updateCountdown() {
if (!status?.cooldownEndsAt) {
countdownText = "";
return;
}
const diff = status.cooldownEndsAt - Date.now();
if (diff <= 0) {
countdownText = "";
// Cooldown elapsed — refresh status once.
loadStatus();
if (countdownId) {
clearInterval(countdownId);
countdownId = null;
}
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
countdownText =
(days > 0 ? `${days}d ` : "") +
`${hours.toString().padStart(2, "0")}h ${minutes
.toString()
.padStart(2, "0")}m`;
}
onMount(() => {
if (isLoggedIn) {
loadStatus();
loadStructure();
}
});
// Start/stop the countdown timer as cooldown state changes.
$effect(() => {
if (onCooldown && status?.cooldownEndsAt && !countdownId) {
updateCountdown();
countdownId = window.setInterval(updateCountdown, 1000);
} else if (!onCooldown && countdownId) {
clearInterval(countdownId);
countdownId = null;
countdownText = "";
}
});
// (Re)load status + structure when the user logs in.
$effect(() => {
if (isLoggedIn) {
loadStatus();
loadStructure();
} else {
status = null;
structure = null;
expanded = false;
submitResult = null;
}
});
onDestroy(() => {
if (countdownId) clearInterval(countdownId);
if (previewTimer) clearTimeout(previewTimer);
});
</script>
<div class="signin-prompt submit-verse-wrap">
{#if submitResult}
<!-- ── Confirmation state (replaces the panel after success) ──────── -->
<div class="confirm-card" in:fly={{ y: -8, duration: 220 }}>
<p class="confirm-title">
✅ Your verse is scheduled for
<strong>{fmtDateVague(submitResult.scheduledDate)}</strong>
</p>
<p class="confirm-ref">{submitResult.reference}</p>
<button
type="button"
class="submit-another-btn"
onclick={() => {
submitResult = null;
expanded = false;
}}
>
Done
</button>
</div>
{:else if !isLoggedIn}
<!-- ── State 1: logged out — sign-in dropdown ─────────────────────── -->
<button
type="button"
class="progress-btn w-full"
aria-expanded={expanded}
onclick={() => (expanded = !expanded)}
data-umami-event="Submit a verse (logged out)"
>
<span>✨ Submit a verse</span>
<svg
class="progress-chev"
class:open={expanded}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
{#if expanded}
<p class="signin-text text-gray-800 dark:text-gray-300">
Sign in to submit a verse for a future day
</p>
<form method="POST" action="/auth/apple" class="w-full">
<input type="hidden" name="anonymousId" value={anonymousId} />
<button
type="submit"
class="apple-signin-btn"
data-umami-event="Sign in with Apple"
>
<svg class="apple-icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"
/>
</svg>
Sign in with Apple
</button>
</form>
<form method="POST" action="/auth/google" class="w-full">
<input type="hidden" name="anonymousId" value={anonymousId} />
<button
type="submit"
class="google-signin-btn"
data-umami-event="Sign in with Google"
>
<svg
class="google-icon"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</button>
</form>
{/if}
{:else if onCooldown}
<!-- ── State 2: logged in, cooldown active — disabled + countdown on button ─── -->
<button
type="button"
class="progress-btn w-full progress-btn-disabled"
disabled
aria-disabled="true"
>
<!-- <span>✨ Submit a verse</span> -->
{#if countdownText}
<span class="countdown-inline" aria-hidden="true"
>⏳ {countdownText}</span
>
{:else}
<span class="lock-icon" aria-hidden="true">⏳</span>
{/if}
</button>
{:else}
<!-- ── State 3: logged in, can submit — selector + preview ────────── -->
<button
type="button"
class="progress-btn w-full"
aria-expanded={expanded}
onclick={() => (expanded = !expanded)}
data-umami-event="Submit a verse (logged in)"
>
<span>✨ Submit a verse</span>
<svg
class="progress-chev"
class:open={expanded}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
{#if expanded}
<div class="submit-panel" in:fly={{ y: -8, duration: 220 }}>
{#if statusError}
<p class="panel-error">{statusError}</p>
{/if}
{#if !structure}
<p class="panel-hint">Loading Bible structure…</p>
{:else}
<div class="select-row">
<label class="select-label">
<span class="select-cap">Book</span>
<select
class="cascading-select"
value={selectedBookId}
onchange={(e) => {
selectedBookId = e.currentTarget.value;
onBookChange();
}}
>
<optgroup label="Old Testament">
{#each oldBooks as book (book.id)}
<option value={book.id}>{book.name}</option>
{/each}
</optgroup>
<optgroup label="New Testament">
{#each newBooks as book (book.id)}
<option value={book.id}>{book.name}</option>
{/each}
</optgroup>
</select>
</label>
<label class="select-label">
<span class="select-cap">Chapter</span>
<select
class="cascading-select"
value={selectedChapter}
onchange={(e) => {
selectedChapter = Number(e.currentTarget.value);
onChapterChange();
}}
>
{#if chapterCount === 0}
<option value={0}>—</option>
{:else}
{#each Array(chapterCount) as _, i (i)}
<option value={i + 1}>{i + 1}</option>
{/each}
{/if}
</select>
</label>
<label class="select-label">
<span class="select-cap">Verse</span>
<select
class="cascading-select"
value={selectedVerse}
onchange={(e) => {
selectedVerse = Number(e.currentTarget.value);
onVerseChange();
}}
>
{#if verseCount === 0}
<option value={0}>—</option>
{:else}
{#each Array(verseCount) as _, i (i)}
<option value={i + 1}>{i + 1}</option>
{/each}
{/if}
</select>
</label>
</div>
<!-- Live 3-verse preview -->
<div class="preview-card">
{#if previewLoading}
<p class="preview-hint">Loading preview…</p>
{:else if preview}
<p class="preview-ref">{preview.reference}</p>
<div class="preview-verses">
{#each preview.windowVerses as verseText, i (i)}
<p class="preview-verse">{verseText}</p>
{/each}
</div>
{:else}
<p class="preview-hint">Select a verse to preview.</p>
{/if}
</div>
{#if submitError}
<p class="panel-error">{submitError}</p>
{/if}
<button
type="button"
class="submit-btn"
disabled={submitting ||
!selectedBookId ||
!selectedChapter ||
!selectedVerse ||
!preview}
onclick={handleSubmit}
>
{submitting ? "Scheduling…" : "Submit verse"}
</button>
{#if status?.upcoming && status.upcoming.length > 0}
<p class="upcoming-text">
Your upcoming verse{status.upcoming.length > 1
? "s"
: ""}:
{#each status.upcoming as u, i (i)}
{#if i > 0}<span class="upcoming-sep">·</span>{/if}
<span class="upcoming-item"
>{u.reference} ({fmtDateVague(u.scheduledDate)})</span
>
{/each}
</p>
{/if}
{/if}
</div>
{/if}
{/if}
</div>
<style>
.submit-verse-wrap {
gap: 0.75rem;
}
/* Reuse WinScreen's neobrutalist .progress-btn base (scoped here too). */
:global(.submit-verse-wrap .progress-btn) {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.85rem 1.25rem;
background: #fff;
color: #111;
border: 2px solid #000;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: 700;
text-decoration: none;
cursor: pointer;
box-shadow: 6px 6px 0 0 #000;
transition:
transform 80ms ease,
box-shadow 80ms ease;
}
:global(.submit-verse-wrap .progress-btn:hover) {
transform: translate(-2px, -2px);
box-shadow: 8px 8px 0 0 #000;
}
:global(.submit-verse-wrap .progress-btn:active) {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 0 #000;
}
:global(.submit-verse-wrap .progress-chev) {
width: 1.25rem;
height: 1.25rem;
transition: transform 200ms ease;
}
:global(.submit-verse-wrap .progress-chev.open) {
transform: rotate(180deg);
}
@media (prefers-color-scheme: dark) {
:global(.submit-verse-wrap .progress-btn) {
background: #111827;
color: #f9fafb;
border-color: #fff;
box-shadow: 6px 6px 0 0 #fff;
}
:global(.submit-verse-wrap .progress-btn:hover) {
box-shadow: 8px 8px 0 0 #fff;
}
:global(.submit-verse-wrap .progress-btn:active) {
box-shadow: 2px 2px 0 0 #fff;
}
}
/* Disabled (cooldown) variant. */
:global(.submit-verse-wrap .progress-btn-disabled) {
opacity: 0.55;
cursor: not-allowed;
box-shadow: 4px 4px 0 0 #000;
}
:global(.submit-verse-wrap .progress-btn-disabled:hover),
:global(.submit-verse-wrap .progress-btn-disabled:active) {
transform: none;
box-shadow: 4px 4px 0 0 #000;
}
@media (prefers-color-scheme: dark) {
:global(.submit-verse-wrap .progress-btn-disabled) {
box-shadow: 4px 4px 0 0 #fff;
}
:global(.submit-verse-wrap .progress-btn-disabled:hover),
:global(.submit-verse-wrap .progress-btn-disabled:active) {
box-shadow: 4px 4px 0 0 #fff;
}
}
.lock-icon {
font-size: 1rem;
}
.countdown-inline {
font-size: 0.85rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0.01em;
opacity: 0.85;
}
.signin-text {
font-size: 0.85rem;
text-align: center;
font-weight: 500;
}
/* ── Apple / Google sign-in buttons (mirror WinScreen) ── */
:global(.submit-verse-wrap .apple-signin-btn),
:global(.submit-verse-wrap .google-signin-btn) {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.6rem 1rem;
width: 100%;
background: #000;
color: #fff;
border-radius: 0.5rem;
font-size: 0.95rem;
font-weight: 600;
border: none;
cursor: pointer;
transition:
background 150ms ease,
transform 80ms ease;
}
:global(.submit-verse-wrap .apple-signin-btn) {
margin-bottom: 0.6rem;
}
:global(.submit-verse-wrap .apple-signin-btn:hover),
:global(.submit-verse-wrap .google-signin-btn:hover) {
background: #222;
transform: translateY(-1px);
}
:global(.submit-verse-wrap .apple-signin-btn:active),
:global(.submit-verse-wrap .google-signin-btn:active) {
background: #111;
transform: scale(0.98);
}
@media (prefers-color-scheme: dark) {
:global(.submit-verse-wrap .apple-signin-btn),
:global(.submit-verse-wrap .google-signin-btn) {
background: #fff;
color: #000;
}
:global(.submit-verse-wrap .apple-signin-btn:hover),
:global(.submit-verse-wrap .google-signin-btn:hover) {
background: #e5e5e5;
}
:global(.submit-verse-wrap .apple-signin-btn:active),
:global(.submit-verse-wrap .google-signin-btn:active) {
background: #ccc;
}
}
:global(.submit-verse-wrap .apple-icon),
:global(.submit-verse-wrap .google-icon) {
width: 1.1rem;
height: 1.1rem;
flex-shrink: 0;
}
/* ── Submit panel ── */
.submit-panel {
display: flex;
flex-direction: column;
gap: 0.85rem;
width: 100%;
padding: 1rem;
background: oklch(94% 0.028 298.626);
border: 2px solid #000;
border-radius: 0.75rem;
box-shadow: 6px 6px 0 0 #000;
}
@media (prefers-color-scheme: dark) {
.submit-panel {
background: oklch(22% 0.025 298.626);
border-color: #fff;
box-shadow: 6px 6px 0 0 #fff;
}
}
.panel-hint,
.preview-hint {
font-size: 0.85rem;
color: #6b7280;
text-align: center;
}
@media (prefers-color-scheme: dark) {
.panel-hint,
.preview-hint {
color: #9ca3af;
}
}
.panel-error {
font-size: 0.85rem;
color: #b91c1c;
text-align: center;
font-weight: 600;
}
@media (prefers-color-scheme: dark) {
.panel-error {
color: #f87171;
}
}
.select-row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.select-label {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
min-width: 5rem;
}
.select-cap {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6b7280;
}
@media (prefers-color-scheme: dark) {
.select-cap {
color: #9ca3af;
}
}
.cascading-select {
width: 100%;
padding: 0.5rem 0.6rem;
border: 2px solid #000;
border-radius: 0.5rem;
background: #fff;
color: #111;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
.cascading-select {
background: #111827;
color: #f9fafb;
border-color: #fff;
}
}
/* ── Preview card ── */
.preview-card {
padding: 0.75rem;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.5rem;
background: rgba(255, 255, 255, 0.5);
}
@media (prefers-color-scheme: dark) {
.preview-card {
border-color: rgba(255, 255, 255, 0.15);
background: rgba(0, 0, 0, 0.25);
}
}
.preview-ref {
font-size: 0.8rem;
font-weight: 700;
color: #374151;
margin-bottom: 0.4rem;
}
@media (prefers-color-scheme: dark) {
.preview-ref {
color: #d1d5db;
}
}
.preview-verses {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.preview-verse {
font-size: 0.82rem;
line-height: 1.45;
color: #4b5563;
font-style: italic;
}
@media (prefers-color-scheme: dark) {
.preview-verse {
color: #9ca3af;
}
}
/* ── Submit button (neobrutalist) ── */
.submit-btn {
padding: 0.7rem 1rem;
background: #16a34a;
color: #fff;
border: 2px solid #000;
border-radius: 0.6rem;
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
box-shadow: 4px 4px 0 0 #000;
transition:
transform 80ms ease,
box-shadow 80ms ease,
background-color 120ms ease;
}
.submit-btn:hover:not(:disabled) {
transform: translate(-2px, -2px);
box-shadow: 6px 6px 0 0 #000;
background: #15803d;
}
.submit-btn:active:not(:disabled) {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 0 #000;
}
.submit-btn:disabled {
opacity: 0.55;
cursor: not-allowed;
}
@media (prefers-color-scheme: dark) {
.submit-btn {
border-color: #fff;
box-shadow: 4px 4px 0 0 #fff;
}
.submit-btn:hover:not(:disabled) {
box-shadow: 6px 6px 0 0 #fff;
}
.submit-btn:active:not(:disabled) {
box-shadow: 2px 2px 0 0 #fff;
}
}
/* ── Upcoming list ── */
.upcoming-text {
font-size: 0.72rem;
color: #6b7280;
text-align: center;
line-height: 1.5;
}
@media (prefers-color-scheme: dark) {
.upcoming-text {
color: #9ca3af;
}
}
.upcoming-item {
font-weight: 600;
}
.upcoming-sep {
margin: 0 0.25rem;
opacity: 0.6;
}
/* ── Confirmation card ── */
.confirm-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
width: 100%;
padding: 1.1rem;
background: oklch(94% 0.028 298.626);
border: 2px solid #000;
border-radius: 0.75rem;
box-shadow: 6px 6px 0 0 #000;
text-align: center;
}
@media (prefers-color-scheme: dark) {
.confirm-card {
background: oklch(22% 0.025 298.626);
border-color: #fff;
box-shadow: 6px 6px 0 0 #fff;
}
}
.confirm-title {
font-size: 1rem;
font-weight: 700;
color: #111;
}
@media (prefers-color-scheme: dark) {
.confirm-title {
color: #f9fafb;
}
}
.confirm-ref {
font-size: 0.85rem;
font-weight: 600;
color: #374151;
}
@media (prefers-color-scheme: dark) {
.confirm-ref {
color: #d1d5db;
}
}
.submit-another-btn {
padding: 0.5rem 1.1rem;
background: #fff;
color: #111;
border: 2px solid #000;
border-radius: 0.5rem;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
box-shadow: 3px 3px 0 0 #000;
transition:
transform 80ms ease,
box-shadow 80ms ease;
}
.submit-another-btn:hover {
transform: translate(-1px, -1px);
box-shadow: 4px 4px 0 0 #000;
}
.submit-another-btn:active {
transform: translate(1px, 1px);
box-shadow: 2px 2px 0 0 #000;
}
@media (prefers-color-scheme: dark) {
.submit-another-btn {
background: #111827;
color: #f9fafb;
border-color: #fff;
box-shadow: 3px 3px 0 0 #fff;
}
.submit-another-btn:hover {
box-shadow: 4px 4px 0 0 #fff;
}
.submit-another-btn:active {
box-shadow: 2px 2px 0 0 #fff;
}
}
</style>