/* ============================================================================
   Andrea & Alex — March 27, 2027 — Austin, Texas
   ----------------------------------------------------------------------------
   One stylesheet, no build step. The site is small enough that a bundler would
   add more moving parts than it removes, and a wedding site should still work
   in five years without anyone reinstalling a toolchain.

   Palette: eucalyptus sage and warm cream, carried over from the template you
   liked. Everything is a custom property, so changing the wedding's colours is
   editing the block below and nothing else.
   ========================================================================= */

:root {
  --cream:        #fbf9f4;
  --paper:        #f4efe4;
  --paper-deep:   #ece5d6;
  --sage:         #8b9a7d;
  --sage-deep:    #58664f;
  --sage-mist:    #dfe5d8;
  --ink:          #3a3d34;
  --ink-strong:   #23261f;
  --ink-muted:    #66685e;
  --rule:         #ddd8c9;
  --danger:       #9c3b2e;
  /* The heart in the hero's typed line. Decorative and aria-hidden, so WCAG
     does not strictly govern it, but the original #b8695e sat at 3.82:1 on
     cream and read washed out next to type that clears 10:1. This is the same
     hue carried darker: 5.13:1 on --cream, 4.71:1 on --paper. */
  --blush:        #a35348;
  --success:      #4f6b4a;
  /* Every ink/ground pairing above clears WCAG AA (4.5:1) for normal text —
     --ink-muted and --sage-deep are darker than they look like they need to be
     for exactly that reason. If you retune the palette, re-check the pairs:
     muted text on --paper-deep and --sage-deep on --sage-mist are the two that
     fail first. */

  --display: 'Cormorant Garamond', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  --body: 'Jost', 'Avenir Next', 'Segoe UI', system-ui, sans-serif;
  /* Only the hero's typed line uses this. A typewriter effect in a
     proportional face looks like a bug — the text visibly reflows on every
     character — so the one place we type needs a fixed advance width. */
  /* Space Mono ships a single weight. Anything using --mono has to set
     font-weight: 400 with it, or it inherits the body's 300, asks for a face
     that does not exist, and leaves the stylesheet describing something the
     browser was never going to render. */
  --mono: 'Space Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  /* A fourth face, used for exactly one line: "Save the date" over the
     numerals. It earns its request because nothing already loaded can do the
     job — Cormorant's italic is a slanted serif, not a hand, and setting a
     script line in it would read as an accident. Pinyon Script ships one
     weight and one style; there is no other cut to ask for.

     Why this face and not one of the other scripts on Google Fonts: copperplate
     stroke contrast. Great Vibes, Ephesis and Allura are close to monoline — an
     even stroke the whole way round — which reads friendly rather than
     engraved, and Great Vibes was tried here and rejected for exactly that.
     Pinyon has the thick downstroke and hairline connector of a nib, which is
     what an engraved save-the-date actually looks like.

     The generic 'cursive' at the end of the stack is a real fallback here, not
     boilerplate: if the webfont never arrives, the line should still look like
     handwriting rather than dropping to Times. */
  --script: 'Pinyon Script', 'Snell Roundhand', 'Apple Chancery', cursive;

  --shadow-soft: 0 1px 2px rgba(35, 38, 31, 0.04), 0 8px 24px rgba(35, 38, 31, 0.06);
  --radius: 2px;
  --nav-h: 4.25rem;

  /* One measure for the page's side gutters, so every section lines up. */
  --gutter: clamp(1.25rem, 5vw, 4rem);
  --measure: 68rem;
}

*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  /* Anchor targets stop below the sticky nav instead of underneath it. */
  scroll-padding-top: var(--nav-h);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
}

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--body);
  font-size: 1rem;
  line-height: 1.65;
  font-weight: 300;
  -webkit-font-smoothing: antialiased;
}

/* `height: auto` is doing real work here, not tidying up. Every <img> carries
   width/height attributes so the browser can reserve space before the file
   arrives, and those attributes map to a presentational `height` that beats
   `aspect-ratio` unless CSS overrides the height explicitly. Without this line
   the gallery renders every photo at its literal pixel height. */
img { max-width: 100%; height: auto; display: block; }

h1, h2, h3 {
  font-family: var(--display);
  font-weight: 300;
  color: var(--ink-strong);
  letter-spacing: 0.01em;
  margin: 0 0 0.5em;
}

h2 { font-size: clamp(2rem, 4vw, 2.9rem); line-height: 1.15; }
h3 { font-size: 1.35rem; line-height: 1.3; }

p { margin: 0 0 1.1rem; }
a { color: var(--sage-deep); text-underline-offset: 0.2em; }
a:hover { color: var(--ink-strong); }

:focus-visible {
  outline: 2px solid var(--sage-deep);
  outline-offset: 3px;
  border-radius: 1px;
}

/* A word set in small letterspaced caps — used above every section heading. */
.eyebrow {
  margin: 0 0 0.9rem;
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--sage-deep);
}

.script-accent {
  font-family: var(--display);
  font-style: italic;
  font-size: 1.25rem;
  color: var(--sage-deep);
  margin: 0 0 0.35rem;
}

/* ---------------------------------------------------------------- nav ---- */

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  gap: 1.5rem;
  padding: 0 var(--gutter);
  background: rgba(251, 249, 244, 0.92);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s ease, background 0.3s ease;
}

/* Set by main.js once the hero has scrolled past, so the bar only grows a rule
   when there is content behind it to separate from. */
.nav.is-stuck { border-bottom-color: var(--rule); }

.nav-mark {
  /* Sized to a 44px target rather than to the two letters inside it. */
  display: flex;
  align-items: center;
  min-height: 2.75rem;
  font-family: var(--display);
  font-size: 1.15rem;
  letter-spacing: 0.06em;
  color: var(--ink-strong);
  text-decoration: none;
  white-space: nowrap;
}

.nav-links {
  display: flex;
  gap: clamp(0.75rem, 2vw, 1.75rem);
  list-style: none;
  padding: 0;
  margin: 0;
}

.nav-links a {
  display: block;
  padding: 0.25rem 0;
  font-size: 0.72rem;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.nav-links a:hover { color: var(--sage-deep); border-bottom-color: var(--sage); }
/* Set by the scroll spy in main.js. */
.nav-links a[aria-current='true'] { color: var(--sage-deep); border-bottom-color: var(--sage-deep); }

.nav-toggle {
  display: none;
  margin-left: auto;
  /* Buttons do not inherit font-family, so without this the glyph rendered in
     the browser's default sans — Arial here — which was a fourth typeface on
     the page nobody chose. */
  font-family: var(--body);
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-strong);
  font-size: 1.1rem;
  cursor: pointer;
}

/*
  Centre the links on the bar rather than on whatever space the wordmark left
  behind. Three columns whose outer two are equal fractions; that equality is
  what does the centring, and it holds however wide "A & A" gets.

  Desktop only, and deliberately so. Below this width the list becomes a fixed
  full-width dropdown, and giving a fixed element a grid-column confines it to
  that column's area — which collapsed the menu into a narrow panel against the
  right edge. The bar has no centring to do once the links are a dropdown, so
  the simplest fix is for the grid not to exist down there.
*/
@media (min-width: 56.0625rem) {
  .nav {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
  }
  .nav-mark  { grid-column: 1; justify-self: start; }
  .nav-links { grid-column: 2; margin-left: 0; }
}

