/*
 * Guest-facing styling. Placeholder proportions until the real design exists —
 * everything here reads from the custom properties in site.css, so a redesign is
 * mostly a matter of changing those values.
 */

/*
 * The invitation's own palette, built out from the artwork. --color-bg is the card's own
 * colour, sampled off img/pearl_bg.jpg — the whole image and its clear centre both average
 * to it — so it stands in seamlessly while the card image loads, and fills any edge the
 * image doesn't cover. It is within a couple of steps of the envelope's own tone, which is
 * what makes the two read as the same paper.
 *
 * The rest of the palette moves because the background did. Cream is a light background,
 * and the greys inherited from site.css were picked against near-white: on #e5dacd the old
 * muted grey lands at 3.9:1 and the old gold accent at 2.8:1, both under the 4.5:1 that
 * body text needs. These are the same hues pushed darker until they clear it.
 *
 * Scoped to .invite-body on purpose — the admin screens keep the plain light theme.
 */
/*
 * Great Vibes, self-hosted rather than pulled from a CDN: these links are private, and the app
 * shouldn't announce a guest's visit to a third party. SIL Open Font License — the licence text
 * ships beside the files in wwwroot/fonts/, which is what it asks for.
 *
 * Two files split by unicode-range, exactly as Google serves it. A browser fetches only the one
 * it needs, so Cyrillic names cost 28KB and never pull the Latin set as well.
 */
/*
 * Raleway for everything that isn't the couple's names — the face the revised design is set in.
 * A variable font, so one file covers 300 to 700 and no weight needs its own download. The two
 * subsets are Google's own split, and the Cyrillic one was checked against the whole Macedonian
 * alphabet before it went in: ѓ ќ ѕ џ љ њ ј are all present, which is not a given in a Latin-first
 * face and is the thing that would have shown up as a fallback mid-sentence.
 *
 * It replaced Montserrat, whose files are gone with it — a body face nothing references is 57KB
 * of nothing on a phone.
 */
