/*
  ნინი & მათე, all styling for index.html.

  Mobile-first: every base rule targets the narrowest phone and the only media
  queries are min-width. 769px hands the screen over to the desktop notice, so
  nothing above it needs a real layout.

  Blocks in order: fonts -> tokens -> reset/scrollbar -> desktop notice ->
  loader -> entry -> hero -> music -> scroll cue -> petals -> sheets ->
  gallery frames -> countdown -> timetable cards -> RSVP -> closing words ->
  footer -> breakpoints -> parallax -> motion.
*/

/* ==========================================================================
   1. FONTS
   font-display: block is only safe because the loader covers the block period
   and gates on document.fonts (with a Georgian probe glyph), see script.js.
   DM Medea has no digits, so it only ever sets words (names, titles); every
   number on the page is BPG Nino.
   ========================================================================== */
@font-face {
    font-family: 'DM Medea';
    src: url('DM-Medea.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
    font-display: block;
}

@font-face {
    font-family: 'BPG Nino';
    src: url('bpg_nino.otf') format('opentype');
    font-weight: normal;
    font-style: normal;
    font-display: block;
}

/* ==========================================================================
   2. TOKENS
   Sampled off the opening itself: --paper is the embossed envelope, --gold the
   champagne wax of the seal, --ink the cool slate in the shadow of the sky
   over Bagrati, so type reads as part of the picture rather than laid on it.
   One accent (gold); --sky is illustration only, never type.
   ========================================================================== */
:root {
    --vh: 1vh;

    --paper:   #f4ece1;
    --paper-2: #ede3d5;
    --paper-3: #e4d7c5;
    --cream:   #fbf7f1;

    --ink:      #344a5c;
    --ink-soft: #57697b;
    --gold:     #9b7752;
    --gold-lt:  #d2b797;
    --line:     #dccbb4;

    --sky: #a0c4f7;

    /* The rounded top lip that makes each section read as a sheet laid over
       the one before it. Used as both radius and negative margin. */
    --lip: 30px;

    --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ==========================================================================
   3. RESET + SCROLLBAR
   ========================================================================== */
*,
*::before,
*::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
    transition: scrollbar-color 1.2s ease;
}

/* The thumb fades in only once the entry is done, so nothing rides the edge
   of the screen while the envelope video is still on it. */
html.is-open {
    scrollbar-color: var(--gold-lt) var(--paper-2);
}

html::-webkit-scrollbar {
    width: 7px;
}

html::-webkit-scrollbar-track {
    background-color: var(--paper-2);
}

html::-webkit-scrollbar-thumb {
    border-radius: 7px;
    background-color: transparent;
    transition: background-color 1.2s ease;
}

html.is-open::-webkit-scrollbar-thumb {
    background-color: var(--gold-lt);
}

html,
body {
    overscroll-behavior-y: none;
}

body {
    background-color: var(--paper);
    color: var(--ink);
    font-family: 'BPG Nino', Georgia, serif;
    font-size: 16px;
    line-height: 1.65;
    -webkit-font-smoothing: antialiased;
    -webkit-tap-highlight-color: transparent;
}

/* Entry/loader scroll lock. NEVER position: fixed or overflow: hidden, that
   freezes Chrome iOS's boot viewport ~110px short and leaves a band of page
   background under the overlay. The gesture is eaten in JS instead. */
html.is-locked,
body.is-locked {
    overscroll-behavior: none;
    touch-action: none;
}

img {
    display: block;
    max-width: 100%;
}

button,
input {
    font: inherit;
    color: inherit;
}

/* ==========================================================================
   4. DESKTOP NOTICE
   ========================================================================== */
.mobile-prompt {
    display: none;
}

/* ==========================================================================
   5. LOADER
   ========================================================================== */
.loader {
    position: fixed;
    inset: 0;
    z-index: 10050;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 26px;
    background-color: var(--paper);
    transition: opacity 0.9s ease;
}

.loader.is-hidden {
    opacity: 0;
    pointer-events: none;
}

.loader.is-removed {
    display: none;
}

.loader__seal {
    width: 112px;
    height: auto;
    animation: seal-breathe 3.6s ease-in-out infinite;
}

@keyframes seal-breathe {
    0%, 100% { opacity: 0.62; transform: scale(0.97); }
    50%      { opacity: 1;    transform: scale(1); }
}

.loader__track {
    position: relative;
    width: 168px;
    height: 2px;
    overflow: hidden;
    border-radius: 2px;
    background-color: var(--line);
}

.loader__bar {
    position: absolute;
    inset: 0;
    transform: scaleX(0);
    transform-origin: left center;
    background-color: var(--gold);
    transition: transform 0.2s linear;
}

.loader__pct {
    font-size: 12px;
    letter-spacing: 0.18em;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   6. ENTRY
   The still sits ON TOP of the video: iOS paints a <video>'s own poster only
   until the first decoded frame, and the gap between "poster gone" and
   "playing" is a black flash. An <img> we fade ourselves has no such gap.
   ========================================================================== */
.entry {
    position: fixed;
    /* inset: 0 reaches the true bottom on its own once the document is
       JS-locked. Forcing a height with --vh / svh oversizes it under the
       Chrome tab bar. */
    inset: 0;
    z-index: 10000;
    overflow: hidden;
    background-color: var(--paper);
    cursor: pointer;
    transition: opacity 1.1s ease;
}

.entry.is-gone {
    opacity: 0;
    pointer-events: none;
}

.entry.is-removed {
    display: none;
}

/* Cropped from the centre: the seal sits dead centre of the 9:16 frame, and
   the walk through the gate ends on the cathedral at the same point, so a
   centre crop keeps both on a phone of any aspect. hero.jpg uses the identical
   crop, which is what makes the hand-over invisible. */
.entry__video,
.entry__still {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center center;
    pointer-events: none;
}

.entry__video {
    z-index: 1;
    background-color: var(--paper);
}

.entry__still {
    z-index: 2;
    transition: opacity 0.55s ease;
}

.entry.is-playing .entry__still {
    opacity: 0;
}

/* The hint goes on the tap (.is-armed), the still only once the video is
   really painting (.is-playing). */
.entry.is-armed .entry__hint {
    opacity: 0;
    animation: none;
}

/* Branded fallback for a blocked autoplay (Low Power Mode): the same envelope
   still, taking a second tap. */
.entry__tap-cover {
    position: absolute;
    inset: 0;
    z-index: 4;
    background: url('entry.jpg') center center / cover no-repeat;
    opacity: 0;
    pointer-events: none;
    cursor: pointer;
    transition: opacity 0.5s ease;
}

.entry__tap-cover.is-visible {
    opacity: 1;
    pointer-events: auto;
}

/* Low on the envelope, under the seal. Frosted, because the paper it lands on
   is nearly the same tone as the pill. */
.entry__hint {
    position: absolute;
    left: 50%;
    bottom: 17%;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 8px 17px;
    border-radius: 999px;
    background-color: rgba(251, 247, 241, 0.8);
    -webkit-backdrop-filter: blur(7px);
    backdrop-filter: blur(7px);
    box-shadow: 0 2px 18px rgba(120, 92, 60, 0.18);
    transform: translateX(-50%);
    white-space: nowrap;
    font-size: 16.5px;
    letter-spacing: 0.07em;
    color: var(--ink);
    animation: hint-pulse 3.6s ease-in-out infinite;
    transition: opacity 0.4s ease;
}

/* Held back until the loader clears, so the pulse is not already mid-cycle
   (or the guest's first sight) behind the spinner. */
.is-loading .entry__hint {
    animation-play-state: paused;
}

.entry__hint-line {
    width: 18px;
    height: 1px;
    background-color: var(--gold);
}

@keyframes hint-pulse {
    0%, 100% { opacity: 0.8; }
    50%      { opacity: 1; }
}

/* ==========================================================================
   7. HERO
   ========================================================================== */
.page {
    position: relative;
    /* clip, not hidden: hidden would make .page a scroll container and break
       the view() timelines the parallax in block 18 runs on. */
    overflow-x: clip;
}

.hero {
    position: relative;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    /* Floors first, then the JS pin; order matters, the pin must win. */
    min-height: 100vh;
    min-height: 100svh;
    /* clip, not hidden: hidden makes this a scroll container, and the view()
       timelines in block 18 would then bind to it and never move. */
    overflow: hidden;
    overflow: clip;
    background-color: var(--paper);
    padding-bottom: 150px;
}

.vh-lock .hero {
    min-height: calc(var(--vh) * 100);
}

/* Cropped exactly like .entry__video above it: the video's last frame IS this
   image, and any offset on one side of the cross-fade turns an invisible
   hand-over into a lurch. The parallax (block 18) only moves it once the
   guest scrolls, long after the hand-over. */
.hero__still {
    position: absolute;
    inset: 0;
    z-index: 1;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center center;
}

/* Lifts the band of sky the text sits in, and leaves the cathedral and the
   candles alone. */
.hero__veil {
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    background-image: radial-gradient(
        ellipse 98% 34% at 50% 27%,
        rgba(251, 247, 241, 0.86) 0%,
        rgba(251, 247, 241, 0.5) 55%,
        rgba(251, 247, 241, 0) 100%
    );
}

.hero__content {
    position: relative;
    z-index: 4;
    width: 100%;
    padding: 0 14px;
    /* Where the names start, measured from the top, so it is the ONE number
       that decides the block's position. --vh so it lands in the open sky
       under the curtain swag on short and tall phones alike. */
    margin-top: calc(var(--vh) * 15);
    text-align: center;
}

/* Held until .is-open; the entrance runs THROUGH the overlay fading out, so
   the video ending and the names arriving read as one movement. */
.hero__names,
.hero__invite,
.hero__rule,
.hero__date,
.hero__table {
    opacity: 0;
}

.is-open .hero__names   { animation: rise 1.7s var(--ease) 0.15s both; }
.is-open .hero__invite  { animation: rise 1.6s var(--ease) 0.55s both; }
.is-open .hero__rule    { animation: rule-in 1.6s var(--ease) 1.1s both; }
.is-open .hero__date    { animation: rise 1.6s var(--ease) 1.4s both; }
.is-open .hero__table   { animation: rise 1.6s var(--ease) 1.75s both; }

@keyframes rise {
    from { opacity: 0; transform: translateY(18px); }
    to   { opacity: 1; transform: none; }
}

@keyframes rule-in {
    from { opacity: 0; transform: scaleX(0); }
    to   { opacity: 1; transform: scaleX(1); }
}

/* One line, nowrap: "ნინი & მათე" in DM Medea measures ~3.9em, so 62px is
   ~242px, clear of a 320px phone's 292px of room. Re-measure if either name
   ever changes. */
.hero__names {
    font-family: 'DM Medea', 'BPG Nino', Georgia, serif;
    font-size: 42px;
    font-weight: normal;
    line-height: 2.25;
    white-space: nowrap;
    color: var(--ink);
    text-shadow: 0 1px 2px rgba(251, 247, 241, 0.9),
                 0 2px 18px rgba(251, 247, 241, 0.95),
                 0 0 44px rgba(251, 247, 241, 0.7);
}

.hero__name {
    display: inline;
}

/* The ampersand in the seal's champagne, set in the body face because DM
   Medea's own "&" is too heavy beside the names. */
.hero__amp {
    display: inline;
    margin: 0 0.08em;
    font-family: 'BPG Nino', Georgia, serif;
    font-size: 0.6em;
    color: var(--gold);
}

.hero__invite {
    margin-top: 14px;
    line-height: 1.45;
    color: var(--ink);
    text-shadow: 0 1px 3px rgba(251, 247, 241, 0.98),
                 0 1px 12px rgba(251, 247, 241, 0.95),
                 0 0 30px rgba(251, 247, 241, 0.85);
}

.hero__invite-greet {
    display: block;
    margin-bottom: 5px;
    font-size: 23px;
    letter-spacing: 0.02em;
    line-height: 1.25;
}

/* Class selector so it still wins over display: block once script.js removes
   the attribute, and keeps the UA [hidden] behaviour until then. */
.hero__invite-greet[hidden] {
    display: none;
}

.hero__invite-title { color: var(--ink-soft); }

.hero__invite-line {
    display: block;
    max-width: 300px;
    margin: 0 auto;
    font-size: 18px;
    letter-spacing: 0.04em;
}

.hero__rule {
    display: block;
    width: 70px;
    height: 1px;
    margin: 18px auto 0;
    background-color: var(--gold);
}

.hero__date {
    display: inline-block;
    margin-top: 16px;
    padding: 10px 22px;
    border-radius: 999px;
    background-color: rgba(251, 247, 241, 0.76);
    -webkit-backdrop-filter: blur(7px);
    backdrop-filter: blur(7px);
    box-shadow: 0 2px 18px rgba(52, 74, 92, 0.12);
    font-size: 20px;
    letter-spacing: 0.12em;
    white-space: nowrap;
    color: var(--ink);
}

/* The guest's seat; a gold-edged pill so it reads as the one practical,
   guest-specific fact among the names and date. */
.hero__table {
    display: flex;
    width: max-content;
    align-items: center;
    gap: 10px;
    margin: 12px auto 0;
    padding: 10px 22px;
    border: 1px solid var(--gold-lt);
    border-radius: 999px;
    background-color: rgba(251, 247, 241, 0.82);
    -webkit-backdrop-filter: blur(7px);
    backdrop-filter: blur(7px);
}

.hero__table[hidden] {
    display: none;
}

.hero__table-label {
    font-size: 14px;
    letter-spacing: 0.12em;
    color: var(--ink-soft);
}

.hero__table-num {
    font-size: 24px;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   7b. MUSIC TOGGLE
   Fixed so the guest can silence it from anywhere. Invisible AND untappable
   until .is-open, so it can never be hit through the entry overlay.
   ========================================================================== */
.music {
    position: fixed;
    top: calc(14px + env(safe-area-inset-top, 0px));
    right: calc(14px + env(safe-area-inset-right, 0px));
    z-index: 60;
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    padding: 0;
    border: 1px solid var(--gold-lt);
    border-radius: 999px;
    background-color: rgba(251, 247, 241, 0.76);
    -webkit-backdrop-filter: blur(7px);
    backdrop-filter: blur(7px);
    box-shadow: 0 2px 18px rgba(52, 74, 92, 0.12);
    color: var(--ink);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    opacity: 0;
    visibility: hidden;
    transform: scale(0.9);
    transition: opacity 0.9s ease, transform 0.9s ease, visibility 0.9s;
}

.is-open .music {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition-delay: 1.1s;
}

.music:active {
    transform: scale(0.96);
}

.music__icon {
    grid-area: 1 / 1;
    width: 21px;
    height: 21px;
    fill: currentColor;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: opacity 0.35s ease;
}

.music__wave {
    fill: none;
}

/* States what is happening now: waves while it plays, a cross while not. */
.music__icon--on { opacity: 0; }
.music__icon--off { opacity: 1; }

.music.is-playing .music__icon--on { opacity: 1; }
.music.is-playing .music__icon--off { opacity: 0; }

/* ==========================================================================
   8. SCROLL CUE
   The hero looks finished at the fold, so only motion tells the guest there
   is more. A transition, not an animation, so .is-scrolled can fade it out.
   ========================================================================== */
.cue {
    position: absolute;
    left: 50%;
    bottom: 30px;
    z-index: 5;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 9px;
    opacity: 0;
    transform: translateX(-50%);
    pointer-events: none;
    transition: opacity 1.1s ease;
}

.is-open .cue { opacity: 1; transition-delay: 2.4s; }
.is-open.is-scrolled .cue { opacity: 0; transition-delay: 0s; }

.cue__label {
    padding: 8px 20px;
    border-radius: 999px;
    background-color: rgba(251, 247, 241, 0.86);
    -webkit-backdrop-filter: blur(7px);
    backdrop-filter: blur(7px);
    box-shadow: 0 2px 18px rgba(52, 74, 92, 0.16);
    font-size: 17px;
    letter-spacing: 0.08em;
    white-space: nowrap;
    color: var(--ink);
}

.cue__ring {
    position: relative;
    display: block;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background-color: rgba(251, 247, 241, 0.84);
    -webkit-backdrop-filter: blur(7px);
    backdrop-filter: blur(7px);
    box-shadow: 0 2px 18px rgba(52, 74, 92, 0.16);
    animation: cue-bob 2.6s ease-in-out infinite;
}

.cue__ring::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 1px solid var(--ink-soft);
    border-radius: 50%;
    animation: cue-ping 2.6s ease-out infinite;
}

.cue__chev {
    position: absolute;
    left: 50%;
    top: 12px;
    width: 10px;
    height: 10px;
    margin-left: -5px;
    border-right: 2px solid var(--ink);
    border-bottom: 2px solid var(--ink);
    transform: rotate(45deg);
    animation: chev-fade 2.6s ease-in-out infinite;
}

.cue__chev--2 {
    top: 19px;
    opacity: 0.45;
    animation-delay: 0.32s;
}

@keyframes cue-bob {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(7px); }
}

@keyframes cue-ping {
    0%   { opacity: 0.5; transform: scale(1); }
    70%  { opacity: 0; transform: scale(1.85); }
    100% { opacity: 0; transform: scale(1.85); }
}

@keyframes chev-fade {
    0%, 100% { opacity: 0.35; }
    45%      { opacity: 1; }
}

/* ==========================================================================
   9. PETALS
   Spans the document above every sheet. Each petal is parked at a fixed
   document offset by script.js and falls ~110vh from there, so the drift is
   spread down the page. Hidden until the entry has handed over.
   ========================================================================== */
.petals {
    position: absolute;
    inset: 0;
    z-index: 20;
    overflow: hidden;
    pointer-events: none;
    visibility: hidden;
}

.petals.is-visible {
    visibility: visible;
}

.petal {
    position: absolute;
    pointer-events: none;
    user-select: none;
    -webkit-user-select: none;
    will-change: transform, opacity;
}

/* --vh, not vh: the fall distance must not jump when an in-app nav bar
   slides and resizes every viewport unit. */
@keyframes petalFallA {
    0%   { transform: translateY(-100px) translateX(0) rotate(0deg); opacity: 0; }
    12%  { opacity: 0.55; }
    30%  { transform: translateY(calc(var(--vh) * 26)) translateX(28px) rotate(50deg); }
    55%  { transform: translateY(calc(var(--vh) * 54)) translateX(-14px) rotate(96deg); }
    80%  { transform: translateY(calc(var(--vh) * 80)) translateX(24px) rotate(148deg); opacity: 0.48; }
    93%  { opacity: 0; }
    100% { transform: translateY(calc(var(--vh) * 110)) translateX(-4px) rotate(183deg); opacity: 0; }
}

@keyframes petalFallB {
    0%   { transform: translateY(-100px) translateX(0) rotate(16deg); opacity: 0; }
    12%  { opacity: 0.52; }
    32%  { transform: translateY(calc(var(--vh) * 28)) translateX(-26px) rotate(-44deg); }
    58%  { transform: translateY(calc(var(--vh) * 58)) translateX(18px) rotate(-90deg); }
    82%  { transform: translateY(calc(var(--vh) * 82)) translateX(-10px) rotate(-145deg); opacity: 0.45; }
    93%  { opacity: 0; }
    100% { transform: translateY(calc(var(--vh) * 110)) translateX(6px) rotate(-176deg); opacity: 0; }
}

@keyframes petalFallC {
    0%   { transform: translateY(-100px) translateX(0) rotate(-8deg); opacity: 0; }
    14%  { opacity: 0.5; }
    28%  { transform: translateY(calc(var(--vh) * 24)) translateX(34px) rotate(36deg); }
    54%  { transform: translateY(calc(var(--vh) * 54)) translateX(4px) rotate(80deg); }
    80%  { transform: translateY(calc(var(--vh) * 80)) translateX(30px) rotate(126deg); opacity: 0.44; }
    93%  { opacity: 0; }
    100% { transform: translateY(calc(var(--vh) * 110)) translateX(12px) rotate(158deg); opacity: 0; }
}

/* ==========================================================================
   10. SHEETS
   Every section below the hero is a sheet of paper laid over the one before
   it (the ucha-megi transition): a rounded top lip, an upward shadow and a
   negative margin so it rides over its neighbour. Painting order is DOM order,
   later on top, which is exactly what the overlap needs.
   ========================================================================== */
.sheet {
    position: relative;
    z-index: 2;
    margin-top: calc(var(--lip) * -1);
    padding: 56px 22px 52px;
    border-top-left-radius: var(--lip);
    border-top-right-radius: var(--lip);
    background-color: var(--paper);
    box-shadow: 0 -14px 32px rgba(92, 74, 52, 0.14);
}

/* Catches the light along the lip, so the fold still reads where the sheet
   underneath is nearly the same tone. */
.sheet::before {
    content: '';
    position: absolute;
    top: 0;
    left: 11%;
    right: 11%;
    height: 1px;
    background-color: rgba(255, 255, 255, 0.8);
}

.sheet--alt { background-color: var(--paper-2); }

.sheet--foot {
    padding-top: 40px;
    padding-bottom: calc(32px + env(safe-area-inset-bottom, 0px));
    background-color: var(--paper-3);
    text-align: center;
}

.sheet__inner {
    width: 100%;
    max-width: 360px;
    margin: 0 auto;
    text-align: center;
}

.sheet__eyebrow {
    font-size: 14px;
    letter-spacing: 0.16em;
    color: var(--ink-soft);
}

.sheet__title {
    margin-top: 12px;
    font-family: 'DM Medea', 'BPG Nino', Georgia, serif;
    font-size: 34px;
    font-weight: normal;
    /* DM Medea's swashes reach far above and below the x-height; anything
       tighter lets them run into the eyebrow and the gold rule. */
    line-height: 1.55;
    color: var(--ink);
}

.sheet__title:first-child { margin-top: 0; }

/* Grows out of the centre once the heading lands. */
.sheet__rule {
    display: block;
    width: 64px;
    height: 1px;
    margin: 24px auto 0;
    background-color: var(--gold);
}

/* ===== Scroll reveal ===== */
.reveal {
    opacity: 0;
    transform: translateY(20px);
    transition: opacity 1.3s ease, transform 1.3s var(--ease);
}

.reveal.is-in {
    opacity: 1;
    transform: none;
}

/* The rule animates scaleX, so it must not take the translate reveal, and its
   own in-state has to be restated at matching specificity. */
.sheet__rule.reveal       { transform: scaleX(0); transition-delay: 0.3s; }
.sheet__rule.reveal.is-in { transform: scaleX(1); }

/* ==========================================================================
   11. GALLERY: FRAMED PRINTS
   Built like a real picture frame, from the outside in, and every layer is a
   physical material rather than a rendered bevel:

     .frame         slim satin-gilt fillet. A soft, low-contrast champagne
                    run (the seal's own wax tones), so it reads as gold leaf
                    catching window light, not as chrome or plastic.
     .frame__mat    the passe-partout, in the envelope's own cotton-rag paper
                    (mat.jpg, a mirrored tile cut from a plain run of it), so
                    the frames are made of the invitation itself. Bottom-
                    weighted, as hand-cut mats are. Tiled at the artwork's own
                    scale: the embossed florals were tried and read as
                    crumpled foil at mat width.
     .frame__window the bevel: a hairline of light paper core around the photo
                    and a whisper of shadow cast inward, which is what a 45°
                    mat cut looks like up close.

   The deck mechanics (stacking, swipe, auto-advance) are the Weddsite
   photo-deck build; see setupDeck in script.js. Every print shares one 2:3
   window and object-fit crops into it, with --pos on each <img> as the focus.
   ========================================================================== */
.gallery {
    padding-bottom: 46px;
}

.deck-wrap {
    margin-top: 30px;
    text-align: center;
}

.deck {
    position: relative;
    width: 250px;
    /* Card: 6 + 18 + 303 (202px window at 2:3) + 28 + 6 = 361px, plus the
       sunk cards' 3 x 10px step and room for the rotation. Fixed, because
       every card is absolute and the stack would otherwise have no height;
       re-derive it if the print size changes. */
    height: 420px;
    margin: 0 auto;
    /* pan-y, not none: a vertical swipe that starts on a card still scrolls
       the page. The horizontal axis is claimed in JS. */
    touch-action: pan-y;
}

.frame {
    position: absolute;
    /* Centred; pairs with the -50% in cardTransform in script.js. */
    top: 50%;
    left: 50%;
    width: 250px;
    padding: 6px;
    border-radius: 2px;
    background-color: #d6bd93;
    background-image: linear-gradient(
        158deg,
        #efe2c4 0%,
        #d3b88c 18%,
        #f3e7cc 34%,
        #c7a676 52%,
        #e6d3ad 70%,
        #b9966a 88%,
        #dcc49b 100%
    );
    /* Inner light on the top-left lip, inner shade on the bottom-right, then
       the frame's own weight on the wall: a tight contact shadow and a soft,
       warm long one. Shadows are tinted to the paper, never black. */
    box-shadow:
        inset 1px 1px 0 rgba(255, 249, 232, 0.75),
        inset -1px -1px 0 rgba(120, 90, 52, 0.35),
        0 2px 4px rgba(92, 70, 44, 0.18),
        0 18px 34px -8px rgba(92, 70, 44, 0.34);
    transform-origin: 50% 60%;
    transition: transform 0.6s var(--ease), opacity 0.45s ease;
    user-select: none;
    -webkit-user-select: none;
    will-change: transform;
}

/* Killed while a finger is on the card so it tracks the drag exactly. */
.frame.is-dragging { transition: none; }

.frame__mat {
    padding: 18px 18px 28px;
    background-color: var(--cream);
    background-image: url('mat.jpg');
    background-size: 340px auto;
    background-repeat: repeat;
    /* The fillet's inner edge: a fine darker line where gilt meets paper. */
    box-shadow: inset 0 0 0 1px rgba(140, 108, 66, 0.28),
                inset 0 2px 5px rgba(92, 70, 44, 0.16);
}

.frame__window {
    position: relative;
    aspect-ratio: 2 / 3;
    overflow: hidden;
    background-color: var(--paper-3);
    box-shadow:
        0 0 0 1px #fffaf0,
        0 0 0 2px rgba(150, 120, 80, 0.22),
        0 1px 0 2px rgba(255, 252, 244, 0.6);
}

/* The cast shadow of the bevel falls inward onto the print. Its own layer so
   it sits above the photo; an inset shadow on the window would be painted
   underneath the <img>. */
.frame__window::after {
    content: '';
    position: absolute;
    inset: 0;
    box-shadow: inset 1px 2px 4px rgba(60, 44, 24, 0.32);
    pointer-events: none;
}

.frame__window img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--pos, 50% 40%);
    pointer-events: none;
}