@media (max-width: 56rem) {
  .nav-toggle { display: block; }
  .nav-links {
    position: fixed;
    inset: var(--nav-h) 0 auto 0;
    flex-direction: column;
    gap: 0;
    margin: 0;
    padding: 0.5rem var(--gutter) 1.5rem;
    background: var(--cream);
    border-bottom: 1px solid var(--rule);
    box-shadow: var(--shadow-soft);
  }
  .nav-links[hidden] { display: none; }
  .nav-links a { padding: 0.85rem 0; border-bottom: 1px solid var(--rule); letter-spacing: 0.14em; }
}

/* -------------------------------------------------------------- layout --- */

/* No scroll-margin here on purpose. `html` already carries a scroll-padding-top
   of the same nav height (see the top of this file), and the two do not pick a
   winner — they add up, which was parking every jump-to link a second nav
   height below the bar and leaving a strip of the previous section on screen.
   The padding on the scroll container is the more general of the two: it covers
   every scroll target, not only sections. */

.section {
  padding: clamp(4rem, 9vw, 7.5rem) var(--gutter);
}

.section--paper { background: var(--paper); }

/* `hidden` has to actually win here. The UA sheet gives it display:none at the
   lowest possible weight, and any later rule that sets a display — the section
   rules below, a grid, a flex container — silently overrides it and the
   "hidden" section renders. */
[hidden] { display: none !important; }

/*
  The one dark band on the page. Details earns it: it is where the cue from the
  hero lands, and a colour change is a stronger "you have arrived" than another
  cream section with a different heading.

  Contrast, measured against --sage-deep #58664f rather than eyeballed:
    --cream      #fbf9f4   5.82:1   headings, links
    --sage-mist  #dfe5d8   4.77:1   secondary text
  Both clear AA for body text. The cards keep their cream ground, so everything
  inside them is unchanged and still measures 5.4:1 or better — which is the
  contrast the section is after: light tiles standing off a dark field, rather
  than low-contrast text laid directly on the green.
*/
.section--forest {
  background: var(--sage-deep);
  color: var(--sage-mist);
  /* A shade tighter at the top than the other sections. The date is a display
     numeral with a lot of air built into its own line box, so the standard
     section padding reads as a gap above it rather than as breathing room. */
  padding-top: calc(clamp(4rem, 9vw, 7.5rem) - 1rem);
}
.section--forest h2 { color: var(--cream); }
.section--forest .eyebrow { color: var(--sage-mist); }

/* Links have to carry the underline here. On cream a link is told apart by its
   green; on the green there is no hue left to change, so the underline is the
   only thing distinguishing it from the sentence around it. */
.section--forest a {
  color: var(--cream);
  text-decoration: underline;
  text-decoration-thickness: 1px;
}
.section--forest a:hover { color: #fff; }



/*
  The tiles here carry no fill. With no ground of their own the 32px radius
  needs a visible edge or the shape does not exist at all, so they are outlined
  in cream at low alpha — enough to draw the corner, not enough to box the text
  in. Everything inside them therefore sits directly on the green and is
  measured against it: --cream at 5.82:1, --sage-mist at 4.77:1.
*/
.section--forest .card {
  border-color: rgba(251, 249, 244, 0.3);
  color: var(--cream);
}
.section--forest .card h3,
.section--forest .card strong { color: var(--cream); }
.section--forest .card .meta { color: var(--sage-mist); }

/*
  "Save the date", set as a hand above the numerals.

  Two decisions worth keeping:

  - It is --cream, not the --sage-mist used by the "Austin, TX area" line
    beneath the date. Mist on this green measures 4.77:1, which passes AA, but
    WCAG measures colour and this face is close to hairline in its thin
    strokes — the one case where a ratio that only just passes visibly fails.
    Cream buys 5.82:1. The hierarchy is carried by size and by the change of
    face, which is more than enough separation from the numerals.
  - Letter-spacing is left alone. A script is drawn to join up; tracking it out
    breaks the joins and leaves the line looking like disconnected letters.
*/
.details-script {
  margin: 0 0 0.1rem;
  text-align: center;
  font-family: var(--script);
  font-weight: 400;
  /* A script carries tall ascenders and deep descenders inside its own line
     box, so a normal line-height opens a gap between this and the date that no
     margin asked for. Tightening the box is what closes it — the glyphs
     overflow the box rather than being clipped, which is what we want.

     On size: Pinyon ships one weight — there is no thin cut to reach for — so
     size is the only lever for making this sit under the date rather than
     compete with it. It sets about 5.3x its own font-size wide, so the cap is
     the number that matters: 2.8rem puts the line at ~240px beneath a 315px
     date, where 3.4rem had it at 263px and reading as a second headline.
     At the 1.6rem floor it is ~136px inside a 320px phone's 280px measure. */
  font-size: clamp(1.6rem, 5.8vw, 2.8rem);
  line-height: 1.05;
  color: var(--cream);
}

/*
  The date is the section's headline — the actual <h2> is hidden — so it takes
  the hero's display scale. Not quite the hero's numbers: "03/27/27" is eight
  glyphs where "ANDREA" is six, and at 16vw it runs off a narrow phone. The
  cap is a little lower and the fluid step a little shallower to buy that back.
*/
.details-date {
  margin: 0 0 1.25rem;
  text-align: center;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(2.6rem, 13.5vw, 7rem);
  line-height: 0.9;
  letter-spacing: 0.01em;
  color: var(--cream);
}

/* Sits directly under the date as its second line, so the gap between them is
   tighter than the gap to anything else.

   This rule was deleted by accident when the single photograph became a
   carousel, and the line fell back to a left-aligned 16px paragraph. Nothing
   errors when a rule goes missing — it just quietly looks wrong. */
.details-place {
  margin: 0 0 0.5rem;
  text-align: center;
  font-family: var(--mono);
  font-weight: 400;
  font-size: 0.72rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--sage-mist);
}

/*
  Carousels. Two of them: the photographs in Date, and the five story cards.

  The base rules below are the mechanism, shared. `.carousel--photos` adds what
  is specific to a row of photographs — the fixed slide width, the dimmed
  neighbours, the 4:5 aperture — and `.carousel--story` sets one card filling
  the track. Anything that is about a photograph belongs in the modifier, not
  the base, or it lands on the story cards too.

  Scroll-snap rather than a slider: the track is a real scrolling element, so a
  phone swipes it natively with the right physics, the arrow keys move it once
  it has focus, and it works before any JavaScript runs. The buttons and the
  counter are the only scripted parts.

  Two layouts. On a phone one photograph fills the track. On a wide screen the
  slides are a fixed width and the track is much wider than one of them, so the
  neighbours sit either side of the centre — the padding-inline is what lets the
  first and last still reach the middle.
*/
.carousel {
  position: relative;
  margin: clamp(2.25rem, 5vw, 3.5rem) auto clamp(2.5rem, 6vw, 4rem);
}

.carousel--photos {
  --slide: 24rem;
  width: min(82vw, var(--slide));
}

/* One card at a time, and no fixed slide width: a story card is text, and text
   wants the measure the section already sets, not a width borrowed from a
   photograph. */
.carousel--story { width: 100%; }

.carousel-track {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* The scrollbar would draw a line across the bottom of the photograph, and
     the buttons below already say this scrolls. */
  scrollbar-width: none;
}
.carousel-track::-webkit-scrollbar { display: none; }
/* Colour comes from the global :focus-visible rules, which already know about
   the green ground. Only the offset is the carousel's own business. */
.carousel-track:focus-visible { outline-offset: 4px; }