@font-face {
    font-family: "Raleway";
    font-style: normal;
    font-weight: 300 700;
    font-display: swap;
    src: url("/fonts/raleway-cyrillic.woff2") format("woff2");
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
    font-family: "Raleway";
    font-style: normal;
    font-weight: 300 700;
    font-display: swap;
    src: url("/fonts/raleway-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
        U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
        U+FEFF, U+FFFD;
}

/* Only ever renders the date, which is digits and full stops — so the Latin subset is the whole
   of what it needs, and there is no Cyrillic file to fetch. */
@font-face {
    font-family: "Playfair Display";
    font-style: italic;
    font-weight: 400 700;
    font-display: swap;
    src: url("/fonts/playfair-italic-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+2122, U+FEFF, U+FFFD;
}

/*
 * Azbuka03_D, by Nikola Kovanovic — the face the names are set in. Chosen for one letter: its Д
 * is a true Cyrillic cursive Д rather than a Latin D wearing Cyrillic's clothes, which is what
 * every script face we tried gets wrong, Great Vibes included. It covers the whole Macedonian
 * alphabet, so renaming the couple in the admin cannot break the cover.
 *
 * Free for non-commercial use, which this is. See LICENSE-Azbuka03_D.txt beside these files —
 * the terms are narrower than the OFL fonts here, so they are worth reading before reusing any
 * of this elsewhere. Subset from the original TTF, kept in design/fonts/.
 */
@font-face {
    font-family: "Azbuka";
    font-style: normal;
    font-weight: 400;
    /* The names are the whole cover; better a beat of Georgia than a beat of nothing. */
    font-display: swap;
    src: url("/fonts/azbuka-cyrillic.woff2") format("woff2");
    unicode-range: U+0020-0040, U+0400-045F;
}

@font-face {
    font-family: "Azbuka";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/azbuka-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+2000-206F;
}

@font-face {
    font-family: "Great Vibes";
    font-style: normal;
    font-weight: 400;
    /* The names are the whole cover; better a beat of Georgia than a beat of nothing. */
    font-display: swap;
    src: url("/fonts/great-vibes-cyrillic.woff2") format("woff2");
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
    font-family: "Great Vibes";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/fonts/great-vibes-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
        U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
        U+FEFF, U+FFFD;
}

.invite-body {
    /*
     * Two script faces, and the split is deliberate.
     *
     * --font-names is Azbuka, and it is used for one thing wherever that thing appears: the
     * couple's own names. The cover, and their signature under the passage. It is here for one
     * letter — its Д is a true Cyrillic cursive Д rather than a Latin D in Cyrillic's clothes,
     * which is what every other script face gets wrong, Great Vibes included. Their names are
     * exactly where that has to be right.
     *
     * --font-script is Great Vibes, which is what the revised design sets its script in, and it
     * takes the uses that are not their names: the addressee and the programme's heading. At that
     * size its Cyrillic reads as calligraphy rather than as a wrong letterform.
     */
    --font-names: "Azbuka", "Great Vibes", "Snell Roundhand", cursive;
    --font-script: "Great Vibes", "Snell Roundhand", cursive;
    /* Overrides the serif from site.css for guest-facing pages only; admin stays as it was. */
    --font-date: "Playfair Display", Georgia, serif;
    --font-body: "Raleway", "Helvetica Neue", Arial, sans-serif;
    --font-display: "Raleway", "Helvetica Neue", Arial, sans-serif;
    --color-bg: #e5e2d7;
    /* Lighter than the page behind the invitation, so the sealed envelope sits on something
       nearer to white than the artwork's own ground. */
    --envelope-surround: #f6f5f2;
    --color-surface: #f2ece2;
    --color-ink-muted: #5e574e;

    /*
     * The gold, and darker than it was — #74572f measured 5.15:1 against the live background.
     * That passes AA, but the nominal ratio flatters what is actually on the page: the eyebrows,
     * the month and year either side of the date, and the programme's times are all small caps
     * tracked out to a third of an em, sitting on a photographed, embossed ground. At that size
     * and that spacing, AA is the floor rather than the target.
     *
     * #5f4726 is 6.70:1, a thirty per cent gain. It stops here rather than going darker for a
     * reason that only shows on the page: the gold has to be legible against the paper *and*
     * distinguishable from the ink beside it. At 7.8:1 it is 1.4:1 from --color-ink and has quit
     * being gold — the eyebrows stop reading as an accent and start reading as more body text in
     * a slightly odd brown.
     */
    --color-accent: #5f4726;
    --color-border: #cfc2b0;

    /*
     * The section rules, and the one line on the page that is not --color-border.
     *
     * --color-border is a form colour: it edges inputs and buttons against the cream, where a
     * hairline only has to be found, not seen. A section rule has to carry across an embossed
     * floral backdrop from two metres away, and at that weight #cfc2b0 simply vanishes into the
     * artwork — the rules were on the page for a whole design pass before anyone could tell they
     * were there. This is --color-accent softened, which reads as drawn rather than as an edge.
     */
    --color-rule: rgba(95, 71, 38, 0.45);

    /* The same gold again, fainter — for rules that separate items *within* a section rather than
       one section from the next. A guest separator at the same weight as the rule that opened the
       RSVP made four things look like four sections. */
    --color-rule-soft: rgba(95, 71, 38, 0.22);

    /*
     * The air on each side of a section rule, and deliberately bigger than --space-lg.
     *
     * The spacing scale is sized for the gaps *within* a block — between a heading and its list,
     * between two rows. Reusing its largest step between two sections left the invitation reading
     * as one continuous column with occasional lines in it: measured against the text column, the
     * separation came out about a quarter of the column's width where it wants to be closer to a
     * third. This is its own token because it is a different job, not a bigger version of the
     * same one.
     */
    --section-gap: clamp(2.25rem, 7svh, 4.75rem);
}

/*
 * Held still behind the whole invitation, so the florals frame the page rather than
 * scrolling past. z-index: -1 puts it behind the content but still in front of the canvas
 * colour, which is what --color-bg above is for while the image is still arriving.
 *
 * Anchored to the top so the opening screen always gets the same corner of the artwork,
 * whatever the screen's shape.
 */
.page-backdrop {
    position: fixed;
    /* Sized to the largest viewport rather than to inset: 0, which resolves against the
       *visual* viewport and therefore changes the moment a phone's address bar slides away. A
       decorative layer that re-lays-out every time the browser chrome animates is the bounce a
       guest sees when they stop scrolling. lvh never changes, so this simply runs on under the
       bar and stays still. */
    inset: 0 0 auto 0;
    height: 100vh;
    height: 100lvh;
    z-index: -1;
    background: var(--color-bg) url("/img/pearl_bg.jpg") center top / cover no-repeat;
    pointer-events: none;
}

/*
 * On a wide screen the portrait artwork is used turned on its side, filling the window edge to
 * edge. Rotated it is 940x528 — near enough 16:9 that a desktop window crops almost nothing,
 * where the upright version had to be scaled past 2x to cover the same space and the florals
 * ballooned.
 *
 * The florals read as texture rather than as a picture with a right way up, so the rotation
 * doesn't announce itself. Turned clockwise on purpose: that leaves the clear band through the
 * middle of the artwork running down the middle of the window, which is where the text sits.
 *
 * The alternative, kept until now, was to stop the backdrop growing past 46rem and centre it,
 * so it read as a portrait card lying on a cream surface. Swap this rule back out for that if
 * the full-bleed version loses the sense of a card.
 */
@media (min-width: 46rem) {
    .page-backdrop {
        background-image: url("/img/pearl_bg_landscape.jpg");
        /* Centred rather than anchored to the top: with the long edge across the window there
           is no particular corner worth holding on to. */
        background-position: center;
    }
}
/* --- Music --- */

/* Mirrors the language switch in the opposite corner, and above the envelope for the same reason:
   a guest must be able to silence it the moment it starts, not only after the letter opens. */
.music {
    position: fixed;
    top: 0.5rem;
    left: 0.5rem;
    z-index: 30;
}

.music[hidden] {
    display: none;
}

.music-toggle {
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    min-height: 0;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: 50%;
    background: color-mix(in srgb, var(--color-bg) 80%, transparent);
    color: var(--color-ink-muted);
    cursor: pointer;
    backdrop-filter: blur(6px);
}

.music-toggle svg {
    width: 1.05rem;
    height: 1.05rem;
}

/* One icon, two states: the waves and the cross are both in the markup and CSS picks. Swapping
   paths in script would let the icon and the button's aria-pressed drift apart. */
.music-off-cross { display: none; }
.music.is-muted .music-on-waves { display: none; }
.music.is-muted .music-off-cross { display: block; }

.music:not(.is-muted) .music-toggle {
    color: var(--color-accent);
    border-color: var(--color-accent);
}

/* --- Language switch --- */

/* Pinned to the corner rather than sitting in the flow, so the opening screen is only the
   butterfly and the names. Above the envelope (z-index 20) on purpose — a guest should be
   able to pick their language before opening it, not only after. */
.lang-switch {
    position: fixed;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 30;
    display: flex;
    gap: 1px;
}

.lang-option {
    background: color-mix(in srgb, var(--color-bg) 80%, transparent);
    border: 1px solid var(--color-border);
    color: var(--color-ink-muted);
    font: inherit;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    /* Deliberately below the usual 44px target. These are small on request, and the cost is
       real: they are fiddlier to hit with a thumb than everything else on the page. Kept at
       32px rather than smaller so they stay tappable at all. */
    min-height: 2rem;
    /* Two characters would otherwise shrink the boxes to almost nothing; the min-width keeps
       them a target rather than a pair of dots. */
    min-width: 2.5rem;
    padding: 0.3rem 0.5rem;
    font-size: 0.7rem;
    cursor: pointer;
    backdrop-filter: blur(6px);
}

.lang-option:first-child {
    border-radius: var(--radius) 0 0 var(--radius);
}

.lang-option:last-child {
    border-radius: 0 var(--radius) var(--radius) 0;
}

.lang-option[aria-current="true"] {
    border-color: var(--color-accent);
    color: var(--color-accent);
}

.invite {
    text-align: center;
}

/* --- The opening screen --- */

/*
 * Ordinary scrolling. This was a three-panel scroll-snap layout, read a page at a time like a
 * printed card being turned over — and on a phone it fought the guest: every flick became a
 * committed jump, and a form that runs past a screen is the last thing that should behave that
 * way. The cover still fills the screen because it is a cover; nothing below it does.
 */

/* The cover sizes itself, so the page's own top padding would push it past one screen. */
/*
 * No padding above or below the snap areas.
 *
 * The top would push the cover past one screen. The bottom is subtler: padding under the last
 * panel is scrollable space belonging to no snap area, and under mandatory snapping there is no
 * valid position to rest in it — so scrolling to the end of the page dragged the guest back up
 * by exactly that much. The last panel carries its own bottom padding instead, inside the snap
 * area where it can be scrolled to.
 */
.invite-body .page {
    padding-top: 0;
    padding-bottom: 0;
}

/*
 * The cover, and the one thing still sized to the screen.
 *
 * svh — the viewport height with the browser's address bar fully shown — because it is the only
 * one of the three that never changes. dvh tracks the bar as it slides away, which means the
 * cover resizes continuously while a guest is scrolling slowly through it, and the whole page
 * shifts under their finger.
 *
 * That was a fair trade while this page snapped: the resize happened once, at a jump. It is not
 * a fair trade against ordinary scrolling.
 *
 * The cost of svh is that once the bar retracts the cover is short of the visible area by its
 * height, so a sliver of the invitation shows below it. Under snapping that read as a broken
 * panel; in a page that simply scrolls it reads as there being more below, which there is.
 *
 * vh first as the fallback for anything that doesn't know svh.
 */
.screen {
    /*
     * A whole screen, and back to it deliberately.
     *
     * It was 85svh for a while. A full-height cover had put the addressee a whole viewport down,
     * and since .hero centres the names in whatever room the butterfly leaves, half that slack
     * fell *below* the scroll cue as a band of nothing — shortening the cover was the only thing
     * that could move the invitation up, because the seam is the cover's own height. The trade
     * was that the top of the invitation showed under the cover on arrival.
     *
     * That reason went when the addressee moved onto the cover: there is nothing waiting a
     * viewport down any more, so the cover can have the whole screen and the invitation starts
     * where the scroll starts. The cue is what says there is more below; showing a line of the
     * next section says it twice and gives the opening away.
     *
     * svh, not dvh — this must not resize under the guest as a phone's address bar retracts.
     */
    min-height: 100vh;
    min-height: 100svh;
    display: flex;
    flex-direction: column;
    padding-top: var(--space-lg);
}

/*
 * What was the middle screen, now just the top of the invitation.
 *
 * Its full-viewport height and vertical centring are gone with the snapping — both existed to
 * make it read as a page, and against ordinary scrolling they only introduced a gap.
 *
 * The tightened spacing stays. It was written to make this fit one screen, which no longer
 * matters, but it earns its place anyway: on a short phone the fixed 3rem rhythm left the
 * invitation mostly air. It is inherited, so the detail rows and their padding come with it.
 *
 * svh rather than dvh here on purpose — these are a scaling reference, not a fit, and pinning
 * them to the smallest viewport keeps every gap on the page from being recalculated each time
 * the chrome slides away.
 */
.invite-panel {
    --space-lg: clamp(1.25rem, 4.6svh, 3rem);
    --space-md: clamp(0.75rem, 2.4svh, 1.5rem);
    --space-sm: clamp(0.4rem, 1.2svh, 0.75rem);
}

/*
 * Capped against the screen's height as well as its width. A short landscape phone is the case
 * that needs it: 16vw of a wide viewport is a fine numeral until there are only 400px of height
 * to put it in.
 *
 * The floor is the design's 88px and is deliberately not lowered — on a narrow phone the vw and
 * svh terms both fall under it and the clamp holds it there, which is the whole point. This used
 * to bottom out at 2.2rem, from when the cover and the panel had to share one screen.
 */
.invite-panel .invite-date {
    --day-size: clamp(5.5rem, min(16vw, 16svh), 8.75rem);
}

.invite-panel .countdown-unit span {
    font-size: clamp(1rem, min(6vw, 3.4svh), 2rem);
}

.invite-rest {
    /*
     * No top padding, deliberately — and it used to have a lot.
     *
     * This was the seam between the invitation the guest reads and the part they answer, set to
     * read as the printed card ending. The section rules do that job now, and the two were being
     * paid for at once: padding on a box does not collapse with the margins either side of it, so
     * the venue's bottom margin, this seam and the divider's own top margin all *added*. The gap
     * above "will you be there" came out at three times every other section break on the page.
     *
     * Leaving it at zero lets the divider's --section-gap collapse through as it does everywhere
     * else, so this break is the same size as the rest.
     */
    /*
     * Tighter section breaks than the invitation above.
     *
     * Not an inconsistency — the design spaces the two halves differently, and for a reason that
     * shows once you look at them side by side. The read half is a printed card, and its sections
     * are each given a lot of air. This half is a form: the guest is working down it, and 4.75rem
     * of nothing between answering the last person and reaching the bus reads as the page having
     * ended.
     */
    --section-gap: clamp(1.5rem, 4svh, 3rem);

    /* Clears whatever is fixed to the bottom of the viewport. Nothing is any more — kept for the
       next control that lands there, because without it the last line of the invitation sits
       underneath one, which is how the seating note ended up unreadable on a phone. */
    padding-bottom: var(--space-lg);
}

/* Takes the room the butterfly leaves, and centres the names in it. */
.hero {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: var(--space-md);
}

.hero .invite-names {
    font-size: clamp(3rem, 15vw, 5.4rem);
}

/* Says "there is more below" without spelling it out. */
.scroll-cue {
    width: 0.75rem;
    height: 0.75rem;
    margin-top: var(--space-lg);
    border-right: 1px solid var(--color-accent);
    border-bottom: 1px solid var(--color-accent);
    transform: rotate(45deg);
    opacity: 0.6;
    animation: scroll-cue 2.4s ease-in-out infinite;
}

@keyframes scroll-cue {
    50% { transform: rotate(45deg) translate(0.2rem, 0.2rem); opacity: 0.25; }
}

/*
 * The two names and the flourish interlock rather than stacking: the first name sits right of
 * centre, the flourish tucks in under it at the left, and the second name comes back across it.
 *
 * Every offset is in em, so the whole arrangement scales with the type and holds from a phone to
 * a desktop. These four numbers are the composition — the two translateX values and the flourish's
 * two negative margins — and they are the only things to touch to rebalance it.
 *
 * transform rather than margins for the horizontal shifts: transforms don't affect layout, so
 * pushing the names sideways can't widen the column or bring on a horizontal scrollbar.
 */
.invite-names {
    display: flex;
    flex-direction: column;
    align-items: center;
    font-family: var(--font-names);
    font-size: clamp(2.6rem, 12vw, 4.2rem);
    line-height: 1.05;
    font-weight: 400;
    margin: 0;
}

/*
 * No focus ring on the names.
 *
 * envelope.js moves focus here when the envelope finishes, so that keyboard and screen reader
 * users are not left on a button that has just been removed from the page. The heading carries
 * tabindex="-1", which means it is a focus *target* but never reachable by Tab — so the ring can
 * only ever appear after that programmatic move, on an element there is nothing to do with.
 *
 * Removing it costs no one anything: the announcement still happens, and nothing here is
 * interactive. It is also the whole cover, so the browser traced the stepped outline of two
 * spans and a flourish straight across the artwork.
 */
.invite-names:focus {
    outline: none;
}

.invite-names .name-first {
    transform: translateX(0.15em);
}

.invite-names .name-second {
    /* Further right than the first name, which is what opens the gap the flourish tucks into.
       Raise this and the second name clears the flourish; lower it and they interlock harder. */
    transform: translateX(0.55em);
}

.invite-names .amp {
    /* How wide the flourish runs. The one number to change when it crowds the second name's
       first letter — which is a property of the face, not of the flourish: a wider K reaches
       further left, so a change of script font is exactly when this needs revisiting. */
    --amp-width: 0.95em;

    display: block;
    /* Pulled up into the first name's descenders and down into the second's ascenders. */
    margin: -0.4em 0 -0.52em;
    transform: translateX(-1.2em);
    /* No colour of its own: the flourish is part of the same written line as the names, and a
       gold swash between two dark names reads as a separate ornament dropped in between them. */
}

/* Sized against the names rather than in rem, so it keeps its proportion as they scale. */
.invite-names .amp svg {
    width: var(--amp-width);
    height: auto;
    display: block;
    overflow: visible;
}

/*
 * The addressee is set in the script face, like the couple's own names — it is the one line on
 * the invitation written to this household in particular.
 *
 * It lives on the cover now rather than at the head of the panel, which changes what it has to
 * be: beside the names it is the third line of one written phrase, so it has to sit clearly
 * below them in weight or it reads as a third name. Capped against the screen's height as well
 * as its width for the same reason the date is — the cover has to hold the names, this, and the
 * scroll cue on a short phone.
 */
.invite-addressee {
    font-family: var(--font-script);
    font-size: clamp(1.15rem, min(6vw, 3.4svh), 2rem);
    line-height: 1.2;
    margin: 0;
    color: var(--color-accent);
    font-style: normal;
}

.invite-message {
    margin: 0 auto var(--space-lg);
    max-width: 26rem;
    line-height: 1.8;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 0.9rem;
}

.invite-message p {
    margin: 0;
}

/* The closing line of the message carries the weight — "our wedding celebration" — the way it
   does on the printed card. */
.invite-message .lead-out {
    font-weight: 700;
}

/*
 * The seating note, flanked by a small mark either side as the design has it.
 *
 * The glyph is U+00D7, the multiplication sign, and not the ✕ the design uses — U+2715 falls
 * outside Raleway's subsets and would come from whatever the phone happens to have, or from
 * nothing. At this size the two are indistinguishable, and this one is in the file.
 *
 * Pseudo-elements, so a screen reader reads the note and not "multiplication sign" twice.
 */
.invite .table-note {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.9rem;
    max-width: 26rem;
}

.invite .table-note::before,
.invite .table-note::after {
    content: "\00D7";
    flex: none;
    font-size: 0.88rem;
    color: var(--color-accent);
    opacity: 0.6;
}

/* --- The RSVP confirmation --- */

/*
 * What a party that has already answered sees in place of the form: the fact that it saved, the
 * date it can still be changed, and the way back into the form.
 *
 * A faint gold ground rather than --color-surface. It has to read as a panel resting on the card
 * rather than as another cream box on a cream page, and it is the one element on the invitation
 * that is reporting state rather than presenting the wedding.
 */
.rsvp-confirmation {
    /* Sits directly under the last person answered, which is where the guest is looking at the
       moment it becomes true. Its own top margin rather than the guest rows' rhythm — it is a
       reply to the list above it, not another row of it. */
    margin: var(--space-lg) 0 0;
    padding: var(--space-md);
    border: 1px solid var(--color-rule);
    background: rgba(95, 71, 38, 0.05);
    text-align: center;
}

/*
 * Flanked by the same four-pointed star the dividers carry, as a background image rather than
 * markup — it is ornament twice over, and two more inline SVGs in the middle of a Razor branch
 * buy nothing. The gold is baked into the data URI, which is the one cost: this is the single
 * place on the page where a colour is not coming from a token.
 */
.rsvp-confirmation-title {
    margin: 0;
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.4em;
    text-indent: 0.4em;
    color: var(--color-accent);
}

.rsvp-confirmation-title::before,
.rsvp-confirmation-title::after {
    content: "";
    display: inline-block;
    width: 0.35rem;
    height: 0.35rem;
    margin: 0 0.9em;
    vertical-align: middle;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%235f4726'%3E%3Cpath d='M12 0c0 6.6 5.4 12 12 12-6.6 0-12 5.4-12 12 0-6.6-5.4-12-12-12 6.6 0 12-5.4 12-12Z'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* Inside the panel, so it is fainter than the rules that part sections — it is dividing a status
   line from the control under it, not one part of the evening from the next. */
.rsvp-confirmation-rule {
    border: none;
    border-top: 1px solid var(--color-rule-soft);
    margin: var(--space-md) 0;
}

.rsvp-confirmation-note {
    margin: 0;
    font-size: 0.65rem;
    line-height: 1.9;
    letter-spacing: 0.15em;
    color: var(--color-ink-muted);
}

/* --- The couple's words --- */

.invite-quote {
    max-width: 24rem;
    margin: 0 auto var(--space-lg);
    text-align: center;
}

/* Centred, unlike the section eyebrows that label a block of facts — this one is a title over a
   passage rather than a label beside it. */
.quote-eyebrow {
    letter-spacing: 0.4em;
    text-indent: 0.4em;
}

/*
 * The passage itself, set in the date's serif italic.
 *
 * That face is loaded italic-only and Latin-only — it exists here for the date's digits. The
 * italic is therefore free, but the Cyrillic is not: Macedonian text falls through to the body
 * face, so the two languages do not read identically. Accepted deliberately. The alternative is
 * a second Playfair subset for a passage of a dozen words, and the fallback is Raleway italic,
 * which is a perfectly good voice — just a different one.
 */
.quote-body {
    position: relative;
    margin: 0;
    padding: 0 var(--space-md);
    font-family: var(--font-date);
    font-style: italic;
    font-size: clamp(0.95rem, 3vw, 1.15rem);
    line-height: 1.85;
    color: var(--color-ink);
}

.quote-body p {
    margin: 0;
    /* Above the quotation marks, which are set large enough to run under the first and last
       lines rather than sitting clear of them. */
    position: relative;
    z-index: 1;
}

/*
 * The quotation marks. Pseudo-elements because they are ornament: written into the markup they
 * would be announced before and after the one personal thing on the page.
 *
 * Both glyphs were checked against the Playfair subset actually shipped rather than against its
 * declared unicode-range — U+201C and U+201D are in the file, so they draw. A missing one would
 * have been a tofu box rather than nothing.
 */
.quote-body::before,
.quote-body::after {
    position: absolute;
    font-family: var(--font-date);
    font-size: 3.25rem;
    line-height: 1;
    color: var(--color-accent);
    opacity: 0.3;
}

.quote-body::before {
    content: "\201C";
    top: -0.6rem;
    left: 0;
}

.quote-body::after {
    content: "\201D";
    bottom: -1.4rem;
    right: 0;
}

/*
 * The signature under the passage — and it is the couple's names, so it takes the couple's face
 * rather than the script the eyebrow and the addressee use. --font-names is not "the font of the
 * cover", it is "the font their names are set in", and this is the second place they are signed.
 *
 * The ampersand comes from Azbuka too: U+0026 falls inside its Cyrillic subset's declared range
 * and is present in the file, so it draws in the same hand rather than dropping to the fallback
 * mid-signature.
 */
.quote-signature {
    margin: var(--space-md) 0 0;
    font-family: var(--font-names);
    font-size: clamp(1.15rem, 5vw, 1.5rem);
    line-height: 1.3;
    color: var(--color-accent);
}

/* Spaced rather than padded, so it stays with the names when the line wraps on a narrow phone. */
.quote-amp {
    padding: 0 0.35em;
}

/* --- Dividers --- */

/*
 * The rule between two sections: two hairlines with a four-pointed star held between them.
 *
 * Everything here used to be separated by borders built into the components themselves — the
 * date block's vertical rules, the detail rows' bottom borders, the countdown's band, the
 * transport <hr>. None could be reused, so every new section invented its own separation and
 * they all sat at slightly different widths. This is that one job, extracted.
 *
 * A flex row rather than a bordered box: the two halves are ::before and ::after at flex: 1, so
 * they stay equal at any width without either of them being measured. The star comes from
 * _Divider.cshtml, drawn as a path — see that file for why it is not the "✦" character.
 */
.invite-divider {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    width: 100%;
    /*
     * The same space above and below, and it has to be --section-gap rather than --space-lg for a
     * reason that is invisible until you measure it: every section above a rule already carries a
     * --space-lg bottom margin, and adjacent margins collapse to the larger of the two. Matched to
     * --space-lg the rule got the section's lg above it and its own lg below — symmetric, but the
     * whole separation was then only as big as a within-block gap.
     *
     * --section-gap is larger than every section's own bottom margin, so the collapse always
     * resolves to it on both sides: symmetric, and the same air whichever two sections meet.
     * It is the one knob for the page's vertical rhythm.
     */
    margin: var(--section-gap) auto;
}

.invite-divider::before,
.invite-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--color-rule);
}

/* Solid gold against the softened rules either side, so the star is the thing the eye lands on
   and the lines read as leading into it. */
.invite-divider-mark {
    flex: none;
    width: 0.4rem;
    height: 0.4rem;
    color: var(--color-accent);
}

/*
 * On the cover it is doing something else: joining the names to the household they are written
 * to, rather than parting two sections. Short, and with no margin of its own, because .hero
 * already spaces its children with a gap.
 *
 * Reached by descent rather than by a modifier class, so _Divider.cshtml needs no model — and
 * a partial that declares one cannot be called without it, which is what the eight plain call
 * sites would each have had to carry.
 */
.hero .invite-divider {
    width: 9rem;
    margin: 0;
}

/*
 * Month, day and year on one line, with the day carrying all of it.
 *
 * The hairlines that used to divide the three cells are gone: with a rule now above and below
 * every section, two more standing inside the date made it the busiest thing on a page whose
 * loudest element is meant to be a single number. Air separates them instead, which is why the
 * gap is wide and set against the viewport rather than the spacing scale.
 */
.invite-date {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    justify-items: center;
    /* One source for the day's size: the label rule below needs it to work out how far to drop,
       and it cannot read another element's font-size. */
    --day-size: clamp(5.5rem, 16vw, 8.75rem);
    /*
     * How far the digits' ink sits below the middle of their own line box, as a fraction of the
     * day's size. Measured from Playfair Display Italic rather than nudged by eye:
     * (ascender 1.082 − descender 0.251) / 2 − (inkTop 0.529 + inkBottom −0.014) / 2 = 0.158em.
     * Usefully it does not depend on line-height — that term cancels — so it holds at any size.
     */
    --day-optical-drop: 0.158;
    gap: clamp(1.25rem, 7vw, 5rem);
    /* Wide enough for the numeral at full size with the design's gaps either side of it. At 20rem
       the three cells were being squeezed together long before the type stopped growing. */
    max-width: 30rem;
    /* The same gap as between two sections, but with no rule in it — which is what says the
       countdown below belongs to this date rather than following it. */
    margin: 0 auto var(--space-lg);
    color: var(--color-ink);
}

/*
 * The month and year are lettered gold rather than more of the date face. They are labels on the
 * number, not part of it — set in the same italic serif at a smaller size they read as the same
 * word broken into three, which is what made the block feel like a price rather than a date.
 */
.invite-date-part {
    font-family: var(--font-body);
    /* A fifth larger than the design's 13px. These two are the smallest thing on the page that
       has to be read rather than merely seen, and they sit where the backdrop's florals are
       busiest. */
    font-size: clamp(0.84rem, 3.6vw, 0.96rem);
    /*
     * 500, not the design's 300.
     *
     * This is the change that actually fixed it. A 300 weight tracked out to nearly half an em
     * is a row of hairlines with air between them, and over an embossed photograph the strokes
     * and the artwork's own highlights are the same weight — the letters do not fail to contrast
     * so much as fail to group, and they read as part of the flower. Colour and size help; the
     * stroke is what was missing.
     */
    font-weight: 500;
    line-height: 1;
    text-transform: uppercase;
    letter-spacing: 0.45em;
    /*
     * Letter-spacing is added after every letter including the last, so the cell is half a
     * character wider than the word inside it and the glyphs sit left of where the box says they
     * are. This claws that trailing space back, which is the only way the cell's edges become the
     * word's edges.
     *
     * It replaces a text-indent that pushed the text right instead. That was wrong twice over —
     * the wrong size, and it grew the shrink-to-fit box rather than moving anything inside it, so
     * both labels drifted right together and the date ended up nearer 2026 than ОКТ.
     */
    margin-right: -0.45em;
    /*
     * A step darker than --color-accent, and the only place on the page that departs from it.
     * The gold is chosen to stay gold against the ink beside it, which is the right compromise
     * for an eyebrow sitting in a column of type — these two sit alone over the artwork, with no
     * ink near them to be confused with, so they can spend the whole budget on being legible.
     * 8.35:1 against the flat ground, 9.4:1 against a raised floral highlight.
     */
    color: #4d3a1e;
}

/*
 * The gaps either side of the day are the grid's gap and nothing else.
 *
 * Centring each label in its own column looked right only while the two words happened to be the
 * same width — "ОКТ." is narrower than "2026", so each sat at its column's middle and the day
 * ended up further from one than the other. Pushing each label against the day instead makes the
 * two gaps the same measurement by construction, whatever the month is called in whichever
 * language, and the day stays centred on the page because the columns either side are still 1fr.
 */
/*
 * Dropped to the digits' optical centre.
 *
 * align-items: center lines up the three boxes, and for the two labels the box and the letters
 * are the same thing. For the day they are not: digits occupy the top two thirds of a line box,
 * from the baseline up to figure height, with the descender space empty beneath them. Centred by
 * box, the day therefore hangs visibly below a line drawn through the month and the year.
 *
 * The labels move rather than the day: the day is the largest thing on the page and its position
 * sets the spacing to the message above and the countdown below, whereas nothing depends on where
 * these two sit.
 */
.invite-date-part {
    transform: translateY(calc(var(--day-size) * var(--day-optical-drop)));
}

.invite-date-part:first-child {
    justify-self: end;
}

.invite-date-part:last-child {
    justify-self: start;
}

/*
 * The day is the loudest thing on the invitation, and by a long way — the design sets it at
 * 88px on the narrowest phone and lets it reach 140px. It was topping out at 4.6rem, a little
 * over half that, which made the date read as one more line of the card rather than as the thing
 * the card is about.
 */
.invite-date-day {
    font-family: var(--font-date);
    font-size: var(--day-size);
    line-height: 1;
}

/* --- Venue --- */

/*
 * Where and when, as one block. It replaced a list of icon rows when the programme arrived: a
 * clock row reading "19:00" directly above a timeline whose first line also reads 19:00 was the
 * same fact told twice, and once the clock went the pin was the only icon left and read as
 * decoration rather than as a heading.
 *
 * Left aligned inside a centred column, against a hairline that runs the height of the block —
 * the same rule as everything else on the page, turned on its side.
 */
.venue-block {
    max-width: 20rem;
    margin: 0 auto var(--space-lg);
    padding-left: var(--space-md);
    text-align: left;
}

/*
 * The hairline beside the address, and only beside the address.
 *
 * It was a border-left on the whole section, which ran it down past the time and the map link —
 * a rule that long stops reading as a mark against a name and starts reading as a column edge
 * the button is sitting inside. The design has it short, ending with the town.
 *
 * Absolute against this wrapper rather than a fixed height, so it is exactly as tall as what it
 * marks: the venue's name wraps to two lines at some widths, and a fixed rule would stop
 * mid-word there and hang past the text everywhere else.
 */
.venue-head {
    position: relative;
}

.venue-head::before {
    content: "";
    position: absolute;
    left: calc(-1 * var(--space-md));
    /* Starts on the eyebrow's cap height rather than its line box, which sits a couple of pixels
       above the letters and left the rule looking mis-hung. */
    top: 0.15rem;
    bottom: 0;
    width: 1px;
    background: var(--color-rule);
}

.venue-block p {
    margin: 0;
}

/* The name of the place, and the loudest thing in the block. */
.detail-venue {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    font-size: 0.95rem;
    line-height: 1.5;
}

/* Lettered like the name above it but quiet — it is the second half of the same address. */
.venue-address {
    text-transform: uppercase;
    letter-spacing: 0.14em;
    font-size: 0.8rem;
    line-height: 1.6;
    color: var(--color-ink-muted);
}

.venue-time {
    margin-top: 0.35rem;
    color: var(--color-accent);
    letter-spacing: 0.14em;
    font-size: 0.8rem;
}

/* Only present when the couple has set a reception window, where the bare time would be saying
   something they haven't — "arrive between these two" rather than "this is when it starts". */
.venue-time-label {
    display: block;
    text-transform: uppercase;
    letter-spacing: 0.28em;
    font-size: 0.65rem;
    color: var(--color-ink-muted);
}

/* --- Countdown --- */

/*
 * The rules that used to band this top and bottom went with the move up under the date. They
 * were there to keep it clear of the RSVP's closing date, which it no longer sits beside — and
 * directly under the date it is not a separate section at all but the same statement continued,
 * so a rule between the two would be arguing with the reading.
 */
.countdown {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-sm);
    margin-bottom: 0;
    padding: 0;
}