.deck-hint {
    margin-top: 18px;
    font-size: 15px;
    letter-spacing: 0.04em;
    color: var(--ink-soft);
}

/* ==========================================================================
   12. COUNTDOWN
   ========================================================================== */
.sheet--count .sheet__eyebrow { margin-top: 18px; }

.cd {
    display: flex;
    gap: 8px;
    margin-top: 30px;
}

.cd__unit {
    flex: 1 1 0;
    min-width: 0;
    padding: 14px 4px 12px;
    border: 1px solid var(--line);
    border-radius: 16px;
    background-color: var(--cream);
    box-shadow: 0 3px 14px rgba(92, 74, 52, 0.08);
}

.cd__num {
    display: block;
    font-size: 27px;
    line-height: 1;
    color: var(--ink);
    /* Tabular figures: otherwise the seconds jitter the row every tick. */
    font-variant-numeric: tabular-nums;
}

.cd__label {
    display: block;
    margin-top: 7px;
    font-size: 12px;
    letter-spacing: 0.05em;
    color: var(--ink-soft);
}

/* ==========================================================================
   13. TIMETABLE CARDS
   The salome-giga event card: the illustration bled into the card's own top
   edge and dissolved into the paper, then the event, the place, the hour and
   the map link. --card-pad-x / --card-pad-y are the card's padding as custom
   properties because .tt__art has to cancel them exactly to reach the edge.
   ========================================================================== */
