feat(ui): elegant wedding white/silver/gold redesign

Reskin the whole app via design tokens in tailwind-theme.css (remapped
color ramps) plus a define-once component layer in lib/styles/components.css
(.btn, .card, .input, .chip, .badge, .sheet, …). Buttons are muted/outlined
gold rather than flat fills. Self-host Inter + Fraunces (woff2) under the
existing font-src 'self' CSP. Restyle the shared components and the account,
admin, host, join, recover and upload screens against the new tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-18 17:28:42 +02:00
parent 5546fb82e6
commit f243bfe89a
22 changed files with 696 additions and 545 deletions

View File

@@ -49,7 +49,7 @@
<div class="relative -translate-y-3">
<button
onclick={() => ($uploadSheetOpen = true)}
class="relative flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg transition active:scale-95 hover:bg-blue-700"
class="relative flex h-14 w-14 items-center justify-center rounded-full border-2 border-primary-500 bg-primary-100 text-primary-700 shadow-lg transition active:scale-95 hover:bg-primary-200 dark:border-primary-500 dark:bg-primary-950/60 dark:text-primary-300"
aria-label="Hochladen"
>
<!-- Camera + plus icon -->

View File

@@ -80,10 +80,7 @@
onclick={handleConfirm}
disabled={busy}
data-testid="confirm-sheet-confirm"
class="mb-3 flex w-full items-center justify-center gap-2 rounded-xl py-3 text-sm font-semibold text-white transition disabled:opacity-60 {tone ===
'danger'
? 'bg-red-600 hover:bg-red-700 active:bg-red-700 dark:bg-red-500 dark:hover:bg-red-400 dark:active:bg-red-400'
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400'}"
class="btn btn-block mb-3 {tone === 'danger' ? 'btn-danger' : 'btn-primary'}"
>
{#if busy}
<span
@@ -98,7 +95,7 @@
onclick={onCancel}
disabled={busy}
data-testid="confirm-sheet-cancel"
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50 active:bg-gray-100 disabled:opacity-60 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
class="btn btn-secondary btn-block"
>
{cancelLabel}
</button>

View File

@@ -147,11 +147,7 @@
</button>
{/each}
<button
type="button"
onclick={onClose}
class="mt-2 w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
>
<button type="button" onclick={onClose} class="btn btn-secondary btn-block mt-2">
Abbrechen
</button>
</div>

View File

@@ -66,170 +66,168 @@
<!-- Off-screen cards are removed from the DOM entirely by the parent VirtualFeed
window-virtualizer, so this card no longer needs content-visibility — and must
not use it, since the virtualizer measures each card's real rendered height. -->
<article
class="bg-white dark:bg-gray-900"
use:longpress={{ duration: 500 }}
onlongpress={openContext}
>
<!-- Uploader row -->
<div class="flex items-center justify-between gap-3 px-4 py-3">
<div class="flex min-w-0 items-center gap-3">
<div
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold
<article class="px-3 py-2" use:longpress={{ duration: 500 }} onlongpress={openContext}>
<div
class="overflow-hidden rounded-2xl border border-gray-200/70 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
>
<!-- Uploader row -->
<div class="flex items-center justify-between gap-3 px-4 py-3">
<div class="flex min-w-0 items-center gap-3">
<div
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold
{avatarPalette(upload.uploader_name)}"
>
{initials(upload.uploader_name)}
</div>
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
{upload.uploader_name}
</p>
<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
>
{initials(upload.uploader_name)}
</div>
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
{upload.uploader_name}
</p>
<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
</div>
</div>
{#if oncontextmenu}
<!-- Desktop kebab — same actions as the mobile long-press context sheet. -->
<button
type="button"
onclick={(e) => {
e.stopPropagation();
openContext();
}}
class="rounded-full p-1 text-gray-400 hover:bg-gray-100 active:bg-gray-200 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-800 dark:active:bg-gray-800 dark:hover:text-gray-200"
aria-label="Mehr Aktionen"
>
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"
><circle cx="5" cy="12" r="2" /><circle cx="12" cy="12" r="2" /><circle
cx="19"
cy="12"
r="2"
/></svg
>
</button>
{/if}
</div>
{#if oncontextmenu}
<!-- Desktop kebab — same actions as the mobile long-press context sheet. -->
<button
type="button"
onclick={(e) => {
e.stopPropagation();
openContext();
}}
class="rounded-full p-1 text-gray-400 hover:bg-gray-100 active:bg-gray-200 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-800 dark:active:bg-gray-800 dark:hover:text-gray-200"
aria-label="Mehr Aktionen"
>
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"
><circle cx="5" cy="12" r="2" /><circle cx="12" cy="12" r="2" /><circle
cx="19"
cy="12"
r="2"
/></svg
>
</button>
{/if}
</div>
<!-- Media -->
<button
class="relative block w-full"
use:doubletap
onsingletap={() => onselect(upload)}
ondoubletap={handleDoubleTap}
onclick={(e) => {
if (e.detail === 0) onselect(upload);
}}
aria-label="Bild vergrößern"
>
<HeartBurst active={heartBurst} />
{#if isVideo(upload.mime_type)}
<div class="relative aspect-video w-full bg-gray-900">
{#if upload.thumbnail_url || upload.preview_url}
<!-- Media -->
<button
class="relative block w-full"
use:doubletap
onsingletap={() => onselect(upload)}
ondoubletap={handleDoubleTap}
onclick={(e) => {
if (e.detail === 0) onselect(upload);
}}
aria-label="Bild vergrößern"
>
<HeartBurst active={heartBurst} />
{#if isVideo(upload.mime_type)}
<div class="relative aspect-video w-full bg-gray-900">
{#if upload.thumbnail_url || upload.preview_url}
<img
src={upload.thumbnail_url ?? upload.preview_url ?? ''}
alt=""
class="h-full w-full object-cover opacity-80"
loading="lazy"
decoding="async"
/>
{/if}
<div class="absolute inset-0 flex items-center justify-center">
<span
class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white"
>
<svg class="h-7 w-7 pl-0.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</span>
</div>
</div>
{:else if mediaSrc}
<!-- Reserve the same 4/5 box the skeleton uses so the card doesn't collapse
to height 0 and reflow as images stream in. The uncropped original is one
tap away in the lightbox. -->
<div class="aspect-[4/5] w-full bg-gray-100 dark:bg-gray-800">
<img
src={upload.thumbnail_url ?? upload.preview_url ?? ''}
src={mediaSrc}
alt=""
class="h-full w-full object-cover opacity-80"
class="h-full w-full object-cover"
loading="lazy"
decoding="async"
/>
{/if}
<div class="absolute inset-0 flex items-center justify-center">
<span
class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white"
>
<svg class="h-7 w-7 pl-0.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</span>
</div>
</div>
{:else if mediaSrc}
<!-- Reserve the same 4/5 box the skeleton uses so the card doesn't collapse
to height 0 and reflow as images stream in. The uncropped original is one
tap away in the lightbox. -->
<div class="aspect-[4/5] w-full bg-gray-100 dark:bg-gray-800">
<img
src={mediaSrc}
alt=""
class="h-full w-full object-cover"
loading="lazy"
decoding="async"
/>
</div>
{:else}
<div
class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
{:else}
<div
class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
>
<svg
class="h-12 w-12 text-gray-300 dark:text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
{/if}
</button>
<!-- Actions row -->
<div class="flex items-center gap-4 px-4 py-2">
<button
onclick={() => {
vibrate(10);
onlike(upload.id);
}}
aria-pressed={upload.liked_by_me}
aria-label={upload.liked_by_me ? 'Gefällt mir nicht mehr' : 'Gefällt mir'}
class="flex items-center gap-1.5 text-sm font-medium transition-colors
{upload.liked_by_me
? 'text-red-500 dark:text-red-400'
: 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
>
<svg
class="h-12 w-12 text-gray-300 dark:text-gray-600"
class="h-5 w-5 {upload.liked_by_me ? 'fill-red-500' : ''}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
</div>
{/if}
</button>
<!-- Actions row -->
<div class="flex items-center gap-4 px-4 py-2">
<button
onclick={() => {
vibrate(10);
onlike(upload.id);
}}
aria-pressed={upload.liked_by_me}
aria-label={upload.liked_by_me ? 'Gefällt mir nicht mehr' : 'Gefällt mir'}
class="flex items-center gap-1.5 text-sm font-medium transition-colors
{upload.liked_by_me
? 'text-red-500 dark:text-red-400'
: 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
>
<svg
class="h-5 w-5 {upload.liked_by_me ? 'fill-red-500' : ''}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
{upload.like_count}
</button>
<button
onclick={() => oncomment(upload.id)}
class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
{upload.like_count}
</button>
<button
onclick={() => oncomment(upload.id)}
class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
{upload.comment_count}
</button>
{#if isOwn}
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
{upload.comment_count}
</button>
{#if isOwn}
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
{/if}
</div>
<!-- Caption -->
{#if upload.caption}
<p class="px-4 pb-3 text-sm text-gray-800 [overflow-wrap:anywhere] dark:text-gray-200">
{upload.caption}
</p>
{/if}
</div>
<!-- Caption -->
{#if upload.caption}
<p class="px-4 pb-3 text-sm text-gray-800 [overflow-wrap:anywhere] dark:text-gray-200">
{upload.caption}
</p>
{/if}
<div class="border-b border-gray-100 dark:border-gray-800"></div>
</article>

View File

@@ -18,10 +18,9 @@
<button
onclick={() => onselect(null)}
aria-pressed={selected === null}
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {selected ===
null
? 'bg-blue-600 text-white dark:bg-blue-500'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'}"
class="chip inline-flex min-h-11 shrink-0 items-center {selected === null
? 'chip-active'
: 'chip-inactive'}"
>
Alle
</button>
@@ -29,10 +28,9 @@
<button
onclick={() => onselect(h.tag)}
aria-pressed={selected === h.tag}
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {selected ===
h.tag
? 'bg-blue-600 text-white dark:bg-blue-500'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'}"
class="chip inline-flex min-h-11 shrink-0 items-center {selected === h.tag
? 'chip-active'
: 'chip-inactive'}"
>
#{h.tag}
<span class="ml-1 text-xs opacity-70">{h.count}</span>

View File

@@ -277,12 +277,12 @@
bind:value={newComment}
placeholder="Kommentar schreiben..."
maxlength={COMMENT_MAX}
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
class="input flex-1"
/>
<button
type="submit"
disabled={loading || !newComment.trim()}
class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-700 active:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
class="btn btn-primary btn-sm"
>
Senden
</button>

View File

@@ -49,9 +49,7 @@
use:focusTrap={{ onclose: onClose }}
use:scrollLock
>
<div
class="pointer-events-auto w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-900"
>
<div class="card pointer-events-auto w-full max-w-sm p-6 shadow-xl">
{@render children()}
</div>
</div>

View File

@@ -130,8 +130,17 @@
<!-- Content -->
<div class="mb-6 text-center">
<div class="mb-3 text-5xl">{currentStep.icon}</div>
<h2 id="onboarding-title" class="mb-2 text-xl font-bold text-gray-900 dark:text-gray-100">
<div class="mb-4 flex justify-center">
<div
class="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary-50 text-3xl dark:bg-primary-900/30"
>
{currentStep.icon}
</div>
</div>
<h2
id="onboarding-title"
class="mb-2 text-xl font-semibold text-gray-900 dark:text-gray-100"
>
{currentStep.title}
</h2>
@@ -193,10 +202,7 @@
>
Überspringen
</button>
<button
onclick={next}
class="flex-1 rounded-xl bg-blue-600 py-3 text-sm font-semibold text-white hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
>
<button onclick={next} class="btn btn-primary flex-1">
{step < steps.length - 1 ? 'Weiter' : "Los geht's!"}
</button>
</div>

View File

@@ -167,12 +167,7 @@
liken und kommentieren.
</p>
</div>
<button
onclick={close}
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
>
Schließen
</button>
<button onclick={close} class="btn btn-secondary btn-block"> Schließen </button>
{:else}
<!-- Gallery option -->
<button
@@ -236,12 +231,7 @@
</button>
<!-- Cancel -->
<button
onclick={close}
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
>
Abbrechen
</button>
<button onclick={close} class="btn btn-secondary btn-block"> Abbrechen </button>
{/if}
</div>
</div>

View File

@@ -0,0 +1,120 @@
/* ─────────────────────────────────────────────────────────────────────────
* EventSnap component layer — the shared, "define-once" building blocks.
*
* Colours/type/spacing tokens live in `tailwind-theme.css`; this file turns the
* *recurring UI patterns* (buttons, inputs, cards, badges, chips, sheets) into
* named classes so every page consumes the same look. Change a button here and
* it changes everywhere.
*
* These live in the `components` layer, which Tailwind emits BEFORE `utilities`
* — so a one-off inline utility (e.g. `rounded-lg`, `w-full`) on an element
* still overrides the component class. Reach for a component class first; drop
* to inline utilities only for genuinely one-off tweaks.
* ───────────────────────────────────────────────────────────────────────── */
@layer components {
/* ── Buttons ──────────────────────────────────────────────────────────
* Base + intent modifier (+ optional size). e.g. `class="btn btn-primary"`,
* `class="btn btn-secondary btn-sm"`, `class="btn btn-primary btn-block"`. */
.btn {
@apply inline-flex select-none items-center justify-center gap-2 rounded-xl border border-transparent px-4 py-3 text-sm font-semibold transition;
@apply focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40;
@apply active:scale-[0.99] disabled:cursor-not-allowed;
}
.btn-lg {
@apply px-5 py-3.5 text-base;
}
.btn-sm {
@apply rounded-lg px-3 py-2;
}
.btn-block {
@apply w-full;
}
/* Primary = muted champagne fill + gold border + gold text (an elegant,
* outlined "signal" rather than an aggressive solid gold block). */
.btn-primary {
@apply border-primary-400 bg-primary-100 text-primary-800 shadow-sm hover:border-primary-500 hover:bg-primary-200;
@apply disabled:border-gray-200 disabled:bg-gray-100 disabled:text-gray-400 disabled:shadow-none;
@apply dark:border-primary-500/50 dark:bg-primary-950/40 dark:text-primary-200 dark:hover:border-primary-500 dark:hover:bg-primary-900/40;
}
.btn-secondary {
@apply border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-50;
@apply dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700;
}
.btn-ghost {
@apply text-gray-600 hover:bg-gray-100 hover:text-gray-900 disabled:opacity-50;
@apply dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100;
}
.btn-danger {
@apply bg-red-600 text-white shadow-sm hover:bg-red-700 disabled:opacity-50;
@apply dark:bg-red-600 dark:hover:bg-red-500;
}
/* ── Form inputs ──────────────────────────────────────────────────────
* Covers text/number inputs and textareas. */
.input {
@apply w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 placeholder-gray-400 transition;
@apply focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/25;
@apply dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500;
}
/* ── Surfaces ─────────────────────────────────────────────────────────
* `card` — the standard raised surface (page → card → surface-muted is the
* three-level elevation ramp).
* `surface-muted` — a nested/inset panel inside a card. */
.card {
@apply rounded-2xl border border-gray-200/70 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900;
}
.surface-muted {
@apply rounded-xl bg-gray-50 dark:bg-gray-800/60;
}
/* ── Badges (status / role pills) ─────────────────────────────────────
* `class="badge badge-gold"` etc. A leading dot is optional in markup. */
.badge {
@apply inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold;
}
.badge-primary {
@apply bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-200;
}
.badge-gold {
@apply bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-200;
}
.badge-gray {
@apply bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300;
}
.badge-success {
@apply bg-green-100 text-green-700 dark:bg-green-950/50 dark:text-green-300;
}
.badge-danger {
@apply bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300;
}
/* ── Chips (filter / toggle) ──────────────────────────────────────────
* `class="chip chip-active"` / `class="chip chip-inactive"`. */
.chip {
@apply rounded-full border border-transparent px-4 py-2 text-sm font-medium transition;
}
.chip-inactive {
@apply bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700;
}
/* Active = gold outline + soft champagne fill (matches .btn-primary). */
.chip-active {
@apply border-primary-400 bg-primary-100 text-primary-800 dark:border-primary-500/50 dark:bg-primary-950/40 dark:text-primary-200;
}
/* ── Bottom-sheet surface ─────────────────────────────────────────────
* The rounded top panel shared by upload / context / confirm sheets. */
.sheet {
@apply rounded-t-3xl bg-white p-6 shadow-2xl dark:bg-gray-900 sm:rounded-2xl;
}
/* ── Page scaffold + section heading ──────────────────────────────────*/
.page {
@apply min-h-screen bg-gray-50 dark:bg-gray-950;
}
.section-title {
@apply font-display text-lg font-semibold text-gray-900 dark:text-gray-100;
}
}