.countdown-unit span {
    display: block;
    font-size: clamp(1.4rem, 6vw, 2rem);
    /* Digits change every second — a tabular font stops the row from jittering. */
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}

.countdown-unit small {
    display: block;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-ink-muted);
}

.countdown.is-now {
    grid-template-columns: 1fr;
    font-size: 1.2rem;
}

/* --- Programme --- */

/*
 * The evening in order, as a timeline: a hairline running down the left with a ring on it per
 * line. The rail is drawn on the list rather than as a border on each item, so it stops at the
 * first and last dot instead of running past them into the section's own padding.
 */
.invite-programme {
    max-width: 22rem;
    margin: 0 auto var(--space-lg);
    text-align: left;
}

/*
 * Set in the script face, like the names and the addressee. It is the only heading on the page
 * that names a part of the evening rather than labelling a fact, and the gold eyebrows the other
 * sections use would have made it one more label above a list of labels.
 */
.programme-title {
    font-family: var(--font-script);
    font-size: clamp(1.5rem, 7vw, 2.1rem);
    font-weight: 400;
    line-height: 1.2;
    letter-spacing: normal;
    text-transform: none;
    text-align: center;
    color: var(--color-accent);
    margin: 0 0 clamp(1.25rem, 3.6svh, 1.9rem);
}