.tt {
    --card-pad-x: 22px;
    --card-pad-y: 30px;
    display: flex;
    flex-direction: column;
    gap: 26px;
    margin-top: 32px;
}

.tt__card {
    position: relative;
    /* clip, not hidden: hidden makes this a scroll container, and the view()
       timelines in block 18 would then bind to it and never move. */
    overflow: hidden;
    overflow: clip;
    padding: var(--card-pad-y) var(--card-pad-x) 28px;
    border: 1px solid var(--line);
    border-radius: 24px;
    background-color: var(--cream);
    box-shadow: 0 4px 18px rgba(92, 74, 52, 0.1);
    text-align: center;
    /* Lit by the IntersectionObserver in script.js as each card arrives. */
    opacity: 0;
    transform: translateY(22px);
    transition: opacity 1.15s ease, transform 1.3s var(--ease);
}

.tt__card.is-lit {
    opacity: 1;
    transform: none;
}

.tt__art {
    position: relative;
    width: calc(100% + var(--card-pad-x) * 2);
    margin: calc(var(--card-pad-y) * -1) calc(var(--card-pad-x) * -1) 18px;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    overflow: clip;
}

.tt__art img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The transparent stop is rgba() of the card colour, never `transparent`:
   Safari interpolates the keyword through rgba(0,0,0,0) and leaves a grey
   haze across the fade. */
