/* ── Variables are loaded from /css/vars.css (generated from .env COLOR_* keys) ── */

@font-face {
	font-family: 'New Icon Script';
	src: url('/fonts/New-Icon-Script.otf') format('opentype');
	font-display: swap;
}

* { box-sizing: border-box; }

/* picture() helper wraps images in <picture> for WebP + fallback; keep the
   wrapper out of the layout so CSS targeting the inner <img> is unaffected. */
picture { display: contents; }

/* ── Base ── */

body {
	background-color: var(--bg);
	font-family: 'Raleway', sans-serif;
	font-weight: 300;
	color: var(--text);
	overflow: hidden;
}

.scroll-container {
	position: fixed;
	inset: 24px;
	overflow-y: auto;
	overflow-x: hidden;
	z-index: 1;
	scrollbar-width: none;
}

.scroll-container::-webkit-scrollbar { display: none; }

/* Background image + noise texture, clipped to the bordered area.
   Anything outside the 24px inset shows the plain --bg colour. */
body::before {
	content: '';
	position: fixed;
	inset: 24px;
	background-color: var(--bg);
	/*#152120*/
	/*#193a28*/
	background-image: url("/images/velvet.png");
	background-image: image-set(url("/images/velvet.webp") type("image/webp"), url("/images/velvet.png") type("image/png"));
	background-repeat: no-repeat;
	background-size: cover;
	background-position: center;
	/*filter: hue-rotate(15deg) saturate(1.4) brightness(1.1);*/
	/*background-image:*/
	/*	url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E"),*/
	/*	url('/images/background.jpg');*/
	/*background-size: auto, cover;*/
	/*background-position: center;*/
	pointer-events: none;
	z-index: 0;
}

body::after {
	content: '';
	position: fixed;
	inset: 24px;
	border: 1px solid var(--border);
	opacity: 0.35;
	pointer-events: none;
	z-index: 0;
}

@media (max-width: 60000px) {
	body::before {
		inset: 0;
		/* Size against the large viewport so the box doesn't change when the
		   mobile URL bar hides/shows — a viewport-sized box makes the cover
		   background rescale mid-scroll (visible jerk on Android Chrome). */
		height: 100vh;
		height: 100lvh;
	}
	body::after { display: none; }
	.scroll-container { inset: 0; }
}

@keyframes shuffleExit {
	from { opacity: 1; transform: translateX(0)     scale(1); }
	to   { opacity: 0; transform: translateX(-72px) scale(0.97); }
}

.page.is-exiting {
	animation: shuffleExit 0.32s cubic-bezier(0.4, 0, 1, 1) forwards;
	pointer-events: none;
}

/* ── Shared components ── */

.eyebrow {
	font-size: 0.65rem;
	letter-spacing: 0.35em;
	text-transform: uppercase;
	color: var(--accent);
	margin-bottom: 1.5rem;
}

.headline {
	font-family: 'Cormorant Garamond', serif;
	font-size: clamp(2.2rem, 5vw, 3rem);
	font-weight: 300;
	line-height: 1.15;
	letter-spacing: 0.02em;
	margin-bottom: 0.4rem;
}

.headline em { font-style: italic; color: var(--accent); }

.divider {
	display: flex;
	align-items: center;
	gap: 12px;
	margin: 1.6rem auto;
	width: 180px;
}

.divider::before,
.divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }

.divider-diamond {
	width: 6px;
	height: 6px;
	background: var(--accent);
	transform: rotate(45deg);
}

/* ── RSVP page ── */

/* One palette, shared with /login: --accent, --border, --surface and the
   button colours come straight from vars.css, so the RSVP card sits on the
   same cream-surface / green-frame system as the login card (border + shadow
   arrive via the base .page rules). Pinned here is only what's RSVP-specific:
   the espresso-plum body ink, the champagne foil layer, and the status motif
   (gold = awaiting, green = attending, wine = declined). */
body.rsvp-page {
	--gold:          #b8966e;  /* hairlines, sparkles, ornaments */
	--gold-ink:      #8a6a45;  /* darker foil for small text (labels) */
	--wine:          #6e061f;  /* calligraphy ink — a hint, not the lead */
	--surface-text:  #463339;  /* body ink: warm espresso-plum */
	--surface-muted: #7d666c;
	--attending:     #5e7a4e;
	--declined:      #a82f44;  /* faded wine — darker/punchier than the old rose */
	--error:         #b5372b;
	--accent-soft:   rgba(47, 93, 67, 0.08);   /* green focus wash */
	--flash-text:    #6e061f;
	--flash-bg:      rgba(184, 150, 110, 0.10);
	--flash-border:  #cfb08f;
}

/* Buttons: outline — transparent with the site's button green, fills on hover */
body.rsvp-page .btn-submit,
body.rsvp-page .btn-rsvp {
	background: transparent;
	border: 1px solid var(--btn);
	color: var(--btn);
	/* The base buttons clip to the padding box, which cancels the fill's
	   -1px underlap below — at fractional pixel positions (e.g. mid-shuffle
	   pinning) the fill and border then miss by a hair, leaving a pale seam
	   inside the border. Unclipped, the fill truly covers the border edge;
	   it's the same green, so nothing shows outside the box. */
	overflow: visible;
}
body.rsvp-page .btn-submit::after,
body.rsvp-page .btn-rsvp::after {
	background: var(--btn);
	/* Fill extends 1px under the same-colour border — at fractional device
	   pixels an inset:0 fill can round short and leave a sliver of gap */
	inset: -1px;
}
body.rsvp-page .btn-submit span,
body.rsvp-page .btn-rsvp span {
	color: var(--btn);
	transition: color 0.3s ease;
}
@media (hover: hover) {
	body.rsvp-page .btn-submit:hover span,
	body.rsvp-page .btn-rsvp:hover span {
		color: var(--btn-text);
	}
}

/* ── Stationery structure ──
   The letterpress details: foil labels, engraved accept/decline cards, and
   hydrangea status seals on the guest list. The divider is the shared base
   one — same as /login. */

/* Field + section labels wear the foil, spreading the palette off the
   red axis; the eyebrow above the guest's name stays wine. */
.rsvp-page .field-label,
.rsvp-page .section-label {
	color: var(--gold-ink);
}

/* Intro line under the divider on the guest-list page */
.rsvp-page .rsvp-note {
	font-family: 'Cormorant Garamond', serif;
	font-size: 1.05rem;
	font-style: italic;
	letter-spacing: 0.03em;
	color: var(--gold-ink);
	margin: -0.4rem 0 0;
}

/* ── FAQ page (rsvp-page palette + these) ──
   Exclusive accordion: question rows dressed like guest-rows, serif italic
   questions, a rotating + for state. faq.php animates max-height. */

.faq-page .faq-list {
	margin-top: 2.5rem;
	text-align: left;
}

.faq-item {
	border: 1px solid var(--border);
	background: rgba(255, 255, 255, 0.28);
	box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.07);
	margin-bottom: 0.75rem;
	transition: border-color 0.2s ease;
}

.faq-item--open { border-color: var(--accent); }

.faq-item__q {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 1rem;
	width: 100%;
	padding: 0.95rem 1.1rem;
	background: none;
	border: none;
	cursor: pointer;
	text-align: left;
	font-family: 'Cormorant Garamond', serif;
	font-size: 1.1rem;
	font-style: italic;
	letter-spacing: 0.03em;
	color: var(--surface-text);
}

.faq-item__icon {
	flex-shrink: 0;
	font-family: 'Raleway', sans-serif;
	font-style: normal;
	font-size: 1rem;
	color: var(--gold-ink);
	transition: transform 0.3s ease;
}

.faq-item--open .faq-item__icon { transform: rotate(45deg); }

.faq-item__a {
	max-height: 0;
	overflow: hidden;
	transition: max-height 0.35s ease;
}