.programme-list {
    position: relative;
    list-style: none;
    margin: 0;
    /* Leaves the dot's column; the rail is drawn down the middle of it. */
    padding: 0 0 0 1.5rem;
}

/* The rail. Inset top and bottom by half a dot so it begins and ends on one rather than
   overshooting into the section's air. */
.programme-list::before {
    content: "";
    position: absolute;
    left: 0.3rem;
    top: 0.45rem;
    bottom: 0.45rem;
    width: 1px;
    background: var(--color-rule);
}

/* Each line gets more air than the spacing scale's middle step — a timeline read at a glance
   needs its stops far enough apart that the rail between them is visible as a run. */
.programme-item {
    position: relative;
    margin-bottom: clamp(1.1rem, 3.2svh, 1.75rem);
}

.programme-item:last-child {
    margin-bottom: 0;
}

/* A ring rather than a filled dot: the rail runs behind it, and a solid gold disc on a cream
   page reads as a bullet where an open one reads as a stop on a line. */
.programme-dot {
    position: absolute;
    left: -1.5rem;
    top: 0.35rem;
    width: 0.55rem;
    height: 0.55rem;
    border: 1px solid var(--color-accent);
    border-radius: 50%;
    background: var(--color-bg);
}

.programme-text p {
    margin: 0;
}