.tt__art::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 26%;
    background-image: linear-gradient(
        to bottom,
        rgba(251, 247, 241, 0),
        var(--cream)
    );
    pointer-events: none;
}

.tt__label {
    font-size: 20px;
    letter-spacing: 0.1em;
    line-height: 1.4;
    color: var(--gold);
}

.tt__venue {
    margin-top: 8px;
    font-size: 23px;
    font-weight: normal;
    line-height: 1.4;
    color: var(--ink);
}

.tt__addr {
    margin-top: 2px;
    font-size: 15px;
    letter-spacing: 0.06em;
    color: var(--ink-soft);
}

.tt__rule {
    display: block;
    width: 44px;
    height: 1px;
    margin: 18px auto;
    background-color: var(--gold-lt);
}

/* The largest thing in a card, so a guest scrolling for the hour finds it
   without reading anything else. */
.tt__time {
    font-size: 38px;
    line-height: 1;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}

.tt__map {
    display: inline-block;
    margin-top: 22px;
    padding-bottom: 3px;
    border-bottom: 1px solid var(--gold-lt);
    font-size: 15px;
    letter-spacing: 0.12em;
    color: var(--ink-soft);
    text-decoration: none;
    transition: color 0.3s ease, border-color 0.3s ease;
}

.tt__map:hover,
.tt__map:focus-visible {
    color: var(--ink);
    border-color: var(--ink);
}