.faq-item__a-inner {
	padding: 0 1.1rem 1rem;
	font-size: 0.82rem;
	letter-spacing: 0.03em;
	line-height: 1.8;
	color: var(--surface-muted);
}

.faq-item__a-inner a {
	color: var(--accent);
	text-decoration: underline;
	text-underline-offset: 3px;
}

.faq-item__a-inner strong { color: var(--surface-text); font-weight: 400; }

/* Deadline reminder above the guest list — small-caps foil, like the labels */
.rsvp-page .rsvp-deadline {
	text-align: center;
	font-size: 0.7rem;
	letter-spacing: 0.25em;
	text-transform: uppercase;
	color: var(--gold-ink);
	margin: 0 0 1.4rem;
}

/* Accept/decline as serif italic stationery cards; selected = engraved
   double border (border + inset shadow) with a blush wash */
.rsvp-page .radio-option label {
	font-family: 'Cormorant Garamond', serif;
	font-size: 1.05rem;
	font-style: italic;
	text-transform: none;
	letter-spacing: 0.04em;
	padding: 0.95rem 0.8rem;
	background: rgba(255, 255, 255, 0.25);
	box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.07);
}

/* Checked (neutral, e.g. transport): quiet espresso ink — logistics
   questions don't wear the status motif. The engraved ring follows each
   option's text colour via currentColor. */
.rsvp-page .radio-option input[type='radio']:checked + label {
	border-color: currentColor;
	box-shadow: inset 0 0 0 3px var(--surface), inset 0 0 0 4px currentColor,
		2px 3px 10px rgba(0, 0, 0, 0.07);
	color: var(--surface-text);
	background: rgba(255, 255, 255, 0.5);
}

/* The attendance choice carries the status motif in colour alone:
   accepting fills sage — the attending colour, standing proud of the paper */
.rsvp-page .radio-option--accept input[type='radio']:checked + label {
	color: #4d6741;
	border-color: var(--attending);
	background: rgba(94, 122, 78, 0.13);
}

/* Declining fills wine — the one place red speaks at full voice */
.rsvp-page .radio-option--decline input[type='radio']:checked + label {
	color: var(--wine);
	border-color: var(--declined);
	background: rgba(110, 6, 31, 0.05);
}

/* Inputs sit on a faint white wash so the fields read against the texture */
.rsvp-page .text-input,
.rsvp-page .text-area {
	background: rgba(255, 255, 255, 0.3);
}

.rsvp-page .text-input::placeholder,
.rsvp-page .text-area::placeholder {
	color: #c9b3b7;
}

/* Guest list: gold monogram medallion + serif name; rows lift on hover */
.rsvp-page .guest-row {
	background: rgba(255, 255, 255, 0.28);
	box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.07);
	transition: border-color 0.2s ease, background 0.2s ease;
}

/* hover-gated: touch browsers (mobile Firefox especially) leave a phantom
   :hover stuck on whatever sat under the last tap after a page swap */
@media (hover: hover) {
	.rsvp-page .guest-row:hover {
		border-color: var(--accent);
		background: rgba(255, 255, 255, 0.45);
	}
}

.rsvp-page .guest-id {
	display: flex;
	align-items: center;
	gap: 1rem;
	min-width: 0;
}

/* Guest seal: a hydrangea bloom that carries the RSVP state. Answered =
   a filled eight-petal bloom (four petals layered over four, diamond
   heart) in sage or faded wine; awaiting = a gold line-drawn four-petal
   bud. The SVGs are masks, so each state just sets a background colour. */
.rsvp-page .guest-seal {
	position: relative;
	flex-shrink: 0;
	width: 42px;
	height: 42px;
}

.rsvp-page .guest-seal::before {
	content: '';
	position: absolute;
	inset: 0;
	margin: auto;
	width: 36px;
	height: 36px;
	-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cg fill='%23fff'%3E%3Cpath d='M20 5.5 C24.2 8.5,24.2 13,20 17 C15.8 13,15.8 8.5,20 5.5 Z' transform='rotate(45 20 20)'/%3E%3Cpath d='M20 5.5 C24.2 8.5,24.2 13,20 17 C15.8 13,15.8 8.5,20 5.5 Z' transform='rotate(135 20 20)'/%3E%3Cpath d='M20 5.5 C24.2 8.5,24.2 13,20 17 C15.8 13,15.8 8.5,20 5.5 Z' transform='rotate(225 20 20)'/%3E%3Cpath d='M20 5.5 C24.2 8.5,24.2 13,20 17 C15.8 13,15.8 8.5,20 5.5 Z' transform='rotate(315 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(90 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(180 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(270 20 20)'/%3E%3Crect x='18.3' y='18.3' width='3.4' height='3.4' transform='rotate(45 20 20)'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
	mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cg fill='%23fff'%3E%3Cpath d='M20 5.5 C24.2 8.5,24.2 13,20 17 C15.8 13,15.8 8.5,20 5.5 Z' transform='rotate(45 20 20)'/%3E%3Cpath d='M20 5.5 C24.2 8.5,24.2 13,20 17 C15.8 13,15.8 8.5,20 5.5 Z' transform='rotate(135 20 20)'/%3E%3Cpath d='M20 5.5 C24.2 8.5,24.2 13,20 17 C15.8 13,15.8 8.5,20 5.5 Z' transform='rotate(225 20 20)'/%3E%3Cpath d='M20 5.5 C24.2 8.5,24.2 13,20 17 C15.8 13,15.8 8.5,20 5.5 Z' transform='rotate(315 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(90 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(180 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(270 20 20)'/%3E%3Crect x='18.3' y='18.3' width='3.4' height='3.4' transform='rotate(45 20 20)'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
}

.rsvp-page .guest-seal--attending::before {
	background: var(--attending);
}

.rsvp-page .guest-seal--declined::before {
	background: var(--declined);
	opacity: 0.5;
}