.carousel-slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  /* Without this a flex item refuses to shrink below its content and the
     slides end up wider than the track, so the snap points never line up with
     a photograph. */
  min-width: 0;
}
.carousel--photos .carousel-slide img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: 32px;
  background: rgba(251, 249, 244, 0.08);
}

@media (min-width: 60rem) {
  /* Wide enough for the centre photograph plus most of the two beside it. */
  .carousel--photos { width: min(94vw, 62rem); }
  .carousel--photos .carousel-slide { flex: 0 0 var(--slide); }
  /* Half a track minus half a slide, so the first and last can still centre
     rather than jamming against the ends. */
  .carousel--photos .carousel-track { padding-inline: calc(50% - var(--slide) / 2); gap: 1.25rem; }

  /*
    The neighbours are context, not competition. Dimmed and set back rather
    than hidden, so it reads as one photograph with more on either side.
    .is-active is set by main.js from scroll position — CSS has no way to ask
    which child is nearest the middle of a scroller.
  */
  .carousel--photos .carousel-slide img {
    opacity: 0.32;
    transform: scale(0.94);
    transition: opacity 0.35s ease, transform 0.35s ease;
  }
  .carousel--photos .carousel-slide.is-active img { opacity: 1; transform: none; }
}

/* Below the photograph, not on top of it: an arrow sitting over a face is the
   one place on this page where a control obscures the thing it is for. */
.carousel-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.25rem;
  margin-top: 1.1rem;
}

.carousel-btn {
  display: grid;
  place-items: center;
  /* 44px, per WCAG 2.5.5. */
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: transparent;
  color: var(--sage-deep);
  font-family: var(--body);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
}
/* These were written when the only carousel was on the green, so cream was the
   base. There is one on cream now too, so the base is the light-ground version
   and the green takes the override — the way every other component on this page
   handles the two grounds. */
.carousel-btn:hover { background: var(--sage-mist); border-color: var(--sage-deep); }
.section--forest .carousel-btn { border-color: rgba(251, 249, 244, 0.4); color: var(--cream); }
.section--forest .carousel-btn:hover { background: rgba(251, 249, 244, 0.12); border-color: var(--cream); }

/* At the ends the button has nothing to do. Dimmed and inert rather than
   removed, so the row does not reflow under the cursor. */
.carousel-btn[disabled] { opacity: 0.3; cursor: default; }
.carousel-btn[disabled]:hover { background: transparent; border-color: var(--rule); }
.section--forest .carousel-btn[disabled]:hover { border-color: rgba(251, 249, 244, 0.4); }

.carousel-count {
  margin: 0;
  min-width: 4.5rem;
  text-align: center;
  font-family: var(--mono);
  font-weight: 400;
  font-size: 0.72rem;
  letter-spacing: 0.2em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}
.section--forest .carousel-count { color: var(--sage-mist); }

/* The smooth scroll and the dimming transition are the motion here, and both
   are the kind somebody who asked for less of it does not want. Snapping still
   works; it just arrives immediately. */
@media (prefers-reduced-motion: reduce) {
  .carousel-track { scroll-behavior: auto; }
  .carousel--photos .carousel-slide img { transition: none; }
}

/* The venue's name inside the Date section. It is an h3 — a part of Date, not
   a section of its own — but it opens a distinct block after the carousel, so
   it is sized between the two rather than at the h3 default of 1.35rem. */
.venue-head { margin-top: clamp(3rem, 7vw, 4.5rem); }
.venue-head h3 {
  font-size: clamp(1.6rem, 3.2vw, 2.2rem);
  line-height: 1.15;
  margin-bottom: 0;
}

.venue-sketch {
  margin: clamp(1.5rem, 4vw, 2.5rem) auto clamp(2.25rem, 5vw, 3.5rem);
  max-width: 34rem;
}
.venue-sketch img { width: 100%; }

/*
  Both selectors, and both are needed.

  A lede on the green can sit in two places: inside a .section-head (Travel,
  Q + A) or loose under the tiles (Details). The base rule for the first case is
  `.section-head p.lede`, which at 0-2-1 outranks a plain `.section--forest
  .lede` at 0-2-0 — so colouring only the short selector leaves the heading
  ledes on --ink-muted, a mid grey meant for cream, measuring 1.08:1 here. That
  is the second time this exact miss has happened on this section; the base
  .lede colour is more specific than it looks. */
.section--forest .lede,
.section--forest .section-head p.lede { color: var(--sage-mist); }

/* The Details lede is a footnote under the tiles rather than a preamble under
   a heading, so it is the one that wants its own spacing. */
.details-note {
  max-width: 34rem;
  margin: clamp(2.5rem, 6vw, 3.5rem) auto 0;
  text-align: center;
  font-size: 0.95rem;
}
.section--forest .map-frame { border-color: rgba(251, 249, 244, 0.35); }

:focus-visible { outline-color: var(--sage-deep); }
.section--forest :focus-visible { outline-color: var(--cream); }