/* ==========================================================================
   14. RSVP
   Labels above inputs, errors below. Inputs are underlines on the panel; the
   interactive choices are pills, matching every other control on the page.
   ========================================================================== */
.rsvp__lead {
    margin-top: 18px;
    font-size: 16.5px;
    line-height: 1.7;
    color: var(--ink-soft);
}

.rsvp__lead[hidden] { display: none; }

.rsvp__panel {
    margin-top: 26px;
    padding: 28px 20px 24px;
    border: 1px solid var(--line);
    border-radius: 24px;
    background-color: var(--cream);
    box-shadow: 0 4px 18px rgba(92, 74, 52, 0.08);
    text-align: left;
}

.rsvp__form {
    display: flex;
    flex-direction: column;
    gap: 22px;
}

.rsvp__form[hidden] { display: none; }

.field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    border: 0;
    min-width: 0;
}

.field__label {
    padding: 0;
    font-size: 14.5px;
    letter-spacing: 0.04em;
    line-height: 1.5;
    color: var(--ink);
}

/* 16px minimum on the input: anything smaller makes iOS zoom the page on
   focus, which then leaves the layout zoomed after the keyboard closes. */
.field__input {
    width: 100%;
    padding: 9px 2px;
    border: 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    background-color: transparent;
    font-size: 17px;
    color: var(--ink);
    outline: none;
    -webkit-appearance: none;
    appearance: none;
    transition: border-color 0.3s ease;
}