.rsvp-page .guest-seal--pending::before {
	background: var(--gold);
	-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cg fill='none' stroke='%23fff' stroke-width='1.7'%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(90 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(180 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(270 20 20)'/%3E%3Crect x='18.3' y='18.3' width='3.4' height='3.4' fill='%23fff' stroke='none' transform='rotate(45 20 20)'/%3E%3C/g%3E%3C/svg%3E");
	mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cg fill='none' stroke='%23fff' stroke-width='1.7'%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(90 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(180 20 20)'/%3E%3Cpath d='M20 3 C25.5 7,25.5 12.5,20 17.5 C14.5 12.5,14.5 7,20 3 Z' transform='rotate(270 20 20)'/%3E%3Crect x='18.3' y='18.3' width='3.4' height='3.4' fill='%23fff' stroke='none' transform='rotate(45 20 20)'/%3E%3C/g%3E%3C/svg%3E");
}

.rsvp-page .guest-name {
	font-size: 1.2rem;
}

/* Status with a small dot; pending guests get an explicit "awaiting" state
   in foil so the list shows at a glance who still needs nudging */
.rsvp-page .guest-status::before {
	content: '';
	display: inline-block;
	width: 5px;
	height: 5px;
	border-radius: 50%;
	background: currentColor;
	margin-right: 0.5em;
	vertical-align: 1px;
}

.rsvp-page .guest-status.pending {
	color: var(--gold-ink);
}

.rsvp-page .headline {
	font-family: 'New Icon Script', cursive;
	font-size: clamp(1.6rem, 5vw, 2rem);
	line-height: 1;
	letter-spacing: 11px;
	font-kerning: none;
	/* Wine calligraphy, exactly like the collage back card */
	color: var(--wine);
}

/* The form page's headline is a guest name (mixed case) — the wide tracking
   only flatters the all-caps "RSVP" on the listing page. Names get a size
   bump: at the shared 2rem cap the calligraphy reads dainty on the card. */
.rsvp-page .page.rsvp .headline {
	letter-spacing: normal;
	font-size: clamp(2rem, 6vw, 2.6rem);
}

/* ── Login ── */

body.login .scroll-container {
	display: flex;
}

.wrapper {
	/* Auto margins centre like align-items/justify-content but stay scrollable
	   when the card is taller than the viewport (flex centering clips both ends
	   unreachably — the Android Firefox keyboard-open bug). */
	margin: auto;
	position: relative;
	z-index: 1;
	text-align: center;
	max-width: 680px;
	width: 100%;
	padding: 3rem 2.5rem;
		background-color: var(--surface);
	color: var(--surface-text);
	border: 1px solid var(--accent);
	box-shadow: 8px 8px 32px rgba(0, 0, 0, 0.22);
}

.wrapper .headline {
	font-size: clamp(2.4rem, 6vw, 3.2rem);
	line-height: 1.1;
}

.subtext {
	font-size: 0.82rem;
	letter-spacing: 0.06em;
	color: var(--surface-muted);
	line-height: 1.8;
	margin-bottom: 2.2rem;
}

.subtext strong { color: var(--surface-text); font-weight: 400; }

.subtext a {
	color: var(--accent);
	text-decoration: underline;
	text-underline-offset: 3px;
}

.pin-form { }

.pin-input-wrapper { position: relative; margin-bottom: 1.2rem; }

.pin-input {
	width: 100%;
	text-align: center;
	font-family: 'Cormorant Garamond', serif;
	font-size: 1.6rem;
	font-weight: 300;
	/* Codes can be long mixed-case words (up to 255 chars), not 6-char pins —
	   modest tracking and no uppercase so what you type matches your link. */
	letter-spacing: 0.5em;
	padding: 0.9rem 1rem;
	border: 1px solid var(--border);
	border-radius: 0;
	background: transparent;
	color: var(--surface-text);
	transition: border-color 0.3s ease, box-shadow 0.3s ease;
	outline: none;
	text-transform: uppercase;
}

.pin-input::placeholder {
	letter-spacing: 0.15em;
	color: var(--border);
	font-size: 1.1rem;
	font-style: italic;
}

.pin-input:focus {
	border-color: var(--accent);
	box-shadow: 0 0 0 3px rgba(184, 150, 110, 0.12);
}

.btn-enter {
	width: 100%;
	padding: 0.85rem;
	background: var(--btn);
	color: var(--btn-text);
	border: none;
	border-radius: 0;
	font-family: 'Raleway', sans-serif;
	font-size: 0.7rem;
	font-weight: 400;
	letter-spacing: 0.3em;
	text-transform: uppercase;
	cursor: pointer;
	position: relative;
	overflow: hidden;
	transition: color 0.3s ease;
}

.btn-enter::after {
	content: '';
	position: absolute;
	inset: 0;
	background: var(--btn-hover);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 0.4s ease;
	z-index: 0;
}

@media (hover: hover) {
	.btn-enter:hover::after { transform: scaleX(1); }
}
.btn-enter span { position: relative; z-index: 1; }

/* ── Home ── */

.page {
	position: relative;
	z-index: 1;
	max-width: 680px;
	margin: 3rem auto;
	padding: 4rem 3rem 5rem;
	text-align: center;
	background-color: var(--surface);
	color: var(--surface-text);
	border: 1px solid var(--accent);
	box-shadow: 8px 8px 32px rgba(0,0,0,0.22);
}

/* The festivities timeline is a self-contained graphic, so drop the cream
   card frame/padding and let it fill the column — maximises width (and so the
   per-section height) for the times, especially on phones. */
.page.timeline {
	padding: 0;
	border: none;
	background: none;
	box-shadow: none;
}

.page.timeline img {
	display: block;
	width: 100%;
}

/* Invisible tap target over the map illustration in the "Getting there"
   section of the venue graphic (pin + dashed route, bottom-right of the
   1024x1600 image). Percentages of the image box so it tracks any size. */
.page.timeline .map-hotspot {
	position: absolute;
	left: 62%;
	top: 78%;
	width: 30%;
	height: 19%;
	cursor: pointer;
}

/* Portal ripple over the map illustration. The SVG bleeds 10% past the
   hotspot on every side, matching the 10% bleed baked into its viewBox, so
   the feathered mask fades out before the overlay's own edge and displaced
   pixels always sample real artwork — no visible seam. */
.page.timeline .map-hotspot .map-portal {
	position: absolute;
	inset: -10%;
	pointer-events: none;
}

/* Iridescent portal light: blush / gold / sage pools painted onto the paper
   with `color` blending (hue lands, texture survives), slowly cycling hue.
   z-index lifts it above the SVG copy; the mask feathers it out before the
   hotspot's bleed edge. */
.page.timeline .map-hotspot::before {
	content: '';
	position: absolute;
	inset: -10%;
	z-index: 1;
	pointer-events: none;
	background:
		radial-gradient(closest-side at 68% 28%, rgba(224, 130, 140, 0.85), transparent 65%),
		radial-gradient(closest-side at 30% 68%, rgba(212, 160, 60, 0.75), transparent 65%),
		radial-gradient(closest-side, rgba(140, 196, 120, 0.65), transparent 72%);
	mix-blend-mode: color;
	-webkit-mask-image: radial-gradient(closest-side, #000 45%, transparent 78%);
	mask-image: radial-gradient(closest-side, #000 45%, transparent 78%);
	animation: map-portal-tint 7s ease-in-out infinite;
}

@keyframes map-portal-tint {
	0%, 100% { opacity: 0.55; filter: hue-rotate(0deg); }
	35%      { opacity: 0.95; filter: hue-rotate(50deg); }
	70%      { opacity: 0.75; filter: hue-rotate(-35deg); }
}

/* Reduced motion: drop the ripple, mark the spot with a soft static
   highlight instead so it still reads as interactive. */
@media (prefers-reduced-motion: reduce) {
	.page.timeline .map-hotspot .map-portal,
	.page.timeline .map-hotspot::before { display: none; }

	.page.timeline .map-hotspot::after {
		content: '';
		position: absolute;
		inset: -6%;
		background: radial-gradient(closest-side,
			rgba(255, 252, 240, 0.4), rgba(255, 252, 240, 0) 72%);
		pointer-events: none;
	}
}

.body-text {
	font-size: 0.85rem;
	letter-spacing: 0.06em;
	color: var(--surface-muted);
	line-height: 1.9;
}

.section { margin-top: 3.5rem; text-align: left; }

.section-label {
	font-size: 0.6rem;
	letter-spacing: 0.4em;
	text-transform: uppercase;
	color: var(--accent);
	margin-bottom: 1rem;
}

.venue-block {
	border: 1px solid var(--border);
	padding: 1.8rem 2rem;
	font-size: 0.85rem;
	line-height: 2;
	letter-spacing: 0.04em;
	color: var(--surface-muted);
}

.venue-block strong {
	display: block;
	font-family: 'Cormorant Garamond', serif;
	font-size: 1.2rem;
	font-weight: 400;
	color: var(--surface-text);
	margin-bottom: 0.3rem;
}

.guest-list { display: flex; flex-direction: column; gap: 0.75rem; }

.guest-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	border: 1px solid var(--border);
	padding: 1rem 1.4rem;
}

.guest-name {
	font-family: 'Cormorant Garamond', serif;
	font-size: 1.1rem;
	font-weight: 300;
	letter-spacing: 0.04em;
}

.guest-status {
	font-size: 0.6rem;
	letter-spacing: 0.2em;
	text-transform: uppercase;
	margin-top: 0.2rem;
}

.guest-status.attending     { color: var(--attending); }
.guest-status.not-attending { color: var(--declined); }

.btn-rsvp {
	font-family: 'Raleway', sans-serif;
	font-size: 0.62rem;
	letter-spacing: 0.28em;
	text-transform: uppercase;
	color: var(--btn-text);
	background: var(--btn);
	border: none;
	padding: 0.55rem 1.2rem;
	text-decoration: none;
	display: inline-block;
	position: relative;
	overflow: hidden;
}

.btn-rsvp::after {
	content: '';
	position: absolute;
	inset: 0;
	background: var(--btn-hover);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 0.35s ease;
	z-index: 0;
}

@media (hover: hover) {
	.btn-rsvp:hover::after { transform: scaleX(1); }
}
.btn-rsvp span { position: relative; z-index: 1; color: var(--btn-text); }

.admin-link {
	position: absolute;
	top: 0.6rem;
	right: 0.8rem;
	font-size: 0.85rem;
	color: var(--accent);
	opacity: 0.15;
	text-decoration: none;
	transition: opacity 0.2s ease;
}

@media (hover: hover) {
	.admin-link:hover { opacity: 0.5; }
}

.logout-link {
	display: block;
	margin-top: 4rem;
	font-size: 0.62rem;
	letter-spacing: 0.3em;
	text-transform: uppercase;
	color: var(--surface-muted);
	text-decoration: none;
	opacity: 0.6;
}

@media (hover: hover) {
	.logout-link:hover { opacity: 1; color: var(--accent); }
}

/* Stays until the next navigation — it renders from a one-shot session
   value, so going home or into a guest's form clears it on the next view. */
.flash {
	font-size: 0.78rem;
	letter-spacing: 0.05em;
	color: var(--flash-text);
	background: var(--flash-bg, transparent);
	border: 1px solid var(--flash-border);
	padding: 0.9rem 1rem;
	margin: 1.75rem 0 0;
	box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.07);
}

/* With a flash showing, match the gap below it to the note→flash gap above
   (the flash-less page keeps the roomier 3.5rem before the guest list). */
.flash + .section { margin-top: 1.75rem; }

/* Validation errors: the flash's stationery box, in the error red */
.error-list {
	list-style: none;
	font-size: 0.78rem;
	letter-spacing: 0.05em;
	color: var(--error);
	background: rgba(181, 55, 43, 0.05);
	border: 1px solid currentColor;
	padding: 0.9rem 1rem;
	margin: 0 0 2rem;
	box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.07);
}

/* ── RSVP ── */

.page.rsvp { max-width: 680px; }

.rsvp-form { text-align: left; margin-top: 2.5rem; }

.field { margin-bottom: 1.8rem; }

.field--collapsible {
	overflow: hidden;
	max-height: 300px;
	opacity: 1;
	visibility: visible;
	transition: max-height 0.35s ease, opacity 0.25s ease, margin-bottom 0.35s ease,
		visibility 0s linear;
}

/* visibility (not just opacity) so collapsed inputs leave the tab order and
   the accessibility tree — it flips after the collapse animation on the way
   out (0.35s delay below), instantly on the way back in (0s above). */
.field--collapsible.field--collapsed {
	max-height: 0;
	opacity: 0;
	margin-bottom: 0;
	pointer-events: none;
	visibility: hidden;
	transition: max-height 0.35s ease, opacity 0.25s ease, margin-bottom 0.35s ease,
		visibility 0s linear 0.35s;
}

.field--collapsible.no-transition {
	transition: none !important;
}

.field-label {
	display: block;
	font-size: 0.6rem;
	letter-spacing: 0.4em;
	text-transform: uppercase;
	color: var(--accent);
	margin-bottom: 0.8rem;
}

.radio-group { display: flex; gap: 1rem; }

.radio-option {
	flex: 1;
	position: relative;
}

.radio-option input[type='radio'] {
	position: absolute;
	opacity: 0;
	width: 0;
}

.radio-option label {
	display: block;
	text-align: center;
	padding: 0.8rem;
	border: 1px solid var(--border);
	font-size: 0.72rem;
	letter-spacing: 0.2em;
	text-transform: uppercase;
	color: var(--surface-muted);
	cursor: pointer;
	transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}

/* The real radio is invisible (opacity 0, width 0), so the browser's focus
   ring has nothing to draw on — surface keyboard focus on the styled label.
   Browser-default focus blue, and drawn inward: an outward ring gets clipped
   by the collapsible fields' overflow:hidden (e.g. transport). */
.radio-option input[type='radio']:focus-visible + label {
	outline: 2px solid #005fcc;
	outline-offset: -2px;
}

.radio-option input[type='radio']:checked + label {
	border-color: var(--accent);
	color: var(--surface-text);
	background: var(--accent-soft, rgba(184, 150, 110, 0.08));
}

@media (hover: hover) {
	.radio-option label:hover {
		border-color: var(--accent);
		color: var(--surface-text);
	}
}

.text-input,
.text-area {
	width: 100%;
	padding: 0.8rem 1rem;
	border: 1px solid var(--border);
	border-radius: 0;
	background: transparent;
	font-family: 'Raleway', sans-serif;
	font-size: 0.82rem;
	font-weight: 300;
	color: var(--surface-text);
	outline: none;
	transition: border-color 0.2s ease, box-shadow 0.2s ease;
	letter-spacing: 0.04em;
}

.text-area { resize: vertical; min-height: 90px; }

/* Small print under an input (e.g. the phone field's WhatsApp note) */
.field-hint {
	font-size: 0.72rem;
	letter-spacing: 0.04em;
	line-height: 1.5;
	color: var(--surface-muted);
	margin: 0.45rem 0 0;
}

.text-input:focus,
.text-area:focus {
	border-color: #000;
	box-shadow: none;
}

.text-input::placeholder,
.text-area::placeholder { color: var(--border); }

.btn-submit {
	width: 100%;
	margin-top: 0.5rem;
	padding: 0.9rem;
	background: var(--btn);
	color: var(--btn-text);
	border: none;
	border-radius: 0;
	font-family: 'Raleway', sans-serif;
	font-size: 0.7rem;
	font-weight: 400;
	letter-spacing: 0.3em;
	text-transform: uppercase;
	cursor: pointer;
	position: relative;
	overflow: hidden;
}

.btn-submit::after {
	content: '';
	position: absolute;
	inset: 0;
	background: var(--btn-hover);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 0.4s ease;
	z-index: 0;
}

@media (hover: hover) {
	.btn-submit:hover::after { transform: scaleX(1); }
}
.btn-submit span { position: relative; z-index: 1; }

.error-list {
	margin-bottom: 1.8rem;
	padding: 0.9rem 1rem;
	border: 1px solid var(--error);
	font-size: 0.78rem;
	letter-spacing: 0.04em;
	color: var(--error);
	list-style: none;
	box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.07);
}

.back-link {
	display: block;
	margin-top: 2.5rem;
	font-size: 0.62rem;
	letter-spacing: 0.3em;
	text-transform: uppercase;
	color: var(--surface-muted);
	text-decoration: none;
	opacity: 0.6;
}

@media (hover: hover) {
	.back-link:hover { opacity: 1; color: var(--accent); }
}

.btn-venue {
	display: block;
	width: 100%;
	padding: 0.9rem;
	background: var(--btn);
	color: var(--btn-text);
	border: none;
	font-family: 'Raleway', sans-serif;
	font-size: 0.7rem;
	font-weight: 400;
	letter-spacing: 0.3em;
	text-transform: uppercase;
	text-align: center;
	text-decoration: none;
	cursor: pointer;
	position: relative;
	overflow: hidden;
}

.btn-venue::after {
	content: '';
	position: absolute;
	inset: 0;
	background: var(--btn-hover);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 0.4s ease;
	z-index: 0;
}

@media (hover: hover) {
	.btn-venue:hover::after { transform: scaleX(1); }
}
.btn-venue span { position: relative; z-index: 1; color: var(--btn-text); }

/* ── In-MC mode (pages loaded inside a mission-control iframe) ──────────────
   When a page is fetched with ?mc=1, PHP adds body.in-mc.
   We remove the page's own background so the home texture shows through,
   making the zoom-in transition completely seamless. */

body.in-mc {
	background: transparent;
}

body.in-mc::before,
body.in-mc::after {
	display: none;
}

body.in-mc .scroll-container {
	background: transparent;
	cursor: pointer; /* iOS Safari requires this to fire click events on empty areas */
}

body.in-mc .page {
	cursor: default;
	box-shadow: none; /* mc-card filter: drop-shadow handles depth */
}

/* ── Mission Control ── */

body.mission-control .scroll-container {
	overflow: hidden;
}

.mc-desktop {
	position: relative;
	width: 100%;
	height: 100%;
}

/* Each card is always full-viewport, parked at top-left.
   JS applies translate+scale+rotate to position it in mini view.
   Adding .mc-card--active clears the transform → CSS transition zooms it in. */
.mc-card {
	position: fixed;
	top: 0;
	left: 0;
	width: 100vw;    /* always full viewport width — content centres via max-width */
	height: 1200px;  /* generous fallback — JS resizes to actual content height */
	transform-origin: top left;
	transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1),
	            filter 0.4s ease,
	            opacity 0.35s ease;
	overflow: hidden;
	/* drop-shadow composites the actual rendered content (the .page card inside
	   the transparent iframe), so the shadow hugs the paper rather than the box */
	filter: drop-shadow(2px 4px 8px rgba(0, 0, 0, 0.30));
	pointer-events: none;
	z-index: 1;
}