/*
  Travel, Q + A and the Honeymoon fund sit on the green now too, and every
  component in them was coloured for cream. These are the overrides that keeps
  them legible; the ratios are all against --sage-deep.

  The QR plate is the exception that stays white — a Venmo code has to be a
  dark pattern on a light ground to scan at all, so it keeps its own card
  inside the section rather than inheriting the green.
*/
.section--forest .faq,
.section--forest .faq details { border-color: rgba(251, 249, 244, 0.22); }
.section--forest .faq summary { color: var(--cream); }
.section--forest .faq summary:hover { color: #fff; }
.section--forest .faq summary::after { color: var(--sage-mist); }
.section--forest .faq .answer { color: var(--sage-mist); }

.section--forest .fund { border-color: rgba(251, 249, 244, 0.3); }

/* Registry and the fund are one section now, and it is the green one, so the
   registry cards need what every other tile on this ground needed. The card is
   an <a>, which is the part worth flagging: `.section--forest a` underlines
   links on purpose, because on the green there is no hue left to tell a link
   from its sentence — but these are tiles, told apart by their border, and an
   underline through a 1.5rem display face just looks like a mistake.
   Ratios on --sage-deep: --cream 5.82:1, --sage-mist 4.77:1. The hover border
   is --sage-mist and not --sage, which measures 2.05:1 here and vanishes. */
.section--forest .registry-card {
  border-color: rgba(251, 249, 244, 0.3);
  color: var(--cream);
  text-decoration: none;
}
.section--forest .registry-card:hover {
  border-color: var(--sage-mist);
  color: var(--cream);
}
.section--forest .registry-card .go { color: var(--sage-mist); }

/* The fund's own heading, opening the second half of Registry & Fund. Less air
   above it than a section head takes, because it is not a new section. */
.fund-head { margin-top: clamp(3rem, 7vw, 4.5rem); margin-bottom: clamp(1.5rem, 3vw, 2rem); }
.fund-head h3 { font-size: clamp(1.5rem, 3vw, 2rem); margin-bottom: 0; }
.section--forest .hint { color: var(--sage-mist); }

/* Left on its light chip on purpose: the handle is the one string on the page
   somebody has to copy correctly, and it earns a plate of its own. */
.section--forest .handle { background: var(--sage-mist); color: var(--ink-strong); }

.section--forest strong { color: var(--cream); }
.section--forest h3 { color: var(--cream); }

.wrap { max-width: var(--measure); margin: 0 auto; }
.wrap--narrow { max-width: 44rem; }

.section-head { text-align: center; margin-bottom: clamp(2.5rem, 5vw, 4rem); }
.section-head p.lede {
  max-width: 34rem;
  margin: 0 auto;
  color: var(--ink-muted);
  font-size: 1.05rem;
}

/* The eucalyptus rule that separates sections. */
/*
  The section ornament: a drawn spray, and the whole divider rather than a glyph
  between two rules. The rules are gone with the glyph — the drawing already
  spans horizontally, so ruling it as well was drawing the same line twice.

  Two files, not one recoloured in CSS. The ink in this artwork is rgb(74,100,77)
  and the forest ground is #58664f, near enough the same colour that the drawing
  disappears into it; and the mask trick that would let one file take
  currentColor flattens the ink and the petal fills into a single silhouette,
  which throws away the line work that makes it worth having. So the light
  grounds get the drawing as drawn, and the green gets a version with the ink
  lifted to --sage-mist and the fills dropped to nothing.

  Sized at 14rem against artwork 448px wide, so it is still 2x on a retina
  screen. The aspect-ratio is the file's own, and holds the space before the
  image arrives so nothing below it jumps.
*/
.divider {
  width: 100%;
  max-width: 14rem;
  aspect-ratio: 448 / 226;
  margin: 0 auto 1.25rem;
  background: url('/images/divider-rose.png') center / contain no-repeat;
}
.section--forest .divider { background-image: url('/images/divider-rose-light.png'); }

/* ---------------------------------------------------------------- hero --- */

/*
  A burst of snapshots that clears to nothing.

  Five photographs cut from one to the next in a small centred box on the
  page's own cream, then the whole box cuts away and the names fade up onto an
  empty ground. There is no cross-fade anywhere in it: every frame swap and the
  final clear are step-end, so each picture is either fully there or fully gone
  and never a blend of two. That hard edge is the effect — a blend would read as
  a slideshow, and this is meant to read as a shutter.

  --slot is how long one photograph holds, and every other time below is
  written as a multiple of it, so retiming the whole opening means changing one
  number. main.js keys off the typed line's own animation rather than repeating
  any of these values.
*/
.hero {
  --slot: 600ms;
  --frames: 6;
  /* When the last photograph starts leaving — not when it has gone. */
  --cleared: calc(var(--slot) * var(--frames));
  /* The one soft edge in the whole opening. Every swap before this is a hard
     cut; the last frame dissolves instead, because cutting straight from a
     photograph to the names read as the page having jumped rather than having
     arrived somewhere. */
  --clear-fade: 1100ms;
  /* The names begin while the photograph is still going, so the two cross
     rather than queue. They stay faint until it has gone — by the time they
     are legible there is nothing behind them. */
  --names-at: calc(var(--cleared) + 500ms);
  --names-fade: 1500ms;
  --type-at: calc(var(--names-at) + 1600ms);

  position: relative;
  /* The bar sits above the hero in flow now that it is opaque again, so the
     hero gets the viewport minus the bar. A flat 100svh here would push the
     hero a full nav-height past the fold and take the scroll cue — which is
     anchored to the hero's bottom edge — off screen with it. */
  min-height: calc(100svh - var(--nav-h));
  display: grid;
  place-items: center;
  text-align: center;
  /* The bottom is deeper than the top on purpose: it is the room the scroll
     cue sits in, and the names should still read as optically centred. */
  padding: clamp(2rem, 5vw, 3.5rem) var(--gutter) clamp(5rem, 10vw, 7rem);
  background: var(--cream);
}

/*
  The snapshot box. Absolutely centred rather than in flow, because the names
  occupy the same optical centre a moment later and the two must not push each
  other around — they are never both on screen, so they can share the spot.
*/
.hero-stage {
  position: absolute;
  left: 50%;
  top: 50%;
  /* The third term is the height constraint expressed as a width. The box is
     4:5, so its height is width x 1.25 and the widest it may be is 0.8 of the
     room the hero actually has — which keeps a landscape phone, or any short
     window, from cropping the snapshots off the top and bottom. Written this
     way rather than as a max-height because a max-height would override the
     aspect-ratio and quietly reshape the window instead of scaling it. */
  width: min(78vw, 25rem, calc((100svh - var(--nav-h) - 9rem) * 0.8));
  transform: translate(-50%, -50%);
  /* Fades the whole box out once the last frame has had its turn. The frames
     themselves still cut — see hero-snap below — so this is the only soft
     transition in the sequence, and it is the one carrying the burst into the
     names. */
  animation: hero-clear var(--clear-fade) ease both;
  animation-delay: var(--cleared);
}

/* One fixed window that every photograph is cropped into. Without it the box
   would resize on each cut — the set is one portrait and four landscape — and
   the burst would read as a layout bug rather than as a sequence. */
.hero-frames {
  position: relative;
  aspect-ratio: 4 / 5;
}

.hero-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* The files are already cut to 4:5, so there is nothing here for cover to
     crop; it stays as a guard against a frame being swapped for one of another
     shape later, and costs nothing while the ratios agree. Per-frame
     object-position used to live here and has gone with the crop — the framing
     is chosen at build time now, where it can be seen. */
  object-fit: cover;
  opacity: 0;
  /* Each frame snaps on at its turn and holds. Later frames sit above earlier
     ones, so covering is all that is needed and nothing has to snap back off —
     which also means a dropped frame can never leave a gap mid-burst.
     1ms linear for the same reason as the clear above: instant to the eye,
     and unambiguous about where it lands. */
  animation: hero-snap 1ms linear both;
}
.hero-frame:nth-child(1) { animation-delay: calc(var(--slot) * 0); }
.hero-frame:nth-child(2) { animation-delay: calc(var(--slot) * 1); }
.hero-frame:nth-child(3) { animation-delay: calc(var(--slot) * 2); }
.hero-frame:nth-child(4) { animation-delay: calc(var(--slot) * 3); }
.hero-frame:nth-child(5) { animation-delay: calc(var(--slot) * 4); }
.hero-frame:nth-child(6) { animation-delay: calc(var(--slot) * 5); }

.hero-inner {
  position: relative;
  max-width: 52rem;
  color: var(--ink);
}


.hero-names {
  display: grid;
  justify-items: center;
  margin: 0;
  font-family: var(--display);
  font-weight: 300;
  text-transform: uppercase;
  /* The lower bound is what fits "ANDREA" on a 320px phone without breaking. */
  font-size: clamp(3.1rem, 16vw, 8.5rem);
  line-height: 0.84;
  letter-spacing: 0.02em;
  color: var(--ink-strong);
}

.hero-names-part,
.hero-names-amp { display: block; }

.hero-names-amp {
  font-style: italic;
  text-transform: none;
  font-size: 0.3em;
  letter-spacing: 0;
  margin: 0.18em 0;
  color: var(--sage-deep);
}

/* The two names and the ampersand arrive in reading order, a beat apart. */
.hero-names > * {
  animation: hero-rise var(--names-fade) cubic-bezier(0.22, 0.61, 0.24, 1) both;
}
.hero-names > *:nth-child(1) { animation-delay: var(--names-at); }
.hero-names > *:nth-child(2) { animation-delay: calc(var(--names-at) + 150ms); }
.hero-names > *:nth-child(3) { animation-delay: calc(var(--names-at) + 300ms); }