.field__input:focus {
    border-bottom-color: var(--gold);
}

.field__error {
    min-height: 0;
    font-size: 13.5px;
    color: #9a4b3c;
}

.field__error:empty { display: none; }

.choices {
    display: flex;
    flex-direction: column;
    gap: 9px;
    margin-top: 4px;
}

.choice,
.pill {
    position: relative;
    cursor: pointer;
}

/* The native radio stays in the DOM (keyboard and screen reader still work)
   but invisible; the face beside it is what the guest sees. */
.choice input,
.pill input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
}

.choice__face {
    display: block;
    padding: 12px 18px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background-color: var(--paper);
    font-size: 15.5px;
    text-align: center;
    color: var(--ink-soft);
    transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease, transform 0.2s ease;
}

.choice input:checked + .choice__face {
    border-color: var(--gold);
    background-color: rgba(210, 183, 151, 0.28);
    color: var(--ink);
}

.choice input:focus-visible + .choice__face,
.pill input:focus-visible + span {
    outline: 2px solid var(--gold);
    outline-offset: 2px;
}

.choice:active .choice__face,
.pill:active span {
    transform: scale(0.98);
}

/* Folds away when the guest answers "no": a party size means nothing then. */
.rsvp__count {
    transition: opacity 0.4s ease;
}