.mc-card iframe {
	width: 100%;
	height: 100%;
	border: none;
	display: block;
	pointer-events: none;
}

/* Hover state driven by JS (hotzone mouseenter) */
.mc-card.mc-card--hover {
	filter: drop-shadow(3px 6px 12px rgba(0, 0, 0, 0.45));
}

/* Zoomed-in state — snapped to full viewport after animation */
.mc-card.mc-card--active {
	width: 100vw;
	height: 100%;
	z-index: 50;
	filter: none;
	pointer-events: auto;
}

.mc-card.mc-card--active iframe {
	pointer-events: auto;
}

/* Cards behind the active one fade out */
.mc-card.mc-card--bg {
	opacity: 0;
	pointer-events: none;
}

/* ── Card flip ── */

/* Flipper sits inside the card, fills it, and owns the 3D transform.
   This keeps the card's own transform-origin:top-left for grid positioning
   while the flipper rotates around its own centre. */
.mc-card__flipper {
	position: absolute;
	inset: 0;
	transform-style: preserve-3d;
	transform-origin: center center;
}

.mc-card__back,
.mc-card__front {
	position: absolute;
	inset: 0;
	backface-visibility: hidden;
	-webkit-backface-visibility: hidden;
}

/* Front face starts face-down; flipper rotating 180deg reveals it */
.mc-card__front {
	transform: rotateY(180deg);
}