/*
  The typed line. main.js listens for this element's own `animationstart` and
  begins typing then, so the handoff from the CSS sequence to the script has a
  single source of truth — the delay below — instead of the same millisecond
  count written out in two files that drift apart.
*/
.hero-typed {
  margin: 1.9rem 0 0;
  font-family: var(--mono);
  font-weight: 400;
  font-size: clamp(0.7rem, 2.7vw, 0.84rem);
  line-height: 1.9;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  animation: hero-fade-in 600ms ease both;
  animation-delay: var(--type-at);
}

/* Holds its line box while empty, so the date below does not jump upward
   between the script blanking the text and typing it back. */
.hero-typed-line {
  display: block;
  min-height: 1.9em;
}

.hero-typed-line.is-typing::after {
  content: '';
  display: inline-block;
  width: 0.5em;
  height: 1.05em;
  margin-left: 0.14em;
  vertical-align: -0.16em;
  background: currentColor;
  animation: hero-caret 1s steps(1, end) infinite;
}

.hero-heart { color: var(--blush); }

.hero-cue {
  position: absolute;
  left: 50%;
  bottom: 2.1rem;
  transform: translateX(-50%);
  display: grid;
  justify-items: center;
  gap: 0.7rem;
  padding: 0.5rem 1rem;
  font-family: var(--mono);
  font-weight: 400;
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--sage-deep);
  text-decoration: none;
}
.hero-cue:hover { color: var(--ink-strong); }
.hero-cue-arrow {
  font-size: 1rem;
  line-height: 1;
  animation: hero-nudge 2.4s ease-in-out infinite;
}

/*
  The scroll cue is held back until the typing has finished, and
  "finished" is something only the script knows: the typing speed jitters on
  purpose, so any millisecond count written here would be wrong on most loads.
  main.js sets .hero-sequencing as soon as it takes charge — before the burst
  starts, so neither element is ever visible over the photographs — and
  .is-typed when it lets go.

  The hiding is scoped to .hero-sequencing rather than being the default, so
  both are simply visible when the script is blocked or skipped under reduced
  motion. main.js also lifts the class itself if the sequence never starts, so
  a hero that stalls resolves to the readable state instead of a blank one.
*/
.hero.hero-sequencing .hero-cue {
  opacity: 0;
}
.hero.hero-sequencing.is-typed .hero-cue {
  opacity: 1;
  transition: opacity 900ms ease;
}

/*
  A phone held sideways has less height than the names, the typed line, the date
  and the cue want between them, so the hero grows past the fold and takes the
  snapshot box off the bottom of the screen with it. Tighten the type and the
  padding until the whole opening resolves inside one screen again. Scoped by
  orientation as well as height so it cannot catch a short desktop window, where
  there is plenty of width and the full-size names are still right.
*/
@media (max-height: 30rem) and (orientation: landscape) {
  .hero { padding-block: 1.25rem 3rem; }
  .hero-names { font-size: clamp(2rem, 9vh, 3.2rem); }
  .hero-typed { margin-top: 1rem; line-height: 1.7; }
  .hero-typed-line { min-height: 1.7em; }
  .hero-cue { bottom: 1rem; gap: 0.4rem; }
}

@keyframes hero-snap {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes hero-clear {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes hero-rise {
  /* Barely any travel. Over 1.5s a half-em slide stops reading as a settle and
     starts reading as movement for its own sake; the fade is doing the work. */
  from { opacity: 0; transform: translateY(0.22em); }
  to   { opacity: 1; transform: none; }
}
@keyframes hero-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes hero-caret {
  0%, 50%       { opacity: 1; }
  50.01%, 100%  { opacity: 0; }
}
@keyframes hero-nudge {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(0.3rem); }
}

/*
  Reduced motion gets the finished hero, not a faster one. The global rule near
  the top of this file collapses every duration to ~0, which lands each element
  on its end state immediately: the burst is already over, the stage is already
  cleared, and the names are simply there on an empty page — which is exactly
  where the animated version ends up anyway. The blinking caret and the nudging
  arrow are the two that would still read as movement, so they are switched off
  by hand. main.js checks the same media query and writes the typed line out in
  full rather than typing it.
*/
@media (prefers-reduced-motion: reduce) {
  .hero-cue-arrow { animation: none; }
  .hero-typed-line.is-typing::after { animation: none; opacity: 1; }
}

/* -------------------------------------------------------------- counter -- */

.countdown {
  display: inline-flex;
  gap: clamp(1rem, 4vw, 2.25rem);
  padding: 1.15rem clamp(1.25rem, 4vw, 2.25rem);
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.countdown div { min-width: 3.25rem; }
.countdown .value {
  display: block;
  font-family: var(--display);
  font-size: clamp(1.75rem, 5vw, 2.5rem);
  line-height: 1;
  color: var(--ink-strong);
  font-variant-numeric: tabular-nums;
}
.countdown .unit {
  display: block;
  margin-top: 0.35rem;
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.countdown-note {
  margin: 1.5rem 0 0;
  font-family: var(--display);
  font-style: italic;
  font-size: 1.2rem;
  color: var(--sage-deep);
}

/*
  The band the counter moved into when it came out of the hero. It is the first
  thing under a full-bleed dark photograph, so it is deliberately quiet: paper
  ground, a hairline above and below, and no heading. Its job is to be the
  landing after the montage, not a second event.
*/
.countbar {
  padding: clamp(2.75rem, 7vw, 4rem) var(--gutter);
  /* Paper: the RSVP section above it is cream now that the page alternates
     green and cream, so this band needs to be the one that is not. */
  background: var(--paper);
  border-top: 1px solid var(--rule);
  text-align: center;
}

.countbar-inner {
  max-width: var(--measure);
  margin: 0 auto;
  display: grid;
  justify-items: center;
  gap: 0.35rem;
}

.countbar .countdown-note { margin: 0.9rem 0 0; }
.countbar-cta { margin: 1.6rem 0 0; }

/* -------------------------------------------------------------- buttons -- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 2.75rem;  /* 44px, per WCAG 2.5.5 */
  padding: 0.7rem 1.6rem;
  border: 1px solid var(--sage-deep);
  border-radius: var(--radius);
  background: var(--sage-deep);
  color: var(--cream);
  font-family: var(--body);
  font-size: 0.78rem;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}
.btn:hover { background: var(--ink-strong); border-color: var(--ink-strong); color: var(--cream); }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }

.btn-quiet {
  background: transparent;
  color: var(--sage-deep);
  border-color: var(--sage);
}
.btn-quiet:hover { background: var(--sage-mist); color: var(--ink-strong); border-color: var(--sage-deep); }

.btn-link {
  border: 0;
  background: none;
  padding: 0.25rem 0;
  min-height: 0;
  color: var(--sage-deep);
  text-transform: none;
  letter-spacing: 0.02em;
  font-size: 0.95rem;
  text-decoration: underline;
  cursor: pointer;
}

/* ---------------------------------------------------------------- story -- */

/*
  A story card: the words and one photograph, sized to be read on its own.

  This was a sticky pile — every card pinned at the same offset, the next
  sliding over the top. It is a carousel now, and the reason is not taste. A
  pinned card taller than the space between the sticky line and the fold has
  its bottom off screen the moment it pins, and never shows it again, because
  the next card rises over it rather than pushing it up. With one line of
  placeholder text per card that never came up; with the real copy it was 815px
  of card against 720px of room on an ordinary phone, and what fell off the
  bottom was most of the photograph and its caption. A carousel has no such
  ceiling — the card is as tall as it needs to be and the page scrolls past it.
*/
.story-card {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
  padding: clamp(1.5rem, 4vw, 2.5rem);
  border-radius: 32px;
  /*
    Told apart from the section by tone, not by a drop shadow, and that is
    forced rather than chosen.

    `overflow-x: auto` on the track forces overflow-y out of `visible` too, so
    the track clips on both axes — and the card fills its slide exactly, with
    zero room on all four sides. A box-shadow paints entirely outside its box,
    so every pixel of it was being clipped hard against the card's edges. The
    card's corners are rounded and that clip is not, so the shadow survived
    only in the four corner notches: a grey right-angle at each corner of a
    rounded card, which is what read as a rectangle sitting on top of it.

    Making the shadow smaller or fainter would not have helped — it would have
    been clipped to the same shape, just quieter. Giving it room would mean
    padding the track to match the blur and keeping the two in step forever.
    Paper against the section's cream does the job with nothing to clip.
  */
  background: var(--paper);
  border: 1px solid var(--rule);
}
@media (min-width: 52rem) {
  .story-card { grid-template-columns: 1fr auto; }
}

/* Flex stretches every slide to the tallest of them, so the card has to fill
   its slide or the shorter stages sit in a box with a gap underneath. */
.carousel--story .carousel-slide { display: flex; }
.carousel--story .story-card { flex: 1; }

/* Slides are 100% of the track, so without this the next card butts straight
   against the current one and they read as one long strip mid-swipe. It was
   3rem when .story-card still had a shadow to keep out of the neighbour; the
   card has no shadow now, so this is back to being ordinary spacing. */
.carousel--story .carousel-track { gap: 1.5rem; }

.story-card-text h3 { margin-bottom: 0.4rem; }
.story-card-text p:last-child { margin-bottom: 0; }

.story-stage {
  margin: 0 0 0.5rem;
  font-family: var(--mono);
  font-weight: 400;
  font-size: 0.65rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--sage-deep);
}