.rsvp__count.is-off {
    display: none;
}

.pills {
    display: flex;
    gap: 8px;
    margin-top: 4px;
}

.pill {
    flex: 1 1 0;
}

.pill span {
    display: grid;
    place-items: center;
    height: 44px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background-color: var(--paper);
    font-size: 17px;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
    transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease, transform 0.2s ease;
}

.pill input:checked + span {
    border-color: var(--gold);
    background-color: rgba(210, 183, 151, 0.28);
    color: var(--ink);
}

.rsvp__submit {
    margin-top: 4px;
    padding: 14px 20px;
    border: 0;
    border-radius: 999px;
    background-color: var(--ink);
    font-size: 16px;
    letter-spacing: 0.1em;
    color: var(--cream);
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
    transition: background-color 0.3s ease, transform 0.2s ease, opacity 0.3s ease;
}

.rsvp__submit:active {
    transform: translateY(1px) scale(0.99);
}

.rsvp__submit:disabled {
    opacity: 0.6;
    cursor: default;
}

.rsvp__status {
    min-height: 1em;
    font-size: 14px;
    text-align: center;
    color: var(--ink-soft);
}

.rsvp__status:empty { display: none; }

.rsvp__status.is-error {
    color: #9a4b3c;
}

.rsvp__done {
    padding: 8px 4px;
    font-size: 18px;
    line-height: 1.7;
    text-align: center;
    color: var(--ink);
}

.rsvp__done[hidden] { display: none; }

/* ==========================================================================
   15. CLOSING WORDS
   The wax seal comes back as the closing mark: the invitation ends on the
   object it opened with.
   ========================================================================== */
.sheet--sign {
    padding-top: 46px;
    padding-bottom: 56px;
}

.sign__seal {
    width: 96px;
    height: auto;
    margin: 0 auto;
}

.words {
    margin-top: 22px;
    font-size: 19px;
    line-height: 1.75;
    color: var(--ink);
}

.sign {
    margin-top: 28px;
}

/* Lozenge on a rule, the closing flourish. */
.sign__mark {
    position: relative;
    display: block;
    width: 88px;
    height: 1px;
    margin: 0 auto;
    background-color: var(--gold-lt);
}

.sign__mark::after {
    content: '';
    position: absolute;
    top: -3px;
    left: 50%;
    width: 7px;
    height: 7px;
    margin-left: -4px;
    border: 1px solid var(--gold);
    background-color: var(--paper);
    transform: rotate(45deg);
}