/* Time and title on one line where they fit, and stacked where they don't — baseline-aligned so
   the two sizes sit on the same line rather than the smaller one floating. */
.programme-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.25rem 0.75rem;
}

.programme-time {
    color: var(--color-accent);
    letter-spacing: 0.18em;
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
}

.programme-name {
    text-transform: uppercase;
    letter-spacing: 0.16em;
    font-size: 0.78rem;
    font-weight: 600;
}

.programme-note {
    margin-top: 0.2rem;
    color: var(--color-ink-muted);
    font-size: 0.78rem;
    line-height: 1.6;
}

/* --- Detail blocks --- */

.invite-detail {
    margin-bottom: var(--space-lg);
}

/*
 * The heading that opens a section — VENUE, WILL YOU BE THERE. Gold and widely lettered so it
 * reads as a label on what follows rather than as a line of the copy, which is what the muted
 * grey above left it looking like once the icons that used to stand in for these went.
 */
.section-eyebrow {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.38em;
    color: var(--color-accent);
    margin: 0 0 var(--space-sm);
}

/*
 * Sits under the venue it belongs to, aligned with that line rather than centred on the page,
 * and sized to read as a quiet action rather than a call to arms.
 *
 * Underlined rather than boxed. The border made it the only framed thing in a block that is now
 * entirely type against a hairline, and a bordered control there read as a form field. The
 * padding stays: it is what carries the tap target without a box around it.
 */