/*
  The polaroid itself: a square aperture, an even border on three sides and a
  deep one at the foot, which is the whole of what makes the shape read. The
  caption lives in that deep border, so it is set in the space the frame
  already had rather than in space added underneath it.
*/
.polaroid {
  margin: 0 auto;
  width: min(68vw, 20rem);
  padding: 0.85rem 0.85rem 0;
  background: #fff;
  border-radius: 2px;
  box-shadow: 0 1px 2px rgba(35, 38, 31, 0.08), 0 10px 24px rgba(35, 38, 31, 0.13);
}
.polaroid img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  background: var(--paper-deep);
}
.polaroid figcaption {
  padding: 0.9rem 0.2rem 1.1rem;
  text-align: center;
  font-family: var(--display);
  font-style: italic;
  font-size: 0.95rem;
  line-height: 1.35;
  color: var(--ink-muted);
}

/* A photograph someone placed is never quite straight. Kept under three
   degrees — past that it stops reading as placed and starts reading as a bug.
   Keyed to the slide rather than the card, because each card is now alone
   inside its slide and would always be :nth-child(1). */
.carousel--story .carousel-slide:nth-child(1) .polaroid { transform: rotate(-2.2deg); }
.carousel--story .carousel-slide:nth-child(2) .polaroid { transform: rotate(1.6deg); }
.carousel--story .carousel-slide:nth-child(3) .polaroid { transform: rotate(-1.1deg); }
.carousel--story .carousel-slide:nth-child(4) .polaroid { transform: rotate(2.4deg); }
.carousel--story .carousel-slide:nth-child(5) .polaroid { transform: rotate(-1.8deg); }

/* The aside at the end of the proposal. Set apart from the paragraph it
   follows, because it is an aside — said quieter, and after a beat. */
.story-ps {
  display: block;
  margin-top: 0.9rem;
  font-family: var(--display);
  font-style: italic;
  font-size: 1.05rem;
  color: var(--ink-muted);
}

/* The carousel's smooth scroll is the motion here, and it is turned off with
   the other carousel's in the block above. Nothing in the card itself moves. */

/* -------------------------------------------------------------- details -- */

.card-grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

/* Outlined rather than filled, everywhere. The tiles used to be cream slabs on
   a cream page, which meant their edges were doing all the work of separating
   them from the ground and none of it well. An outline and a generous radius
   say "this is a group of facts" without pretending to be a raised surface. */
.card {
  padding: clamp(1.75rem, 4vw, 2.5rem);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 32px;
}
.card h3 { margin-bottom: 0.4rem; }
.card .meta {
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--sage-deep);
  margin-bottom: 0.75rem;
}
.card p:last-child { margin-bottom: 0; }

.map-frame {
  margin-top: 2.5rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--paper-deep);
  aspect-ratio: 16 / 7;
}
.map-frame iframe { width: 100%; height: 100%; border: 0; display: block; }

/* --------------------------------------------------------------- photos -- */

.gallery {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(2, 1fr);
  /* The wide slots span two columns, which leaves a hole whenever one cannot
     fit in the remaining space. `dense` backfills the hole with the next photo
     that does fit. It reorders the display relative to the DOM, which is fine
     for a gallery and would not be for a list you have to read in order. */
  grid-auto-flow: dense;
}
@media (min-width: 46rem) { .gallery { grid-template-columns: repeat(3, 1fr); gap: 1rem; } }

.gallery figure { margin: 0; }
.gallery img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--paper-deep);
}
/* One photo per row-pair spans wider, so the grid does not read as a contact
   sheet. Applied to every fourth item. */
@media (min-width: 46rem) {
  .gallery figure:nth-child(4n + 1) { grid-column: span 2; }
  .gallery figure:nth-child(4n + 1) img { aspect-ratio: 16 / 10; }
}

/* ------------------------------------------------------------------ faq -- */

.faq { border-top: 1px solid var(--rule); }
.faq details {
  border-bottom: 1px solid var(--rule);
}
.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.35rem 0;
  font-family: var(--display);
  font-size: 1.25rem;
  color: var(--ink-strong);
  cursor: pointer;
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: '+';
  flex: none;
  width: 1.5rem;
  text-align: center;
  font-family: var(--body);
  font-size: 1.35rem;
  font-weight: 300;
  color: var(--sage-deep);
  transition: transform 0.2s ease;
}
.faq details[open] summary::after { content: '\2013'; }
.faq summary:hover { color: var(--sage-deep); }
.faq .answer { padding: 0 0 1.5rem; max-width: 46rem; color: var(--ink); }
.faq .answer p:last-child { margin-bottom: 0; }

/* ------------------------------------------------------------- registry -- */

.registry-grid {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}
.registry-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 9rem;
  padding: 2rem 1.5rem;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 32px;
  color: var(--ink-strong);
  text-decoration: none;
  text-align: center;
  transition: border-color 0.2s ease, transform 0.2s ease;
}
.registry-card:hover { border-color: var(--sage); transform: translateY(-2px); color: var(--ink-strong); }
.registry-card .name { font-family: var(--display); font-size: 1.5rem; }
.registry-card .go {
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--sage-deep);
}

/* ------------------------------------------------------------ honeymoon -- */

.fund {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
  grid-template-columns: 1fr;
  padding: clamp(1.75rem, 4vw, 3rem);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 32px;
}
@media (min-width: 44rem) { .fund { grid-template-columns: auto 1fr; } }

.fund-qr {
  width: clamp(9rem, 30vw, 12rem);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  padding: 0.75rem;
  background: #fff;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  margin: 0 auto;
}
.fund-qr img { width: 100%; }