.mc-card__front iframe {
	width: 100%;
	height: 100%;
	border: none;
	display: block;
	pointer-events: none;
}

/* Back face: padding confines flex layout to the paper area (same 680px width +
   top/bottom margins as a .page), so align-items/justify-content can aim the SVG
   at the corner of the paper that faces the grid centre. */
.mc-card__back {
	display: flex;
	box-sizing: border-box;
	/* top/bottom = BACK_TOP (48px) → content area = BACK_H (980px) exactly, so
	   flex-end lands on paper bottom; left/right = sideMargin → 680px wide */
	padding: 48px max(0px, calc((100vw - 680px) / 2));
	align-items: flex-end;        /* index 1 (top-right): SVG at paper bottom */
	justify-content: flex-start;  /* index 1, 3 (right col): SVG at paper left */
}

/* bottom-left (Festivities): SVG at paper top-right */
[data-index="2"] .mc-card__back {
	align-items: flex-start;
	justify-content: flex-end;
}

/* bottom-right (RSVP): SVG at paper top-left */
[data-index="3"] .mc-card__back {
	align-items: flex-start;
}

.mc-card__back-svg {
	display: block;
	width: 65%;
	max-width: 480px;
	height: auto;
}


/* Active flippable card: iframe inside the front face gets pointer events */
.mc-card--flippable.mc-card--active .mc-card__front iframe {
	pointer-events: auto;
}

/* Transparent click target sized and positioned over the visual card.
   Handles zoom-in clicks and hover; positioned entirely by JS. */
.mc-hotzone {
	position: absolute;
	z-index: 10;
	cursor: pointer;
	transform-origin: 0 0;
}

/* ── Invite ── */

.page.invite {
	background: transparent;
	border: none;
	box-shadow: none;
	padding: 0;
}

.invite-svg {
	display: block;
	width: 100%;
	height: auto;
}


/* ── Venue ── */

.venue-detail-text {
	font-size: 0.85rem;
	line-height: 1.9;
	letter-spacing: 0.04em;
	color: var(--surface-muted);
	margin: 0;
}
/* ── Shared album (/upload) ──
   One object at two densities: the grid is a jostled pile of small Instax
   prints (72/86 frame, square window, blank chin — nothing machine-aligned,
   each gets a seeded tilt/nudge via --jr/--jx/--jy so edges never grid up)
   and the lightbox is the same polaroid grown large, so the zoom is one
   object lifting off the pile rather than a swap. Chins stay blank in the
   grid; the attribution is written on during the zoom. */

/* No paper: the prints lie straight on the velvet (2026-09-13 — the cream
   .page card is stripped like .page.timeline does), and the header lives
   on its own small card, a place card resting on the table above the pile.
   Bottom padding clears the fixed pills. */
.upload-page .page {
	padding: 0 0.6rem 7.5rem;
	border: none;
	background: none;
	box-shadow: none;
}

.album-masthead {
	max-width: 540px;
	margin: 0 auto;
	padding: 2.4rem 2rem 2.6rem;
	background-color: var(--surface);
	border: 1px solid var(--accent);
	box-shadow: 8px 8px 32px rgba(0, 0, 0, 0.22);
}

/* The empty-album line sits on velvet now, not on paper */
body.upload-page .album-empty {
	color: rgba(242, 238, 228, 0.78);
}

/* container-type lets strip tiles size themselves off the grid's width
   (cqw) — see .album-tile--strip */
.album-grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 0.4rem;
	margin-top: 1.8rem;
	container-type: inline-size;
}

/* Each tile is a small Instax print (frame numbers mirror the lightbox and
   /home: window 86.1% wide, 6.9% top margin) so the lightbox zoom is a pure
   uniform scale of the same shape. The jitter transform is what makes the
   pile: values are seeded from the media id (same hash in upload.php and
   upload.js) so every print keeps its pose across reloads and live polls. */
.album-tile {
	position: relative;
	aspect-ratio: 72 / 86;
	padding: 0;
	border: none;
	border-radius: 1px;
	background: linear-gradient(160deg, #fdfbf6, #f2eee4);
	box-shadow: 0 1px 4px rgba(0, 0, 0, 0.22);
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
	transform: translate(var(--jx, 0), var(--jy, 0)) rotate(var(--jr, 0deg));
	/* Buttons vertically centre their content by default, which would split
	   the chin's cream between top and bottom — flex-start pins the window
	   up so the chin sits below, like the lightbox's figure */
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: flex-start;
}

.album-tile__window {
	display: block;
	position: relative;
	flex: none;
	width: 86.1%;
	margin-top: 6.9%;
	aspect-ratio: 1 / 1;
	overflow: hidden;
	background: #e9e5db;
}

.album-tile__window::after {
	content: '';
	position: absolute;
	inset: 0;
	box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.18);
	pointer-events: none;
}