.map-link {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin-top: 0.7rem;
    padding: 0.3rem 0;
    border-bottom: 1px solid var(--color-rule);
    color: var(--color-accent);
    text-decoration: none;
    text-transform: uppercase;
    letter-spacing: 0.2em;
    font-size: 0.65rem;
}

.map-link svg {
    width: 0.9rem;
    height: 0.9rem;
    flex: none;
}

/*
 * The same three colours as the confirmation notice above it — surface, accent border, accent
 * text — so the button and the message read as one thing rather than as a form control that
 * wandered in.
 *
 * The background is what was missing: this class started life on a link, where the absence of a
 * background is invisible, and a <button> falls back to the browser's own grey instead.
 */
.invite-action {
    display: inline-block;
    margin-top: var(--space-sm);
    padding: 0.8rem 2rem;
    border: 1px solid var(--color-ink);
    /* Square, and outlined in ink rather than gold. At `font: inherit` this was the only 16px
       thing left in a section that had come down to 10 and 11, and it read as a web form's submit
       button sitting on a printed card. */
    border-radius: 0;
    /* Explicitly transparent, not unset. A <button> with no background at all falls back to the
       browser's own grey — which is what this class did for a while, because it started life on
       an <a> where the absence is invisible. */
    background: transparent;
    color: var(--color-ink);
    font-family: inherit;
    font-size: 0.65rem;
    font-weight: 500;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    text-decoration: none;
    cursor: pointer;
}