.handle {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 1rem;
  padding: 0.55rem 1rem;
  background: var(--sage-mist);
  border-radius: var(--radius);
  font-family: var(--display);
  font-size: 1.5rem;
  color: var(--ink-strong);
}

/* ----------------------------------------------------------------- rsvp -- */

.rsvp-shell {
  max-width: 40rem;
  margin: 0 auto;
  padding: clamp(1.75rem, 4vw, 2.75rem);
  background: var(--cream);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-soft);
}

label { display: block; margin-bottom: 0.4rem; font-size: 0.85rem; font-weight: 400; color: var(--ink-strong); }

input[type='text'], input[type='search'], input[type='password'], textarea {
  width: 100%;
  min-height: 2.75rem;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: #fff;
  color: var(--ink-strong);
  font-family: var(--body);
  font-size: 1rem;
  font-weight: 300;
}
textarea { min-height: 6rem; resize: vertical; line-height: 1.6; }
input:focus-visible, textarea:focus-visible { border-color: var(--sage-deep); }

.field { margin-bottom: 1.25rem; }
.field .hint { margin: 0.4rem 0 0; font-size: 0.85rem; color: var(--ink-muted); }

.search-row { display: flex; gap: 0.6rem; align-items: stretch; }
.search-row input { flex: 1; }
.search-row .btn { flex: none; }
@media (max-width: 30rem) {
  .search-row { flex-direction: column; }
}

/* Search results: one button per household, the guest picks theirs. */
.match-list { list-style: none; margin: 1.5rem 0 0; padding: 0; display: grid; gap: 0.6rem; }
.match {
  width: 100%;
  text-align: left;
  padding: 1rem 1.15rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: #fff;
  cursor: pointer;
  font-family: inherit;
  color: var(--ink);
}
.match:hover { border-color: var(--sage); background: var(--sage-mist); }
.match .who { font-family: var(--display); font-size: 1.3rem; color: var(--ink-strong); display: block; }
.match .names { font-size: 0.9rem; color: var(--ink-muted); }

/* Per-guest attending / not attending. Radios styled as a segmented pair —
   still real radios underneath, so keyboard and screen readers behave. */
.guest-block {
  padding: 1.25rem 0;
  border-bottom: 1px solid var(--rule);
}
.guest-block:first-of-type { padding-top: 0; }
.guest-name { font-family: var(--display); font-size: 1.45rem; color: var(--ink-strong); margin: 0 0 0.75rem; }