.album-tile:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.album-tile img,
.album-tile video {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

/* Yours: a small gold corner stitch, quiet enough to ignore */
.album-tile--mine::after {
	content: '';
	position: absolute;
	top: 0;
	right: 0;
	border: 5px solid transparent;
	border-top-color: var(--accent);
	border-right-color: var(--accent);
}

/* Live arrivals develop into place */
.album-tile--new {
	animation: album-develop 0.6s ease both;
}

/* Keyframes restate the jitter — a bare scale() would zero the tilt for
   the animation's duration, then snap it back */
@keyframes album-develop {
	from {
		opacity: 0;
		transform: translate(var(--jx, 0), var(--jy, 0)) rotate(var(--jr, 0deg)) scale(0.85);
	}
	to {
		opacity: 1;
		transform: translate(var(--jx, 0), var(--jy, 0)) rotate(var(--jr, 0deg)) scale(1);
	}
}

@media (prefers-reduced-motion: reduce) {
	.album-tile--new {
		animation: none;
	}
}

/* Booth strips are their true printed shape: two rows tall at the strip's
   natural ratio (--ar, set inline from the stored w/h), lying on the pile.
   The cream frame and footer are baked into the composed image, so no CSS
   frame — the image IS the print. Height is 2 rows (each col × 86/72, the
   Instax tiles set the row height) + 1 gap, computed from the grid's width
   (cqw) rather than percentage-of-area so auto row sizing never sees a
   circular dependency. */
.album-tile--strip {
	grid-row: span 2;
	justify-self: center;
	aspect-ratio: var(--ar, 1 / 3);
	height: 100%; /* fallback for pre-container-query engines */
	height: calc((100cqw - 0.8rem) / 3 * 86 / 72 * 2 + 0.4rem);
	width: auto;
	background: none;
}

.album-tile--strip .album-tile__window {
	width: 100%;
	height: 100%;
	margin: 0;
	aspect-ratio: auto;
	background: none;
}

.album-tile__play {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	color: #fff;
	font-size: 1.3rem;
	text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
	pointer-events: none;
}

.album-tile__pending {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	padding: 0.5rem;
	text-align: center;
	font-size: 0.65rem;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--surface-muted);
}

.album-empty {
	margin-top: 2rem;
	text-align: center;
	font-style: italic;
	color: var(--surface-muted);
}

#album-sentinel {
	height: 1px;
}

/* The page's actions, fixed in thumb reach: the Add pill + the selfie booth */
.album-actions {
	position: fixed;
	left: 50%;
	bottom: calc(1.2rem + env(safe-area-inset-bottom, 0px));
	transform: translateX(-50%);
	z-index: 60;
	display: flex;
	align-items: center;
	gap: 0.6rem;
}

/* Two equal pills: Photobooth (gold, shimmering — the one that begs to be
   pressed) and Upload (the quiet green partner) */
.album-cta {
	position: relative;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: min(9.5rem, 42vw);
	padding: 0.85rem 0;
	border: none;
	border-radius: 999px;
	font-size: 0.8rem;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	overflow: hidden; /* clips the sheen sweep */
	box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
	cursor: pointer;
}

.album-cta--upload {
	background: var(--btn);
	color: var(--btn-text);
	transition: background 0.2s ease;
}

.album-cta--upload:hover {
	background: var(--btn-hover);
}

.album-cta--upload input {
	display: none;
}

/* Same idea as the /home unseen-card shimmer (glow breath + sheen sweep),
   but louder: this one has to read across a dark reception. Gold from the
   RSVP palette (--gold/--gold-ink, in scope via body.rsvp-page). */
.album-cta--booth {
	background: linear-gradient(160deg, #d9bd93, var(--gold) 55%, var(--gold-ink));
	color: #3b2c14;
	animation: cta-glow 2.8s ease-in-out infinite;
}

/* Zooming veils the page instead of scrimming it: the page card and the
   action pills fade right out, so the polaroid floats on the bare velvet
   (body::before) — same idea as /home's collage-dim. The transition lives
   on the base rule so the fade-back plays on close too. */
body.upload-page .page,
.album-actions {
	transition: opacity 650ms ease; /* = ZOOM_MS in upload.js */
}

body.album-veil .page,
body.album-veil .album-actions {
	opacity: 0;
	pointer-events: none;
}

/* Seamless sweep: the band (40–60% of a 350%-wide canvas) is fully
   off-screen at BOTH loop endpoints (the visible window is 28.6% of the
   canvas: [0,28.6] at one end, [71.4,100] at the other), and linear timing
   keeps the velocity constant across the loop point — so there is no
   visible start or end frame, just a band that crosses and a natural pause
   while the empty runway passes. Don't narrow background-size or re-ease
   this: that's what made the old one visibly park at the right edge. */
.album-cta--booth::after {
	content: '';
	position: absolute;
	inset: 0;
	pointer-events: none;
	background: linear-gradient(115deg,
		transparent 40%,
		rgba(255, 255, 255, 0.45) 46%,
		rgba(255, 253, 245, 0.75) 50%,
		rgba(255, 255, 255, 0.45) 54%,
		transparent 60%);
	background-size: 350% 100%;
	background-position: 100% 0;
	animation: cta-sheen 3.2s linear infinite;
}

@keyframes cta-sheen {
	from { background-position: 100% 0; }
	to   { background-position: 0% 0; }
}

@keyframes cta-glow {
	0%, 100% { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35), 0 0 9px  rgba(255, 211, 130, 0.35); }
	50%      { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35), 0 0 20px rgba(255, 211, 130, 0.8); }
}

@media (prefers-reduced-motion: reduce) {
	.album-cta--booth,
	.album-cta--booth::after {
		animation: none;
	}

	.album-cta--booth {
		box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35), 0 0 12px rgba(255, 211, 130, 0.5);
	}
}

/* Progress toasts stack above the pill; rows are small paper cards since
   they float over the dark page background */
.album-toasts {
	position: fixed;
	left: 50%;
	bottom: calc(4.6rem + env(safe-area-inset-bottom, 0px));
	transform: translateX(-50%);
	z-index: 85; /* above the booth/lightbox scrims — progress stays visible */
	width: min(560px, calc(100vw - 2rem));
}

.upload-queue {
	list-style: none;
	margin: 0;
	padding: 0;
}

.upload-row {
	display: flex;
	align-items: center;
	gap: 0.8rem;
	margin-top: 0.4rem;
	padding: 0.5rem 0.8rem;
	border-radius: 4px;
	background: var(--surface);
	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
	font-size: 0.78rem;
	color: var(--surface-muted);
	transition: opacity 0.6s ease;
}

.upload-row--done {
	opacity: 0;
}

.upload-row--error {
	color: var(--error);
	cursor: pointer;
}