/*
 * The browser's own [hidden] rule is `display: none` at the very bottom of the cascade, so the
 * `display: inline-block` above simply beat it and rsvp.js's `submit.hidden = true` did nothing
 * visible at all. Anything both styled and hidden has to say this explicitly — which is why
 * .envelope[hidden] and .music[hidden] already exist.
 */
.invite-action[hidden] {
    display: none;
}

/* --- Table --- */

.table-number {
    /* Big enough to read at arm's length while walking into a noisy hall. */
    font-size: clamp(2.5rem, 12vw, 4rem);
    line-height: 1;
    margin: 0;
    color: var(--color-accent);
}

.table-label {
    margin-top: var(--space-sm);
    color: var(--color-ink-muted);
    font-style: italic;
}

/*
 * Notes and hints on the invitation.
 *
 * These carry the same class as the admin form's hints — but admin.css is never loaded here, so
 * every one of them was rendering completely unstyled: full body size, sentence case, full-strength
 * ink. That is why the transport note read as body copy sitting under a heading rather than as an
 * aside, and why it looked like a different typeface from the letterspaced caps above it. It is the
 * same face throughout; only the treatment was missing.
 *
 * Same treatment as the copy above them, two steps down: an aside, not a paragraph.
 */
/*
 * The quiet lines under a control — when the table appears, until when an answer can change.
 * Sentence case: they are asides in ordinary language, and setting them in caps gave two
 * housekeeping notes the same voice as the headings they sit beneath.
 */
.invite .field-hint {
    max-width: 22rem;
    margin: 0 auto var(--space-sm);
    font-size: 0.72rem;
    line-height: 1.9;
    letter-spacing: 0.15em;
    text-transform: none;
    color: var(--color-ink-muted);
}

/* The transport note explains an offer, so it gets a little more air than a one-line hint. */
.rsvp-guest .field-hint {
    margin-bottom: var(--space-md);
}

/* --- RSVP --- */

/* The RSVP runs past a screen on a phone whatever we do, so the space above the names is worth
   more to the names than to the margin. Tightened here rather than on .invite-message generally,
   which sets the intro on the screen before and wants the air. */
/*
 * The RSVP's message is not the invitation's message, though it shares the class.
 *
 * The intro on the cover is display copy — wide tracking, uppercase, set to be looked at. This is
 * a sentence asking a favour, and at the intro's 0.9rem uppercase it came out louder than the
 * gold eyebrow above it and louder than the guests' own names below. 0.7rem, ordinary tracking
 * and sentence case is what the design sets it at, and it reads as speech rather than signage.
 */
.rsvp .invite-message {
    margin-bottom: var(--space-lg);
    max-width: 24rem;
    font-size: 0.77rem;
    line-height: 1.8;
    letter-spacing: 0.1em;
    text-transform: none;
    color: var(--color-ink-muted);
}

/*
 * No rule between guests, only air.
 *
 * There was one under each answer for a while, and before that above each name. Both read as
 * section breaks rather than as list separators: with the pills below every name already drawing
 * two strong boxes, a hairline under them made each person look like a closed-off section, and a
 * household of four came out as four sections. The names are headings and the spacing is
 * generous — that is enough to tell them apart.
 */
.rsvp-guest {
    border: none;
    margin: 0 0 var(--space-md);
    padding: 0 0 var(--space-md);
}

/*
 * The name above each set of choices. It used to be the <legend>, floated full-width to lift it
 * out of the fieldset border — but .rsvp-choices below is a flex container, which establishes its
 * own formatting context and therefore *avoids* floats rather than flowing under them. With a
 * 100%-wide float above it there was no width left, so the two choices collapsed into a narrow
 * column jammed against the right edge.
 *
 * The legend stays as the group's accessible name, out of flow; this is the visible copy.
 */
.rsvp-name {
    margin: 0 0 var(--space-sm);
    font-weight: 600;
    text-align: center;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    font-size: 0.8rem;
}

/* The two choices as a centred row of their own. They used to be inline-flex labels relying on
   the article's text-align, which put them half of their trailing padding left of true centre —
   and the padding was 1rem on the right and nothing on the left. A flex row centres regardless.

   A wrapper rather than making the <fieldset> itself flex: a <legend> inside a flex container is
   not laid out as a flex item, so the group's accessible name would end up somewhere unintended. */
/*
 * Two columns of equal width, sized to whichever of the pair is wider.
 *
 * A grid rather than the flex row this was, because flex sizes each pill to its own label: for
 * "attending / not attending" that is a small mismatch, and for "yes, I'm bringing someone / no,
 * coming without a plus one" it was a pill twice the width of its partner, wrapped onto a second
 * row. Two answers to one question have to look like two answers to one question.
 *
 * width: fit-content is what keeps them from stretching across the whole column — an fr track
 * measured inside an intrinsically sized container resolves to the widest item, so the pair comes
 * out exactly as wide as it needs and no wider. max-width caps that on a narrow phone, where
 * minmax(0, …) is what lets the labels wrap instead of pushing the row off the screen.
 */
.rsvp-choices {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: 0.5rem;
    width: fit-content;
    max-width: 100%;
    margin: 0 auto;
}

.rsvp-choice {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    /* Generous vertical padding: this is answered with a thumb, on a phone, once, and it is what
       carries the tap target now that the control itself is back to its natural size. */
    padding: 0.6rem 0;
    cursor: pointer;
}

/* Sized explicitly and tinted to the invitation's own gold — a default blue radio on cream is
   the one thing on the page that looks like a form. */
.rsvp-choice input[type="radio"],
.rsvp-choice input[type="checkbox"] {
    width: 0.875rem;
    height: 0.875rem;
    margin: 0;
    flex: none;
    accent-color: var(--color-accent);
}

/*
 * --- The two answers, as a pair of pills ---
 *
 * Still a pair of radios, and deliberately so: the tag helper's own checked-by-string-comparison
 * was rejected long ago, and a guest with no JavaScript still has to be able to answer. The
 * control is only moved out of sight and its <span> painted to look like the button.
 *
 * Clipped rather than display:none or visibility:hidden — both of those take the input out of
 * the tab order, which would leave the whole form unanswerable from a keyboard. It stays
 * focusable and hit-testable exactly where it was; it simply has no pixels.
 */
/*
 * Sized by the pair's own words, not stretched to the row. The design sets them at a fixed inset
 * either side of the label and lets the pair come out however wide the longer answer needs — the
 * grid above is what makes the shorter one match it rather than shrink to its own text.
 */
.rsvp-choices .rsvp-choice {
    padding: 0;
    gap: 0;
}

.rsvp-choices .rsvp-choice input[type="radio"] {
    position: absolute;
    width: 1px;
    height: 1px;
    clip-path: inset(50%);
    overflow: hidden;
    white-space: nowrap;
}