.sign__mark--end {
    margin-top: 26px;
}

.sign__lead {
    margin-top: 24px;
    font-size: 14.5px;
    letter-spacing: 0.16em;
    color: var(--gold);
}

.sign__hosts {
    margin-top: 8px;
    font-family: 'DM Medea', 'BPG Nino', Georgia, serif;
    font-size: 32px;
    line-height: 1.3;
    color: var(--ink);
}

/* ==========================================================================
   16. FOOTER
   ========================================================================== */
.credit {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin-top: 22px;
    font-size: 12px;
    color: var(--ink-soft);
    text-decoration: none;
    opacity: 0.3;
    transition: opacity 0.3s ease;
}

.credit:hover,
.credit:focus-visible {
    opacity: 0.75;
}

.credit__logo {
    width: 16px;
    height: auto;
}

/* ==========================================================================
   17. BREAKPOINTS
   Mobile-first, min-width only. 481px is the large-phone step; 769px hands
   the screen to the desktop notice entirely.
   ========================================================================== */
@media (min-width: 481px) {
    .hero__names        { font-size: 72px; }
    .hero__invite-line  { font-size: 19px; max-width: 340px; }
    .hero__date         { font-size: 21px; }
    .cue__label         { font-size: 18.5px; }
    .sheet__title       { font-size: 38px; }
    .sheet__inner       { max-width: 400px; }
    .cd__num            { font-size: 31px; }
    .tt__label          { font-size: 22px; }
    .tt__venue          { font-size: 25px; }
    .tt__time           { font-size: 42px; }
    .words              { font-size: 20px; }
    .sign__hosts        { font-size: 36px; }
}

@media (min-width: 769px) {
    .mobile-prompt {
        position: fixed;
        inset: 0;
        z-index: 20000;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 18px;
        padding: 30px;
        background-color: var(--paper);
        text-align: center;
    }

    .mobile-prompt__icon {
        width: 52px;
        height: auto;
        opacity: 0.8;
    }

    .mobile-prompt p {
        font-size: 17px;
        letter-spacing: 0.06em;
        color: var(--ink-soft);
    }
}

/* ==========================================================================
   18. PARALLAX
   Slight, and CSS scroll-driven only (animation-timeline: view()), so it runs
   on the compositor with no scroll listener and no getBoundingClientRect per
   frame. That is the whole reason it is safe on Chrome iOS, which resizes its
   own chrome mid-scroll and makes JS parallax lurch. Browsers without
   scroll-driven animations (older iOS) simply get a still page.

   Every target here is a wrapper that carries no other animation or
   transform-transition of its own, so the timelines never fight a reveal:

     .hero__still      drifts down at ~1/5 of the scroll as the hero leaves,
                       so the next sheet visibly slides over it.
     .hero__content    lifts a touch faster and fades, ahead of the picture.
     .deck-wrap        is the reveal target, so the parallax rides on .deck.
     .tt__art img      moves inside its window, the picture behind glass.
     .sheet__inner     settles the last 24px as the sheet comes up.

   The animation shorthand resets animation-timeline, so the timeline is
   always declared after it.
   ========================================================================== */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        .hero__still {
            animation: px-hero linear both;
            animation-timeline: view();
            animation-range: exit 0% exit 100%;
        }

        .hero__content {
            animation: px-hero-copy linear both;
            animation-timeline: view();
            animation-range: exit 0% exit 80%;
        }

        .deck {
            animation: px-float linear both;
            animation-timeline: view();
            animation-range: cover 0% cover 100%;
        }

        .tt__art img {
            animation: px-art linear both;
            animation-timeline: view();
            animation-range: cover 0% cover 100%;
        }

        .sheet__inner {
            animation: px-settle linear both;
            animation-timeline: view();
            animation-range: entry 0% entry 100%;
        }
    }
}

@keyframes px-hero {
    from { transform: translateY(0); }
    to   { transform: translateY(20%); }
}

@keyframes px-hero-copy {
    from { transform: translateY(0); opacity: 1; }
    to   { transform: translateY(-40px); opacity: 0.2; }
}

@keyframes px-float {
    from { transform: translateY(22px); }
    to   { transform: translateY(-22px); }
}

/* Scaled up so the travel never shows the window's edge. */
@keyframes px-art {
    from { transform: scale(1.14) translateY(-5%); }
    to   { transform: scale(1.14) translateY(5%); }
}

@keyframes px-settle {
    from { transform: translateY(24px); }
    to   { transform: translateY(0); }
}

/* ==========================================================================
   19. REDUCED MOTION
   Ambient loops go entirely; reveals become instant rather than disabled, so
   nothing is left stuck at opacity 0.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
    .loader__seal,
    .entry__hint,
    .cue__ring,
    .cue__chev {
        animation: none !important;
    }

    .cue__ring::after,
    .petals {
        display: none;
    }

    .music {
        transform: none;
    }

    .is-open .hero__names,
    .is-open .hero__invite,
    .is-open .hero__rule,
    .is-open .hero__date,
    .is-open .hero__table {
        animation: none !important;
        opacity: 1;
        transform: none;
    }

    .reveal,
    .tt__card,
    .frame,
    .sheet__rule {
        transition-duration: 0.01ms !important;
    }
}