.upload-row__name {
	flex: 0 1 38%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.upload-row__bar {
	flex: 1;
	height: 4px;
	border-radius: 2px;
	background: rgba(0, 0, 0, 0.08);
	overflow: hidden;
}

.upload-row__fill {
	display: block;
	width: 0;
	height: 100%;
	background: var(--accent);
	transition: width 0.15s linear;
}

/* ── Album lightbox: the polaroid ──
   Frame numbers mirror collage.css exactly (window 86.1% wide, 6.9% top
   margin, chin from 77.9%) so this and the /home polaroids read as the same
   print. No scrim: the page itself is veiled (body.album-veil, above) so
   the polaroid floats on bare velvet. It opens as a FLIP lift: upload.js
   poses the frame over the clicked tile (same aspect, so a uniform scale)
   and flies it to rest while the page fades away behind it; the
   attribution ink writes on landing (--written) and fades during the
   return flight (--fading). */

body.album-light-open,
body.album-light-open .scroll-container {
	overflow: hidden;
}

/* display:flex would beat the UA's [hidden] rule — restate it */
.album-light[hidden] {
	display: none;
}

.album-light {
	position: fixed;
	inset: 0;
	z-index: 80;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	cursor: zoom-out;
}

.album-light__frame {
	position: relative;
	margin: 0;
	width: min(430px, 88vw, 65vh);
	aspect-ratio: 72 / 86;
	background: linear-gradient(160deg, #fdfbf6, #f2eee4);
	/* Rests perfectly straight — the /home polaroids keep their tilt, but
	   here the zoom is the reading pose (changed from -1.2deg, 2026-09-13) */
	/* box-shadow, NOT filter: drop-shadow — a filter makes Chrome re-raster
	   the whole frame (photo included) through the filter pipeline on every
	   frame of the ink's clip-path write, which is what made the caption
	   jitter. The frame is a plain rect, so box-shadow looks the same. */
	box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.4);
	cursor: default;
}

.album-light__window {
	position: relative;
	width: 86.1%;
	margin: 6.9% auto 0;
	aspect-ratio: 1 / 1;
	overflow: hidden;
	background: #e9e5db;
}

/* Photos fill the square window exactly like their grid tile — the zoom
   never changes the crop, and a real Instax has no letterbox. Tops/bottoms
   of tall shots crop off; originals are kept server-side if one matters. */
.album-light__window img,
.album-light__window video {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Videos keep their full frame — the bars read as film, not gaps */
.album-light__window video {
	object-fit: contain;
	background: #1c1a17;
}

.album-light__window::after {
	content: '';
	position: absolute;
	inset: 0;
	box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.18);
	pointer-events: none;
}

/* Booth strips zoom as themselves: the frame takes the strip's natural
   ratio (--ar, set inline by upload.js) with the window full-bleed — the
   cream border and footer are baked into the composed image. The chin has
   no printed home, so the attribution is written on the scrim below. */
.album-light--strip .album-light__frame {
	width: auto;
	height: min(76vh, 720px);
	aspect-ratio: var(--ar, 1 / 3);
	background: none;
}

.album-light--strip .album-light__window {
	position: absolute;
	inset: 0;
	width: 100%;
	margin: 0;
	aspect-ratio: auto;
	background: none;
}

.album-light--strip .album-light__chin {
	top: 100%;
	bottom: auto;
	left: -60%;
	right: -60%;
	height: 3.2rem;
}

.album-light--strip .album-light__ink {
	color: #f2eee4;
}

.album-light__note {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	padding: 1.5rem;
	text-align: center;
	font-family: 'Cormorant Garamond', serif;
	font-style: italic;
	font-size: 1.05rem;
	color: #6d675c;
}

.album-light__chin {
	position: absolute;
	left: 6%;
	right: 6%;
	top: 77.9%;
	bottom: 0;
	display: flex;
	align-items: center;
	justify-content: center;
}

/* Attribution ink, revealed left→right like the /home captions — but as a
   transform-only "curtain": the clip wrapper (overflow hidden) starts slid
   fully left and the ink inside counter-slid fully right by the same pixel
   distance (--ink-sweep, set by upload.js), so on screen the text stands
   still while the wrapper's right edge sweeps across it. Both halves share
   one duration/easing, so the sums cancel exactly at every frame, and
   transforms run entirely on the compositor. The previous clip-path reveal
   hit a real Android Chrome bug: mid-write, the compositor kept vsyncing
   but produced no frames for 0.2–1s stretches (traced 2026-09-13) — so
   never move this back to clip-path/mask. Padding + negative margin give
   the script's swashes room without shifting layout. */
.album-light__ink-clip {
	overflow: hidden;
	font-size: min(6vw, 6vh, 1.6rem);
	padding: 0.8em 0.4em;
	margin: -0.8em -0.4em;
	transform: rotate(-2deg) translateX(calc(var(--ink-sweep, 120%) * -1));
	will-change: transform;
}

.album-light__ink {
	display: inline-block;
	font-family: 'New Icon Script', cursive;
	line-height: 1;
	white-space: nowrap;
	color: #2b2620;
	transform: translateX(var(--ink-sweep, 120%));
	will-change: transform;
}

.album-light--written .album-light__ink-clip {
	transform: rotate(-2deg) translateX(0);
	transition: transform var(--write-dur, 1400ms) cubic-bezier(0.35, 0, 0.65, 1);
}

.album-light--written .album-light__ink {
	transform: translateX(0);
	transition: transform var(--write-dur, 1400ms) cubic-bezier(0.35, 0, 0.65, 1);
}

/* Return flight: ink lets go before the polaroid lands back on the pile */
.album-light--fading .album-light__ink-clip {
	opacity: 0;
	transition: opacity 250ms ease;
}

/* Action pills under the polaroid: Download for everyone, Delete on your
   own. The row arrives with the ink, not mid-flight. */
.album-light__actions {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 0.8rem;
	margin-top: 1.4rem;
	opacity: 0;
	pointer-events: none;
	transition: opacity 300ms ease;
}

.album-light__pill {
	padding: 0.5rem 1.2rem;
	border: 1px solid rgba(253, 245, 245, 0.4);
	border-radius: 999px;
	background: rgba(0, 0, 0, 0.25);
	color: #fdf5f5;
	font-size: 0.72rem;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	text-decoration: none;
	cursor: pointer;
}

.album-light__pill:hover {
	border-color: rgba(253, 245, 245, 0.8);
}

.album-light__pill--delete:hover {
	border-color: var(--declined);
}

.album-light--written .album-light__actions {
	opacity: 1;
	pointer-events: auto;
}

/* Return flight: the pills let go with the ink. Must sit after the
   --written rule — both classes are on during the close, and these tie on
   specificity, so file order decides. */
.album-light--fading .album-light__actions {
	opacity: 0;
	pointer-events: none;
	transition: opacity 250ms ease;
}

/* For strips the scrim-written attribution occupies the space below the
   frame, so the pills drop beneath it */
.album-light--strip .album-light__actions {
	margin-top: 4rem;
}

@media (prefers-reduced-motion: reduce) {
	.album-light__ink-clip {
		transform: rotate(-2deg);
		opacity: 0;
	}

	.album-light__ink,
	.album-light--written .album-light__ink {
		transform: none;
		transition: none;
	}

	.album-light--written .album-light__ink-clip {
		transform: rotate(-2deg);
		opacity: 1;
		transition: opacity 0.4s ease;
	}
}

/* ── Selfie booth ──
   The viewfinder is the polaroid: live mirrored camera in the Instax window,
   "smile!" on the chin, round shutter below. Same scrim as the lightbox. */

.selfie-booth[hidden] {
	display: none;
}

.selfie-booth {
	position: fixed;
	inset: 0;
	z-index: 80;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 1.4rem;
	background: rgba(15, 32, 23, 0.9);
}

/* The final beat of a close: the shut curtains fade away */
.selfie-booth--out {
	opacity: 0;
	transition: opacity 320ms ease;
}

/* Velvet curtains, deep green with a gold fringe on the meeting edge —
   shut when the booth appears (hiding the camera warm-up), drawn apart by
   .selfie-booth--open, swept shut again on close before the fade-out. The
   fold stripes are a repeating gradient over the velvet base. (A fancier
   rig — drop-in from above, scalloped swaying hems, rise-away close — was
   built and fully reverted on Lachlan's call, 2026-09-13: this simpler
   version felt better. Don't rebuild it.) */
.selfie-booth__curtain {
	position: absolute;
	top: 0;
	bottom: 0;
	width: 51%; /* slight centre overlap: no seam of light while shut */
	z-index: 5;
	background:
		repeating-linear-gradient(90deg,
			rgba(0, 0, 0, 0.30) 0 8px,
			rgba(255, 255, 255, 0.05) 8px 18px,
			rgba(0, 0, 0, 0.14) 18px 34px),
		linear-gradient(180deg, #173524, #122c1d 40%, #0c2114);
	transition: transform 560ms cubic-bezier(0.55, 0, 0.45, 1);
	will-change: transform;
}

.selfie-booth__curtain::after {
	content: '';
	position: absolute;
	top: 0;
	bottom: 0;
	width: 5px;
	background: linear-gradient(180deg, #d9bd93, var(--gold, #b8966e) 55%, #8a6a45);
}

.selfie-booth__curtain--l { left: 0; }
.selfie-booth__curtain--l::after { right: 0; }
.selfie-booth__curtain--r { right: 0; }
.selfie-booth__curtain--r::after { left: 0; }

.selfie-booth--open .selfie-booth__curtain--l { transform: translateX(-102%); }
.selfie-booth--open .selfie-booth__curtain--r { transform: translateX(102%); }

@media (prefers-reduced-motion: reduce) {
	.selfie-booth__curtain {
		display: none; /* plain show/hide instead of the sweep */
	}
}

.selfie-booth__frame {
	position: relative;
	margin: 0;
	width: min(430px, 88vw, 60vh);
	aspect-ratio: 72 / 86;
	background: linear-gradient(160deg, #fdfbf6, #f2eee4);
	transform: rotate(-1.2deg);
	filter: drop-shadow(2px 4px 10px rgba(0, 0, 0, 0.4));
}

.selfie-booth__window {
	position: relative;
	width: 86.1%;
	margin: 6.9% auto 0;
	aspect-ratio: 1 / 1;
	overflow: hidden;
	background: #1c1a17;
}

.selfie-booth__window video {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
	transform: scaleX(-1); /* preview mirrors, like a mirror — capture matches */
}

/* Countdown numeral over the viewfinder */
.selfie-booth__count {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	font-family: 'Cormorant Garamond', serif;
	font-size: 5.5rem;
	color: #fdfbf6;
	text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55);
	pointer-events: none;
}

.selfie-booth__count--tick {
	animation: selfie-tick 0.75s ease-out;
}

@keyframes selfie-tick {
	from {
		opacity: 0.4;
		transform: scale(1.25);
	}
	to {
		opacity: 1;
		transform: scale(1);
	}
}

.selfie-booth__window::after {
	content: '';
	position: absolute;
	inset: 0;
	box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.18);
	pointer-events: none;
}

/* Fullscreen white burst: doubles as a face-light in a dim reception */
.selfie-booth__flash {
	position: fixed;
	inset: 0;
	z-index: 95;
	background: #fff;
	opacity: 0;
	pointer-events: none;
}

.selfie-booth__flash--on {
	animation: selfie-flash 0.45s ease-out;
}

@keyframes selfie-flash {
	from { opacity: 0.9; }
	to   { opacity: 0; }
}

.selfie-booth__chin {
	position: absolute;
	left: 6%;
	right: 6%;
	top: 77.9%;
	bottom: 0;
	display: flex;
	align-items: center;
	justify-content: center;
}

.selfie-booth__chin span {
	font-family: 'New Icon Script', cursive;
	font-size: min(6vw, 6vh, 1.6rem);
	line-height: 1;
	color: #2b2620;
	transform: rotate(-2deg);
}

/* Shutter: a film-white ring, gold when pressed */
.selfie-booth__snap {
	width: 4.2rem;
	height: 4.2rem;
	border-radius: 50%;
	border: 4px solid #fdfbf6;
	background: rgba(253, 251, 246, 0.25);
	cursor: pointer;
	transition: background 0.15s ease, border-color 0.15s ease;
}

.selfie-booth__snap:active {
	background: var(--accent);
	border-color: var(--accent);
}

.selfie-booth__snap:disabled {
	opacity: 0.4;
	cursor: default;
}

.selfie-booth__close {
	padding: 0.5rem 1.2rem;
	border: 1px solid rgba(253, 245, 245, 0.4);
	border-radius: 999px;
	background: rgba(0, 0, 0, 0.25);
	color: #fdf5f5;
	font-size: 0.72rem;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	cursor: pointer;
}

.selfie-booth__error {
	max-width: min(430px, 88vw);
	margin: 0;
	text-align: center;
	font-size: 0.85rem;
	line-height: 1.6;
	color: #efd5d4;
}

@media (prefers-reduced-motion: reduce) {
	.selfie-booth__flash--on {
		animation: none;
	}
}

/* ── Mobile ──
   48rem = 768px, the usual phone/tablet cut. Tighter side padding on the
   paper (1.4rem matches the form's field rhythm) and a roomier gap between
   the hydrangea seal and the guest name. */
@media (max-width: 48rem) {
	.page {
		padding: 4rem 1.4rem 5rem;
	}

	.rsvp-page .guest-id {
		gap: 1.4rem;
	}
}

/* ── Album moderation (/{MODERATE_URL_SECRET}) ──
   Rides on the upload page's velvet + masthead (body.upload-page), but the
   grid is a plain utility view: 2-up on phones so each picture is big enough
   to judge, whole image shown (contain, never cropped), hidden items dimmed
   with a badge. Built for the bridal party mid-reception, not for show. */
.moderate-filter {
	display: flex;
	justify-content: center;
	gap: 0.6rem;
	margin-top: 1.4rem;
}

.moderate-filter a {
	padding: 0.4rem 1rem;
	border: 1px solid var(--border);
	border-radius: 999px;
	color: var(--surface-text);
	font-size: 0.72rem;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	text-decoration: none;
}

.moderate-filter a.is-active {
	background: var(--btn);
	border-color: var(--btn);
	color: var(--btn-text);
}

.moderate-grid {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: 0.8rem;
	max-width: 960px;
	margin: 1.8rem auto 0;
}

@media (min-width: 40rem) {
	.moderate-grid {
		grid-template-columns: repeat(4, 1fr);
	}
}

.moderate-item {
	display: flex;
	flex-direction: column;
	padding: 0.4rem;
	background: var(--surface);
	box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
}

.moderate-item__media {
	position: relative;
	aspect-ratio: 1;
	background: #1a1a1a;
}

.moderate-item__media a {
	display: block;
	height: 100%;
}

.moderate-item__media img,
.moderate-item__media video {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
}

.moderate-item__badge {
	display: none;
	position: absolute;
	top: 0.4rem;
	left: 0.4rem;
	padding: 0.15rem 0.5rem;
	background: var(--declined);
	color: #fdf5f5;
	font-size: 0.65rem;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	pointer-events: none;
}

.moderate-item__meta {
	flex: 1;
	margin: 0.45rem 0.2rem 0.5rem;
	color: var(--surface-muted);
	font-size: 0.72rem;
	line-height: 1.35;
	overflow-wrap: anywhere;
}

.moderate-item__toggle {
	padding: 0.55rem;
	border: 1px solid var(--declined);
	background: none;
	color: var(--declined);
	font-size: 0.72rem;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	cursor: pointer;
}

.moderate-item__toggle:disabled {
	opacity: 0.5;
}

/* Hidden: dim the picture (still judgeable), badge it, and flip the button
   to the calm green "Unhide" */
.moderate-item--hidden .moderate-item__media img,
.moderate-item--hidden .moderate-item__media video {
	opacity: 0.35;
}

.moderate-item--hidden .moderate-item__badge {
	display: block;
}

.moderate-item--hidden .moderate-item__toggle {
	border-color: var(--btn);
	background: var(--btn);
	color: var(--btn-text);
}

.moderate-paging {
	margin: 1.6rem 0 0;
	text-align: center;
}

.moderate-paging a {
	color: rgba(242, 238, 228, 0.85);
	letter-spacing: 0.1em;
}