.rsvp-choices .rsvp-choice span {
    display: block;
    /*
     * A floor under every pill on the form, so the guests' row and the plus one's row come out the
     * same width instead of each sizing to its own longest word.
     *
     * The grid above equalises a *pair*; nothing equalises one pair against another, and in
     * Macedonian "Не доаѓам" is a letter longer than "Не доаѓа" — the plus one asks about somebody
     * else, so it is written in the third person. Measured against the face at this size and
     * tracking, with the padding, that is 13.16em against 12.06em: a difference of about ten
     * pixels, which is small enough to look like a mistake rather than a choice.
     *
     * 13.2em is the wider of the two. The guests' row therefore does not move at all — only the
     * plus one's grows to meet it. English is unaffected: both of its rows carry the same two
     * words and are wider than this anyway. Rewording a Macedonian pill past 13.2em is the one
     * thing that would set the rows apart again.
     */
    min-width: 13.2em;
    /*
     * The vertical padding is the one number here not taken from the design, which sets these at
     * 8px and lands a ~28px tall target. This is what a guest taps with a thumb, once, and it is
     * the only thing on the page they *have* to hit — 0.7rem keeps it near 40px, still visibly
     * the small toggle the design draws. The horizontal inset is the design's.
     */
    padding: 0.7rem 1.6rem;
    border: 1px solid var(--color-rule);
    /* Square and unfilled: the cream fill made an unchosen answer look chosen, which is exactly
       the wrong way round for the one control on the page that has to show its state. */
    border-radius: 0;
    background: transparent;
    color: var(--color-ink);
    text-align: center;
    text-transform: uppercase;
    letter-spacing: 0.22em;
    font-size: 0.6rem;
    transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

/* The chosen one fills solid. Ink rather than gold: gold on cream at this size sat under the
   contrast floor, and a filled answer has to be unmistakable from across a phone. */
.rsvp-choices .rsvp-choice input[type="radio"]:checked + span {
    background: var(--color-ink);
    border-color: var(--color-ink);
    color: var(--color-bg);
}

/* The ring the hidden input can no longer draw for itself. Without this the form is answerable
   from a keyboard but gives no sign of where the focus is. */
.rsvp-choices .rsvp-choice input[type="radio"]:focus-visible + span {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .rsvp-choices .rsvp-choice span {
        transition: none;
    }
}

/* Only shown when a party may bring more than one extra guest, where yes/no wouldn't say enough. */
/* Field labels are set like the section eyebrows, a step quieter. At body size they were the
   largest thing in their own block and shouted over the answer they were introducing. */
.rsvp-count-label,
.rsvp-form label {
    display: block;
    padding-bottom: var(--space-sm);
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.22em;
    color: var(--color-ink-muted);
}

/*
 * The plus-one count, and the name boxes on the shared invitation — the two controls a guest
 * actually operates rather than reads.
 *
 * Squared and gold-ruled like everything else, but they keep two things the pills gave up:
 *
 *   - 1rem type. iOS Safari zooms the whole viewport when a focused form control is under 16px,
 *     and it does not zoom back out. A guest typing their name mid-RSVP would be left on a page
 *     twice the size they started on. This is the one place small type is not worth it.
 *   - a --color-surface ground. A transparent field over the backdrop's embossed florals is text
 *     being typed onto a photograph; a button can carry that and an input cannot.
 */
.rsvp-count,
.rsvp-form input[type="text"] {
    font-family: inherit;
    font-size: 1rem;
    /* Same thumb-sized target as the answers above. */
    padding: 0.6rem 0.75rem;
    border: 1px solid var(--color-rule);
    border-radius: 0;
    background: var(--color-surface);
    color: inherit;
}

/* The name boxes on the shared invitation. */
.rsvp-form .field {
    margin-bottom: var(--space-sm);
}

.rsvp-form input[type="text"] {
    width: 100%;
    text-align: center;
}

/* Dashed, because it adds a box that is not there yet — the one place a dashed border says
   something. Otherwise set like the submit button, a size down and in muted rather than ink. */
.add-guest {
    display: block;
    margin: 0 auto var(--space-md);
    padding: 0.55rem 1.4rem;
    border: 1px dashed var(--color-rule);
    border-radius: 0;
    background: transparent;
    color: var(--color-ink-muted);
    font-family: inherit;
    font-size: 0.6rem;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    cursor: pointer;
}

/* A single checkbox rather than a pair of radios, so it centres on its own. The label is the one
   piece of copy in the RSVP left in sentence case — it is the guest's own answer written out. */
.bus-choice {
    justify-content: center;
    gap: 0.65rem;
}

.bus-choice span {
    font-size: 0.7rem;
    letter-spacing: 0.15em;
    color: var(--color-ink);
}

/*
 * The transport block is its own subject, so it drops the top rule the guest fieldsets carry —
 * the divider above it is doing that job now, and two hairlines within a centimetre of each
 * other read as a mistake.
 */
/*
 * The transport block drops the spacing a guest row carries — the divider under it is doing that
 * job, and the row's own padding *plus* the divider's margin came to twice the gap every other
 * section break on the page has.
 */
.bus-block {
    margin-bottom: 0;
    padding-bottom: 0;
    text-align: center;
}

/* Titled like a section rather than named like a guest: nobody is being asked about a person
   here, and the guest names above it are set the same way. */
.bus-title {
    margin: 0 0 var(--space-md);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.22em;
    font-size: 0.75rem;
    color: var(--color-ink);
}

/* A step down again from the other hints: this is background to an offer, not something to be
   answered, and it is the longest run of text in the RSVP.
   The tighter margin pulls the checkbox up under it — they are one question, and the default
   hint spacing left them reading as two unrelated things. */
.bus-note {
    font-size: 0.75rem;
    line-height: 1.9;
    letter-spacing: 0.08em;
    /* Kept when .field-hint gave its uppercase up. This one is a block of display copy explaining
       an offer, not a one-line aside, and it is set the same way as the invitation's own intro. */
    text-transform: uppercase;
}

.rsvp-guest .bus-note {
    margin-bottom: var(--space-sm);
}

/* Focused by envelope.js when a submitted answer brings the guest back, so that it is announced
   rather than only scrolled to. It is a message, not a control — there is nothing to do with it,
   so the ring is noise drawn around the one line they came back to read. */
.rsvp-notice:focus {
    outline: none;
}

/* Squared with the rest. It keeps its ground and its full-size type: it is the line a guest comes
   back to read after submitting, and envelope.js scrolls them to it. */
.rsvp-notice {
    padding: var(--space-sm) var(--space-md);
    border-radius: 0;
    background: var(--color-surface, transparent);
    border: 1px solid var(--color-rule);
    font-size: 0.8rem;
    letter-spacing: 0.05em;
}

.rsvp-notice-error {
    border-color: var(--color-danger, currentColor);
}

.rsvp-summary {
    margin: 0;
}

/* The read-only view once RSVP has closed. Set like the live form it replaces — the name in the
   same lettered caps as .rsvp-name, the answer quiet beneath it — so a guest returning after the
   deadline sees the same invitation, not a different page. */
.rsvp-summary dt {
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    font-size: 0.8rem;
}

.rsvp-summary dd {
    margin: 0 0 var(--space-md);
    color: var(--color-ink-muted);
    font-size: 0.7rem;
    letter-spacing: 0.1em;
}

.invite-notfound {
    text-align: center;
    padding-top: var(--space-lg);
}