.choice { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.choice label {
  flex: 1 1 8rem;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;
  padding: 0.6rem 1rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: #fff;
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.choice input { position: absolute; opacity: 0; width: 0; height: 0; }
.choice label:hover { border-color: var(--sage); }
.choice input:focus-visible + span { outline: 2px solid var(--sage-deep); outline-offset: 4px; }
.choice input:checked + span { font-weight: 400; }
.choice label:has(input:checked) { background: var(--sage-deep); border-color: var(--sage-deep); color: var(--cream); }
.choice label:has(input[value='no']:checked) { background: var(--ink-muted); border-color: var(--ink-muted); }

.dietary { margin-top: 0.9rem; }
.dietary[hidden] { display: none; }

/* The write-in guest block. Set apart from the guest's own answer so it reads
   as "and someone with you" rather than as another question about them. */
.plus-one {
  margin-top: 1.1rem;
  padding: 1rem 1.1rem;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.plus-one[hidden] { display: none; }
.plus-one-head {
  margin: 0 0 0.35rem;
  font-family: var(--display);
  font-size: 1.15rem;
  color: var(--ink-strong);
}
.plus-one .hint { margin: 0 0 0.9rem; }
.name-row { display: grid; gap: 0.75rem; grid-template-columns: 1fr 1fr; }
@media (max-width: 30rem) { .name-row { grid-template-columns: 1fr; } }

.summary-plus { padding-left: 1.25rem; font-size: 0.92rem; color: var(--ink-muted); }

.form-error {
  margin: 0 0 1rem;
  padding: 0.75rem 1rem;
  background: #f7e9e6;
  border-left: 3px solid var(--danger);
  color: var(--danger);
  font-size: 0.92rem;
}

.form-note {
  margin: 0 0 1.25rem;
  padding: 0.75rem 1rem;
  background: var(--sage-mist);
  border-left: 3px solid var(--sage-deep);
  font-size: 0.92rem;
  color: var(--ink);
}

.rsvp-success { text-align: center; }
.rsvp-success .tick {
  width: 3.5rem; height: 3.5rem; margin: 0 auto 1.25rem;
  display: grid; place-items: center;
  border-radius: 50%; background: var(--sage-mist); color: var(--sage-deep);
  font-size: 1.5rem;
}
.summary-list { list-style: none; padding: 0; margin: 0 0 1.5rem; text-align: left; }
.summary-list li {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 0.7rem 0; border-bottom: 1px solid var(--rule);
}
.summary-list .yes { color: var(--success); }
.summary-list .no { color: var(--ink-muted); }

.spinner {
  display: inline-block; width: 1rem; height: 1rem;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --------------------------------------------------------------- footer -- */

.footer {
  padding: clamp(3rem, 6vw, 5rem) var(--gutter);
  text-align: center;
  background: var(--sage-deep);
  /* Re-measured: --sage-mist is 4.77:1 on this green and does clear AA, so the
     4.2:1 this comment used to claim was simply wrong. Keeping the lighter
     tone anyway — the date line is small uppercase text with wide tracking,
     and 5.16:1 gives it headroom the boundary case does not. */
  color: #e9ede4;
}
.footer .names { font-family: var(--display); font-size: clamp(2rem, 6vw, 3rem); color: var(--cream); margin: 0 0 0.5rem; }
.footer .meta { font-size: 0.75rem; letter-spacing: 0.2em; text-transform: uppercase; }
/* The same spray as the dividers, quieter. It is always on the green here, so
   it only ever needs the light version. */
.footer-mark {
  width: 100%;
  max-width: 9rem;
  aspect-ratio: 448 / 226;
  margin: 0 auto 0.75rem;
  opacity: 0.7;
  background: url('/images/divider-rose-light.png') center / contain no-repeat;
}

/* ---------------------------------------------------------------- admin -- */

/*
  The dashboard reads at 400, not the site's 300.

  The public pages are set in Jost Light because they are mostly display type
  and short paragraphs read at arm's length. The dashboard is the opposite: a
  dense table of small labels, counts and timestamps, scanned rather than read,
  and Jost at 300 gets thin and papery below about 15px. One declaration here
  lifts every inherited weight on the page — cells, labels, hints, badges — and
  the headings keep their own 300 because they declare it themselves.
*/
.admin-body { background: var(--paper); font-weight: 400; }
.admin-wrap { max-width: 68rem; margin: 0 auto; padding: clamp(1.5rem, 4vw, 3rem) var(--gutter) 4rem; }
.admin-header {
  display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; justify-content: space-between;
  padding-bottom: 1.5rem; margin-bottom: 2rem; border-bottom: 1px solid var(--rule);
}
.admin-header h1 { margin: 0; font-size: clamp(1.9rem, 4vw, 2.5rem); }
.countdown-pill {
  margin: 0; padding: 0.5rem 1rem; background: var(--sage-mist); border-radius: 999px;
  font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--sage-deep);
}

.stats { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); margin-bottom: 2rem; }
.stat { padding: 1.25rem; background: var(--cream); border: 1px solid var(--rule); border-radius: var(--radius); }
.stat-value { margin: 0; font-family: var(--display); font-size: 2.4rem; line-height: 1; color: var(--ink-strong); }
.stat-label { margin: 0.5rem 0 0; font-size: 0.8rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-muted); }
.stat-sub { margin: 0.3rem 0 0; font-size: 0.9rem; color: var(--sage-deep); }

.panel {
  padding: clamp(1.25rem, 3vw, 2rem); margin-bottom: 1.5rem;
  background: var(--cream); border: 1px solid var(--rule); border-radius: var(--radius);
}
.panel h2 { font-size: 1.6rem; margin-bottom: 1rem; }
.panel-head { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; justify-content: space-between; }
.panel-head h2 { margin: 0; }

.filters { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.chip {
  padding: 0.45rem 0.85rem; border: 1px solid var(--rule); border-radius: 999px;
  background: transparent; color: var(--ink-muted); font-family: inherit; font-size: 0.78rem; cursor: pointer;
}
.chip:hover { border-color: var(--sage); color: var(--ink); }
.chip.is-active { background: var(--sage-deep); border-color: var(--sage-deep); color: var(--cream); }

.sync-form { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; margin: 1rem 0 0.5rem; }
/* No explicit 300 here. It was the one label on the page still declaring the
   site's light weight for itself, so it survived the 400 set on .admin-body —
   and it guards a destructive action, which is the last text that should be
   the faintest thing on screen. */
.checkbox { display: flex; align-items: center; gap: 0.5rem; margin: 0; font-size: 0.9rem; line-height: 1.5; color: var(--ink-muted); }
.checkbox input { width: 1.1rem; height: 1.1rem; accent-color: var(--sage-deep); }
.sync-status { margin: 0; font-size: 0.95rem; color: var(--ink-muted); }
.sync-status.ok { color: var(--ink); }
.sync-status.bad { padding: 0.75rem 1rem; background: #f7e9e6; border-left: 3px solid var(--danger); color: var(--danger); }
.hint { font-size: 0.9rem; line-height: 1.6; color: var(--ink-muted); }

.table-scroll { overflow-x: auto; margin: 1.25rem 0; }
table { width: 100%; border-collapse: collapse; font-size: 0.98rem; }
th, td { padding: 0.85rem 0.85rem; text-align: left; border-bottom: 1px solid var(--rule); white-space: nowrap; }
th { font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-muted); font-weight: 400; }
/* Counts are compared down the column, not read across the row, so they get
   fixed-width digits — otherwise a 1 is narrower than a 4 and the column
   edges wander. */
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
td.when { color: var(--ink-muted); font-size: 0.9rem; }
tbody tr:hover { background: var(--paper); }

/* These were the least readable thing on the dashboard: 11.5px, light, and
   sitting at 4.5:1 — just past the line and no further. Bigger, heavier, and
   re-toned to clear 6.3:1 at minimum, because a status pill is glanced at, not
   studied. */
.badge { display: inline-block; padding: 0.25rem 0.65rem; border-radius: 999px; font-size: 0.8rem; letter-spacing: 0.03em; }
.badge.waiting { background: var(--paper-deep); color: #4b4d44; }   /* 6.85:1 */
.badge.partial { background: #f6ecd8; color: #6b5117; }             /* 6.36:1 */
.badge.yes { background: var(--sage-mist); color: #414d39; }        /* 6.97:1 */
.badge.no { background: #efe6e4; color: #7d2f24; }                  /* 7.43:1 */

/* The double-count warning. Amber rather than red: it is a "check this",
   not a failure — two guests really can share a name. */
.panel-warn { border-left: 3px solid #b8860b; background: #fdf8ec; }
.warn-list { margin: 0 0 0.75rem; padding-left: 1.1rem; display: grid; gap: 0.5rem; }
.warn-list li { font-size: 0.95rem; color: var(--ink); }
.warn-list strong { color: var(--ink-strong); }

.notes { list-style: none; padding: 0; margin: 0; display: grid; gap: 1rem; }
.notes li { padding: 1rem 1.15rem; background: var(--paper); border-radius: var(--radius); }
.note-text { margin: 0 0 0.4rem; font-family: var(--display); font-size: 1.15rem; color: var(--ink-strong); }
.note-meta { margin: 0; font-size: 0.82rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-muted); }

.admin-foot { text-align: center; margin-top: 2rem; font-size: 0.9rem; }

/* ------------------------------------------------------ admin analytics -- */

/* The RSVP / Analytics switcher. Real links, not buttons: they go to two URLs,
   so each is bookmarkable, opens in a new tab on middle-click, and needs no
   JavaScript. aria-current marks the one you are on for a screen reader. */
.segmented {
  display: inline-flex;
  gap: 0.25rem;
  margin: 0 0 1.75rem;
  padding: 0.25rem;
  background: var(--paper-deep);
  border-radius: 999px;
}
.seg {
  padding: 0.5rem 1.1rem;
  border-radius: 999px;
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  /* Not --ink-muted: that grey is tuned for cream and lands at 4.52:1 on this
     darker track — passing, and no more. The inactive tab is a control someone
     is looking for, so it gets headroom (5.68:1) while still reading quieter
     than the active one. */
  color: #57594f;
  text-decoration: none;
  white-space: nowrap;
}
.seg:hover { color: var(--ink-strong); }
.seg.is-active { background: var(--cream); color: var(--ink-strong); box-shadow: var(--shadow-soft); }

.panel-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  margin-bottom: 1.5rem;
}

/*
  Views per day, as bars. No chart library — it is a row of small integers, and
  a div per day with a percentage height renders the same shape with nothing to
  load and nothing to fail. `align-items: end` is what makes them grow from the
  baseline; without it every bar is top-aligned and the chart reads upside down.
*/
.chart {
  display: flex;
  align-items: end;
  gap: 2px;
  height: 7rem;
  margin: 0.5rem 0 0.4rem;
  padding-top: 0.5rem;
}
.chart .bar {
  flex: 1;
  min-width: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--sage);
}
.chart .bar:hover { background: var(--sage-deep); }
/* A day with no views still gets a mark. An empty column is ambiguous — it
   could be a quiet day or a gap in the data — and a hairline says "zero". */
.chart .bar[style*="height:0%"] {
  min-height: 1px;
  background: var(--rule);
}
.chart-axis {
  display: flex;
  justify-content: space-between;
  margin: 0 0 0.75rem;
  font-size: 0.78rem;
  color: var(--ink-muted);
}

/* Label, proportional bar, count. The bar is decoration on top of a number
   that is already there in text, so it needs no accessible name of its own. */
.breakdown { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.breakdown li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 4.5rem auto;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.92rem;
}
.bd-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bd-track { height: 6px; border-radius: 999px; background: var(--paper-deep); overflow: hidden; }
.bd-fill { display: block; height: 100%; background: var(--sage); }
.bd-num { font-variant-numeric: tabular-nums; color: var(--ink-muted); }

.funnel-line {
  margin: 0 0 0.5rem;
  font-family: var(--display);
  font-size: 1.5rem;
  color: var(--ink-strong);
}
.funnel-line strong { font-weight: 400; }

/* Bot rows stay legible — they are evidence, not noise to be greyed out until
   it cannot be read. Only the ground shifts. */
tbody tr.is-bot { background: var(--paper); }
.back { display: inline-block; margin-top: 2rem; font-size: 0.85rem; color: var(--ink-muted); }
.lede { color: var(--ink-muted); margin-bottom: 1.5rem; }
