/* The public stylesheet.
 *
 * Assembled by tools/build.mjs from the parts below into one hashed file, so
 * the page makes a single request and the pieces stay separately editable.
 * Nothing here is minified: the whole thing gzips to a few kilobytes and being
 * able to read the shipped CSS is worth more than the bytes.
 */
/* ---- parts/fonts.css ---- */
/* Generated by tools/fetch-fonts.mjs -- do not edit by hand.
 *
 * Self-hosted so the site has no third-party origin at all. That removes two
 * DNS lookups and a TLS handshake from first paint, lets the CSP collapse to
 * font-src 'self', and keeps the RSVP page -- where guests type dietary needs
 * and children's names -- free of third-party requests, which the project's
 * POPIA note requires.
 *
 * font-display: swap, not optional: optional risks the hero names rendering in
 * Georgia permanently on a slow first visit, and they are the one thing on this
 * site that has to look right.
 *
 * unicode-range is what makes the split pay: without it a page containing no
 * latin-ext codepoint would still download the latin-ext file.
 */

@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 300;
  font-display: swap;
  src: url('/assets/fonts/cormorant-garamond-300i-latin-ext.OrQuh1r0.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 300;
  font-display: swap;
  src: url('/assets/fonts/cormorant-garamond-300i-latin.uTHwpF-d.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('/assets/fonts/cormorant-garamond-300-latin-ext.z6mjl9hv.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

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

@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/cormorant-garamond-400-latin-ext.z6mjl9hv.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

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

@font-face {
  font-family: 'Jost';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('/assets/fonts/jost-300-latin-ext.MS7yuAzv.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

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

@font-face {
  font-family: 'Jost';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/jost-400-latin-ext.MS7yuAzv.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

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

@font-face {
  font-family: 'Jost';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/jost-500-latin-ext.MS7yuAzv.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

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

/* ---- parts/core.css ---- */
/* ==========================================================================
   Nortjé Wedding 2027 — mockup design system
   Palette and type roles carried forward from the Canva v1 design.
   See output/planning/CONTENT_AUDIT.md §3 for where these values came from.
   ========================================================================== */

:root {
  /* --- colour ------------------------------------------------------------ */
  --sand: #F6F5EE;
  /* primary light surface                        */
  --cream: #F3EBCF;
  /* accent surface / display text on dark        */
  --paper: #EEEDE7;
  /* borders, subtle fills                        */
  --paper-warm: #E8E2D2;
  --ink: #5C4E3D;
  /* body + display text  — 6.5:1 on sand         */
  --ink-deep: #574737;
  /* emphasis                                     */
  /* From the dress-code palette. A pale fill on a pale page has no silhouette
     of its own (mist is 1.42:1 against sand), so the button's navy border is
     load-bearing rather than decorative. */
  --mist: #CDCFDB;
  --amber: #F6B557;
  /* accent, links, focus  — 6.9:1 on sand        */
  --sea: #6B8BA3;
  /* dividers, secondary fills                    */
  --sea-light: #9EC3DE;
  --navy: #304254;
  /* dark surfaces                                */
  --navy-deep: #26333F;

  /* --- type -------------------------------------------------------------- */
  --font-display: 'Cormorant Garamond', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  --font-body: 'Jost', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;

  /* --- rhythm ------------------------------------------------------------ */
  --section-y: clamp(4.5rem, 9vw, 10rem);
  --gutter: clamp(1.25rem, 5vw, 4rem);
  --measure: 68ch;
  --hairline: rgba(92, 78, 61, 0.18);
  --hairline-light: rgba(246, 245, 238, 0.26);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ==========================================================================
   Base
   ========================================================================== */

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

html {
  scroll-behavior: smooth;
  /* clears the fixed header when jumping to an anchor */
  scroll-padding-top: 6rem;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background-color: var(--sand);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: clamp(1.0625rem, 0.35vw + 1rem, 1.25rem);
  line-height: 1.6;
  letter-spacing: 0.006em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* Fine stationery grain. Sits above the background, below all content. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.055'/%3E%3C/svg%3E");
}

body>* {
  position: relative;
  z-index: 1;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

/* Guard: an inline display (display:grid on a panel we toggle) otherwise beats
   the UA stylesheet's [hidden] rule and the element stays visible. */
[hidden] {
  display: none !important;
}

/* Defined here rather than relying on the Tailwind utility, because the header
   and footer are injected by JS and must not depend on the CDN's scan timing. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Honeypot — off-screen, never announced, never tabbable. */
.hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px;
  height: 1px;
  opacity: 0;
}

::selection {
  background: var(--cream);
  color: var(--ink-deep);
}

/* ==========================================================================
   Typography
   ========================================================================== */

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 300;
  line-height: 1.02;
  letter-spacing: -0.004em;
  margin: 0;
  text-wrap: balance;
}

.display-hero {
  font-size: clamp(3.6rem, 12vw, 9.5rem);
}

/* Cursive modifier for the display scale — the hero names. Sits on top of a
   .display-* size, which it scales up: the script still draws smaller than the
   serif per point, and Alex Brush needs a good deal of it: long swashes off a
   comparatively small body. It also undoes the negative tracking,
   which would otherwise pull the joined letterforms into each other. */
.display-script {
  font-weight: 300;
  font-style: normal;
  font-size: clamp(3.6rem, 12vw, 9.5rem);
  letter-spacing: 0.012em;
}

.display-xl {
  font-size: clamp(2.9rem, 8vw, 6.5rem);
}

.display-lg {
  font-size: clamp(2.4rem, 5.5vw, 4.25rem);
}

.display-md {
  font-size: clamp(1.9rem, 3.6vw, 3rem);
}

.display-sm {
  font-size: clamp(1.5rem, 2.4vw, 2.1rem);
}

/* The oversized ampersand from the v1 hero, kept as a design moment — now in
   the same script as the names it sits between. */
.ampersand {
  font-weight: 400;
  font-style: normal;
  font-size: clamp(3.5rem, 10.5vw, 8.5rem);
  line-height: 0.8;
}

/* Tracked-out uppercase sans — the v1 label voice. */
.label {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: clamp(0.66rem, 0.28vw + 0.6rem, 0.8rem);
  text-transform: uppercase;
  letter-spacing: 0.3em;
  line-height: 1.5;
}

.label-lg {
  font-size: clamp(0.75rem, 0.3vw + 0.7rem, 0.95rem);
  letter-spacing: 0.26em;
}

.prose {
  max-width: var(--measure);
  margin-inline: auto;
}

.prose p {
  margin: 0 0 1.35em;
}

.prose p:last-child {
  margin-bottom: 0;
}

.prose strong {
  font-weight: 500;
}

.on-dark .prose strong {
  color: var(--cream);
}

.script {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 300;
}

a {
  color: inherit;
}

.link {
  color: var(--navy);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--navy) 35%, transparent);
  padding-bottom: 1px;
  transition: border-color 0.25s var(--ease), color 0.25s var(--ease);
}

.link:hover {
  border-bottom-color: var(--navy);
}

.on-dark .link {
  color: var(--cream);
  border-bottom-color: rgba(243, 235, 207, 0.4);
}

.on-dark .link:hover {
  border-bottom-color: var(--cream);
}

/* ==========================================================================
   Layout
   ========================================================================== */

.shell {
  width: 100%;
  max-width: 1240px;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.shell-narrow {
  width: 100%;
  max-width: 880px;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  padding-block: var(--section-y);
}

.section-tight {
  padding-block: clamp(3rem, 6vw, 6rem);
}

.on-dark {
  background-color: var(--navy);
  color: var(--sand);
}

.on-dark h1,
.on-dark h2,
.on-dark h3 {
  color: var(--sand);
}

.on-cream {
  background-color: var(--cream);
}

.on-paper {
  background-color: var(--paper);
}

/* ==========================================================================
   Rules & ornament
   ========================================================================== */

.hr-hair {
  border: 0;
  border-top: 1px solid var(--hairline);
  margin: 0;
}

.on-dark .hr-hair {
  border-top-color: var(--hairline-light);
}

/* Hairline with a small centred diamond — the section divider. */
.ornament {
  display: flex;
  align-items: center;
  gap: 1.15rem;
  justify-content: center;
  color: var(--ink);
}

.ornament::before,
.ornament::after {
  content: '';
  height: 1px;
  width: min(7rem, 18vw);
  background: currentColor;
  opacity: 0.28;
}

.ornament span {
  width: 5px;
  height: 5px;
  background: currentColor;
  transform: rotate(45deg);
  opacity: 0.55;
}

.on-dark .ornament {
  color: var(--cream);
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 3rem;
  padding: 0.85rem 2.1rem;
  border-radius: 999px;
  border: 1px solid var(--ink);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: clamp(0.7rem, 0.25vw + 0.65rem, 0.82rem);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.3s var(--ease), color 0.3s var(--ease),
    border-color 0.3s var(--ease), transform 0.3s var(--ease);
}

.btn:hover {
  background: var(--ink);
  color: var(--sand);
}

.btn:active {
  transform: translateY(1px);
}

/* Primary action: solid navy, no contrasting edge, light label. The border is
   kept at the fill colour purely so the box model matches the other .btn
   variants — visually there is no ring.

   A pale fill cannot carry a light label: white on mist is 1.55:1. So "no
   border" and "white text" together settle the fill, and navy is the dress-code
   palette's dark blue. It also means the page buttons and the floating mobile
   bar are now one treatment instead of two. */
.btn-solid {
  background: var(--navy);
  border-color: var(--navy);
  color: var(--sand);
}

.btn-solid:hover {
  background: var(--navy-deep);
  border-color: var(--navy-deep);
  color: var(--sand);
}

.btn-light {
  border-color: rgba(246, 245, 238, 0.65);
  color: var(--sand);
}

.btn-light:hover {
  background: var(--sand);
  color: var(--ink);
  border-color: var(--sand);
}

.btn-sm {
  min-height: 2.5rem;
  padding: 0.6rem 1.4rem;
  letter-spacing: 0.18em;
}

/* Leading glyph inside a button. The uppercase label carries a 0.18em trailing
   letter-space, so the icon needs a hair less gap to look centred. */
.btn-glyph {
  flex: 0 0 auto;
  margin-right: -0.15rem;
}

.btn-whatsapp {
  padding-inline: 1.5rem;
}

/* Universal focus treatment — navy, always visible. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 3px;
  border-radius: 3px;
}

.on-dark :where(a, button, input, select, textarea, summary):focus-visible {
  outline-color: var(--cream);
}

.skip-link {
  position: absolute;
  left: 0;
  top: 0;
  transform: translateY(-120%);
  z-index: 200;
  margin: 0.5rem;
  background: var(--ink);
  color: var(--sand);
  padding: 0.7rem 1.2rem;
  border-radius: 999px;
  text-decoration: none;
  transition: transform 0.2s var(--ease);
}

.skip-link:focus {
  transform: translateY(0);
}

/* ==========================================================================
   Forms
   ========================================================================== */

.field {
  display: grid;
  gap: 0.5rem;
}

.field>label,
.fieldset-legend {
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  opacity: 0.8;
}

.field .hint {
  font-size: 0.86rem;
  opacity: 0.65;
  letter-spacing: 0.01em;
}

.req {
  color: var(--navy);
}

/* Every text-like input, by exclusion rather than by enumeration.
   The mock listed the five types it happened to use, so the first password
   field added -- the one on the admin login -- rendered completely unstyled.
   Written this way a type nobody anticipated is styled by default, and only
   the controls that genuinely need their own treatment are excluded. */
input:not([type="radio"]):not([type="checkbox"]):not([type="file"]):not([type="range"]):not([type="color"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]):not([type="hidden"]),
select,
textarea {
  width: 100%;
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 1rem;
  color: var(--ink);
  background: var(--sand);
  border: 1px solid var(--hairline);
  border-radius: 2px;
  padding: 0.85rem 1rem;
  min-height: 3rem;
  transition: border-color 0.25s var(--ease), background-color 0.25s var(--ease);
  appearance: none;
}

textarea {
  min-height: 7.5rem;
  resize: vertical;
  line-height: 1.55;
}

select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%235C4E3D' stroke-width='1.4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1rem center;
  padding-right: 2.6rem;
}

input:hover,
select:hover,
textarea:hover {
  border-color: rgba(92, 78, 61, 0.4);
}

input::placeholder,
textarea::placeholder {
  color: rgba(92, 78, 61, 0.45);
}

[aria-invalid="true"] {
  border-color: #A33B1F !important;
  background: rgba(163, 59, 31, 0.04);
}

.error-text {
  color: #8E3319;
  font-size: 0.86rem;
  display: flex;
  gap: 0.4rem;
  align-items: flex-start;
}


.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
}

.chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 0.55rem 1.1rem;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  cursor: pointer;
  font-size: 0.88rem;
  min-height: 2.5rem;
  transition: background-color 0.25s var(--ease), border-color 0.25s var(--ease), color 0.25s var(--ease);
}

.chip input {
  position: absolute;
  opacity: 0;
  inset: 0;
  cursor: pointer;
}

.chip:has(input:checked) {
  background: var(--cream);
  border-color: var(--ink);
  color: var(--ink-deep);
}

.chip:has(input:focus-visible) {
  outline: 2px solid var(--navy);
  outline-offset: 2px;
}

/* Display lives here, not in an inline style, so the declined-guest rule below
   can actually win. */
.when-attending {
  display: grid;
  gap: 1.25rem;
}

/* Guest block */
.guest-card {
  border: 1px solid var(--hairline);
  background: rgba(246, 245, 238, 0.6);
  padding: clamp(1.25rem, 2.6vw, 2rem);
  display: grid;
  gap: 1.5rem;
}

.guest-card[data-attending="no"] {
  opacity: 0.72;
  background: rgba(238, 237, 231, 0.5);
}

.guest-card[data-attending="no"] .when-attending {
  display: none;
}

/* Copy-to-clipboard row (bank details) */
.copy-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--hairline);
}

.copy-row:last-of-type {
  border-bottom: 0;
}

.copy-row dt {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  opacity: 0.7;
}

.copy-row dd {
  margin: 0;
  font-family: var(--font-body);
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  gap: 0.85rem;
  text-align: right;
}

/* A number broken across two lines is easy to misread or half-copy by hand,
   so the value never wraps. The label beside it wraps instead. */
.copy-value {
  white-space: nowrap;
}

/* Small enough to sit inline with the value, with the label type of the
   rest of the site. min-height keeps a comfortable tap target on a phone. */
.copy-btn {
  flex: 0 0 auto;
  /* Wide enough for "Copied", so the value beside it does not shift when the
     label changes. */
  min-width: 5.75rem;
  min-height: 2rem;
  padding: 0.3rem 0.85rem;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font-family: var(--font-body);
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  cursor: pointer;
  transition: background-color 0.3s var(--ease), color 0.3s var(--ease),
    border-color 0.3s var(--ease);
}

.copy-btn:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--sand);
}

.on-dark .copy-btn {
  border-color: rgba(246, 245, 238, 0.45);
}

.on-dark .copy-btn:hover {
  background: var(--sand);
  border-color: var(--sand);
  color: var(--ink);
}

/* When the card is narrow, label, value and button do not fit on one line.
   The card is the thing to measure, not the screen: at tablet widths the two
   cards sit side by side and are as narrow as on a phone. So the list is a
   size container, and in a narrow card the label goes on top with the value
   and its button on a full-width line under it. If even that is too tight (an
   IBAN on a 320px phone), the button drops under the value, so the number
   itself never wraps and never runs out of the card. */
dl:has(> .copy-row) {
  container-type: inline-size;
}

@container (max-width: 24rem) {
  .copy-row {
    flex-wrap: wrap;
    gap: 0.45rem 1rem;
  }

  .copy-row dt {
    flex: 1 0 100%;
  }

  .copy-row dd {
    flex: 1 0 100%;
    flex-wrap: wrap;
    justify-content: space-between;
    row-gap: 0.6rem;
    text-align: left;
  }
}


/* ---- parts/public.css ---- */
/* ==========================================================================
   Header / navigation
   ========================================================================== */

.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  transition: background-color 0.4s var(--ease), box-shadow 0.4s var(--ease),
    backdrop-filter 0.4s var(--ease);
}

.site-header.is-scrolled {
  background: rgba(246, 245, 238, 0.92);
  backdrop-filter: saturate(1.1) blur(12px);
  box-shadow: 0 1px 0 var(--hairline);
}

/* Over a dark hero the nav starts light, then inverts once scrolled. */
.site-header[data-over="dark"]:not(.is-scrolled) {
  color: var(--sand);
}

.site-header[data-over="dark"]:not(.is-scrolled) .btn {
  border-color: rgba(246, 245, 238, 0.6);
  color: var(--sand);
}

.site-header[data-over="dark"]:not(.is-scrolled) .btn:hover {
  background: var(--sand);
  color: var(--ink);
}

.nav-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 4.75rem;
}

.monogram {
  font-family: var(--font-display);
  font-size: 1.35rem;
  letter-spacing: 0.16em;
  text-decoration: none;
  white-space: nowrap;
}

.nav-links {
  display: none;
  align-items: center;
  gap: clamp(1.1rem, 2.4vw, 2.4rem);
}

@media (min-width: 62rem) {
  .nav-links {
    display: flex;
  }
}

.nav-link {
  position: relative;
  text-decoration: none;
  font-size: 0.74rem;
  font-weight: 300;
  text-transform: uppercase;
  letter-spacing: 0.26em;
  padding-block: 0.35rem;
}

.nav-link::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s var(--ease);
}

.nav-link:hover::after {
  transform: scaleX(1);
}

.nav-link[aria-current="page"]::after {
  transform: scaleX(1);
  opacity: 0.55;
}

.nav-toggle {
  display: inline-flex;
}

@media (min-width: 62rem) {
  .nav-toggle {
    display: none;
  }
}

/* Mobile drawer */
.nav-drawer {
  position: fixed;
  inset: 0;
  z-index: 150;
  background: var(--sand);
  display: grid;
  place-items: center;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s var(--ease), visibility 0.35s;
}

.nav-drawer[data-open="true"] {
  opacity: 1;
  visibility: visible;
}

.nav-drawer ol {
  list-style: none;
  margin: 0;
  padding: 0;
  text-align: center;
}

.nav-drawer li {
  border-bottom: 1px solid var(--hairline);
}

.nav-drawer li:last-child {
  border-bottom: 0;
}

.nav-drawer a {
  display: block;
  padding: 1.1rem 2rem;
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 7vw, 2.5rem);
  text-decoration: none;
}

/* Persistent mobile RSVP bar — the highest-value mobile pattern here. */
.rsvp-bar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 90;
  padding: 0.7rem var(--gutter) calc(0.7rem + env(safe-area-inset-bottom));
  background: rgba(246, 245, 238, 0.95);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--hairline);
  display: flex;
  transform: translateY(110%);
  transition: transform 0.4s var(--ease);
}

.rsvp-bar.is-visible {
  transform: translateY(0);
}

.rsvp-bar .btn {
  flex: 1;
}

@media (min-width: 62rem) {
  .rsvp-bar {
    display: none;
  }
}

/* ==========================================================================
   Hero
   ========================================================================== */

.hero {
  position: relative;
  min-height: min(100svh, 60rem);
  display: grid;
  place-items: center;
  text-align: center;
  overflow: hidden;
}

.hero-media {
  position: absolute;
  inset: 0;
}

/* Absolutely positioned, not in flow. With a single child the two are
   equivalent, but the home page hero holds a poster AND a video: in flow they
   became two full-height boxes stacked one below the other, so the still sat a
   whole viewport underneath the footage instead of behind it. */
.hero-media img,
.hero-media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Once the video is genuinely playing there is nothing for the poster to do. */
.hero-media.is-playing img {
  opacity: 0;
}

/* The footage is 1280x676; on a wide screen it is upscaled, so keep it slightly
   soft rather than letting the browser sharpen the noise. */
.hero-video {
  background: var(--navy);
}

/* Never put body text straight over a photo — always a scrim. */
.scrim::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
      rgba(48, 66, 84, 0.52) 0%,
      rgba(48, 66, 84, 0.38) 34%,
      rgba(48, 66, 84, 0.62) 100%);
}

.scrim-soft::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(87, 71, 55, 0.28) 0%, rgba(87, 71, 55, 0.46) 100%);
}

/* Directional scrim for a left-aligned block over a bright photograph: darkens
   the text column and lets the image breathe on the other side. */
.scrim-editorial::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg,
      rgba(38, 51, 63, 0.78) 0%,
      rgba(38, 51, 63, 0.58) 34%,
      rgba(38, 51, 63, 0.22) 62%,
      rgba(38, 51, 63, 0.10) 100%),
    linear-gradient(180deg, rgba(38, 51, 63, 0.22), rgba(38, 51, 63, 0.30));
}

/* Scrim tuned for the hero footage. The wave clip runs to near-white foam,
   which the standard .scrim (built for a darker photograph) could not hold down
   - the date line and button measured 3.2:1 against it. Weighted towards the
   centre, where the type sits, so the corners of the frame stay open. */
.scrim-video::after {
  content: '';
  position: absolute;
  inset: 0;
  /* 
  Note: Keep original warm theme of video
  background:
    radial-gradient(0% 0% at 0% 0%,
      rgba(38, 51, 63, 0.76) 0%,
      rgba(38, 51, 63, 0.68) 55%,
      rgba(38, 51, 63, 0.46) 100%); */
}

.scrim-strong::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(38, 51, 63, 0.62) 0%, rgba(38, 51, 63, 0.74) 100%);
}

/* The dress-code band sits on the same golden-water still as the home
   countdown, but the section is shorter, so `cover` lands on a brighter,
   foamier crop and .scrim-strong could not hold it: the small-caps line under
   the heading measured 3.2:1. Weighted towards the top, where that crop is
   brightest and where all the small type sits. */
.scrim-dress::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
      rgba(38, 51, 63, 0.84) 0%,
      rgba(38, 51, 63, 0.80) 45%,
      rgba(38, 51, 63, 0.74) 100%);
}

.hero-inner {
  position: relative;
  z-index: 2;
  padding-block: 6.5rem 4.5rem;
}

/* ==========================================================================
   Watercolour seal — the v1 illustration, given a soft painted edge
   ========================================================================== */

.seal {
  width: clamp(7rem, 16vw, 11rem);
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 58%, transparent 78%);
  mask-image: radial-gradient(circle at 50% 50%, #000 58%, transparent 78%);
  mix-blend-mode: multiply;
}

.seal img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ==========================================================================
   Dress code — sits on the same navy band as the order of events, so it
   follows that section's shape: centred heading block, one contained column,
   real classes rather than inline styles. Everything here assumes a dark
   surface, which is why the hairlines are cream-based rather than ink.
   ========================================================================== */

.palette {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(1.25rem, 3.2vw, 2.5rem);
  margin: 0;
  padding: 0;
}

.palette li {
  text-align: center;
}

/* Six chips do not fit one row on a phone, and plain wrapping leaves the last
   one alone on a second line. Two rows of three keep it balanced. */
@media (max-width: 29.99rem) {
  .palette {
    display: grid;
    grid-template-columns: repeat(3, auto);
    justify-content: center;
    column-gap: 2.5rem;
    row-gap: 1.5rem;
  }
}

.palette-chip {
  display: block;
  width: clamp(3rem, 6vw, 4rem);
  aspect-ratio: 1;
  border-radius: 50%;
  margin-inline: auto;
  /* An inset ring rather than a border. It is what gives a chip its edge when
     the fill is close to what is behind it: the navy chip on the navy-scrimmed
     water measures 1.11:1 on fill alone, so the ring is doing all the work of
     making it a shape. Strong enough to clear 3:1 (WCAG 1.4.11) on the darkest
     chip, which is more than the lighter ones need. */
  box-shadow: inset 0 0 0 1px rgba(243, 235, 207, 0.65);
}

.palette-name {
  display: block;
  margin-top: 0.65rem;
  font-size: 0.55rem;
  opacity: 0.75;
}

.dress-notes {
  display: grid;
  gap: clamp(1.75rem, 3.5vw, 2.5rem);
  margin: clamp(2.5rem, 5vw, 3.5rem) 0 0;
  padding-top: clamp(2rem, 4vw, 2.75rem);
  border-top: 1px solid rgba(243, 235, 207, 0.18);
  text-align: center;
}

.dress-notes dt {
  margin-bottom: 0.55rem;
  opacity: 0.75;
}

.dress-notes dd {
  margin: 0;
  font-size: 0.98rem;
}

@media (min-width: 48rem) {
  .dress-notes {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ==========================================================================
   Kids — the one section allowed to be playful. It sits between two deep navy
   bands, so it is pitched as a bright pocket between them: a sunlit wash, the
   heading in Rust from the dress-code palette, and a row of toys drawn in the
   same five colours so the fun still belongs to the wedding.

   Everything that moves is slow and small — this is a wedding site, not a
   toy shop — and the global prefers-reduced-motion rule stops all of it.
   ========================================================================== */

.kids-scene {
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(58% 80% at 14% 18%, rgba(246, 181, 87, 0.20), transparent 70%),
    radial-gradient(52% 72% at 88% 26%, rgba(236, 115, 130, 0.16), transparent 70%),
    radial-gradient(60% 70% at 60% 96%, rgba(205, 207, 219, 0.30), transparent 70%),
    var(--sand);
}

/* Rust is 4.28:1 on sand — under AA for body text, comfortably over the 3:1
   that large text needs. Only ever used at display size. */
.kids-title {
  color: #C1521D;
}

.kids-title span {
  display: inline-block;
  animation: kid-letter 2.2s var(--ease) infinite;
}

.kids-title span:nth-child(3) {
  animation-delay: 0.08s;
}

.kids-title span:nth-child(4) {
  animation-delay: 0.16s;
}

.kids-title span:nth-child(5) {
  animation-delay: 0.24s;
}

@keyframes kid-letter {

  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(-0.09em);
  }
}

.kids-toys {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  /* sized so all five hold a single line down to ~360px rather than
     dropping one toy onto a row of its own */
  gap: clamp(0.9rem, 4vw, 3rem);
  margin: clamp(2.5rem, 5vw, 3.75rem) 0 0;
  padding: 0;
}

.kids-toys li {
  /* the bob lives on the li so hover can scale the svg independently */
  animation: kid-bob 2.6s ease-in-out infinite;
  animation-delay: var(--d, 0s);
}

.kids-toys svg {
  display: block;
  width: clamp(3.25rem, 8vw, 5.75rem);
  height: auto;
  transform: rotate(var(--tilt, 0deg));
  transition: transform 0.25s var(--ease);
}

.kids-toys li:hover svg,
.kids-toys li:focus-within svg {
  transform: rotate(var(--tilt, 0deg)) scale(1.12);
}

@keyframes kid-bob {

  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(-0.55rem);
  }
}

.kids-aside {
  margin: clamp(1.75rem, 3.5vw, 2.5rem) 0 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.05rem, 1.6vw, 1.35rem);
  color: var(--ink);
  opacity: 0.85;
}

/* ==========================================================================
   RSVP — two questions deep. One radio pair decides everything; the detail
   fields only exist for a yes. Nothing here is conditional on a guest list.
   ========================================================================== */

/* Sits above the question on purpose: whether one reply covers the household
   changes whether you fill this in at all, so it has to be read first. */
.rsvp-note {
  margin: 0 0 clamp(2rem, 4vw, 2.75rem);
  padding: 1.1rem 1.35rem;
  background: var(--paper);
  border-left: 2px solid var(--navy);
  font-size: 0.95rem;
  line-height: 1.6;
}

fieldset.bare {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.answers {
  display: grid;
  gap: 1rem;
}

@media (min-width: 40rem) {
  .answers {
    grid-template-columns: 1fr 1fr;
  }
}

/* A whole-card target rather than a 16px dot: it is the only decision on the
   page, and it has to be comfortable on a phone. */
/* Scoped to .answers on purpose. `.answer` is ALSO the class the FAQ uses for
   an accordion's answer body, and this rule's border, background, flex and
   min-height were leaking onto it -- every FAQ answer rendered inside a box.
   The radio card only ever appears inside .answers, so the scope costs nothing
   and the two stop colliding. */
.answers .answer {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  min-height: 4.75rem;
  padding: 1.1rem 1.35rem;
  border: 1px solid var(--hairline);
  background: var(--sand);
  cursor: pointer;
  transition: border-color 0.25s var(--ease), background-color 0.25s var(--ease);
}

.answers .answer:hover {
  border-color: var(--navy);
}

.answers .answer input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.answer-mark {
  flex: 0 0 auto;
  width: 1.15rem;
  height: 1.15rem;
  border-radius: 50%;
  border: 1px solid var(--ink);
  display: grid;
  place-items: center;
}

.answer-mark::after {
  content: '';
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: var(--navy);
  transform: scale(0);
  transition: transform 0.2s var(--ease);
}

.answer-text {
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 1.6vw, 1.3rem);
  line-height: 1.25;
  color: var(--ink-deep);
}

/* A thumb at the end of each card. Decorative — the words already say which
   is which, and a colour-blind or icon-blind reader loses nothing. */
.answer-icon {
  flex: 0 0 auto;
  margin-left: auto;
  width: 1.85rem;
  height: 1.85rem;
  color: var(--ink);
  opacity: 0.4;
  transition: opacity 0.25s var(--ease), color 0.25s var(--ease),
    transform 0.25s var(--ease);
}

.answers .answer:hover .answer-icon {
  opacity: 0.7;
}

.answers .answer:has(input:checked) .answer-icon {
  color: var(--navy);
  opacity: 1;
  transform: scale(1.08);
}

.answers .answer:has(input:checked) {
  border-color: var(--navy);
  background: var(--mist);
}

.answers .answer:has(input:checked) .answer-mark {
  border-color: var(--navy);
}

.answers .answer:has(input:checked) .answer-mark::after {
  transform: scale(1);
}

/* The card is the control, so the ring belongs on the card, not the hidden
   input — otherwise keyboard users get no visible focus at all. */
.answers .answer:has(input:focus-visible) {
  outline: 2px solid var(--navy);
  outline-offset: 3px;
}

.rsvp-panel {
  display: grid;
  gap: clamp(1.5rem, 3vw, 2rem);
  margin-top: clamp(2.25rem, 4vw, 3rem);
  padding-top: clamp(2.25rem, 4vw, 3rem);
  border-top: 1px solid var(--hairline);
}

/* Follows a panel that already drew the divider, so no second rule. */
.rsvp-block {
  display: grid;
  gap: clamp(1.5rem, 3vw, 2rem);
  margin-top: clamp(1.5rem, 3vw, 2rem);
}

.field-pair {
  display: grid;
  gap: clamp(1.5rem, 3vw, 2rem);
}

@media (min-width: 40rem) {
  .field-pair {
    grid-template-columns: 1fr 1fr;
  }
}

.rule {
  height: 1px;
  background: var(--hairline);
  border: 0;
}

.party-row {
  display: grid;
  gap: 1rem;
  align-items: end;
  padding: 1.25rem;
  border: 1px solid var(--hairline);
  background: var(--paper);
}

@media (min-width: 48rem) {
  .party-row {
    grid-template-columns: 1fr 1fr 1.15fr auto;
  }
}

/* The row states what it is, because the choice was made by which button was
   pressed rather than by a control inside the row. */
.party-type {
  grid-column: 1 / -1;
  margin: 0 0 0.35rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem;
}

.party-type span {
  font-family: var(--font-body);
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--navy);
}

.party-type small {
  font-size: 0.8rem;
  opacity: 0.6;
}

.party-adders {
  margin: 1.25rem 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* dashboard: marks a party member as a child in the guest list cell */
.tag {
  display: inline-block;
  padding: 0.05rem 0.4rem;
  border: 1px solid var(--hairline);
  border-radius: 3px;
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  vertical-align: 0.1em;
  opacity: 0.8;
}

.party-row .field {
  margin: 0;
}

.party-remove {
  justify-self: start;
  background: none;
  border: 0;
  padding: 0.5rem 0;
  font-family: var(--font-body);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.7;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.party-remove:hover {
  opacity: 1;
}

/* ==========================================================================
   Quick-facts strip
   ========================================================================== */

.facts {
  display: grid;
  grid-template-columns: 1fr;
  border-top: 1px solid var(--hairline);
  border-left: 1px solid var(--hairline);
}

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

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

.fact {
  border-right: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  padding: clamp(1.5rem, 3vw, 2.5rem) clamp(1.25rem, 2.5vw, 2rem);
  text-align: center;
}

.fact dt {
  margin-bottom: 0.85rem;
  opacity: 0.7;
}

.fact dd {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 2.2vw, 1.85rem);
  line-height: 1.15;
}

.fact small {
  display: block;
  margin-top: 0.5rem;
  font-size: 0.82rem;
  opacity: 0.72;
  letter-spacing: 0.02em;
}

/* ==========================================================================
   Countdown
   ========================================================================== */

.countdown {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  max-width: 44rem;
  margin-inline: auto;
}

.countdown>div {
  padding: 0.75rem 0.35rem;
  border-left: 1px solid var(--hairline-light);
}

.countdown>div:first-child {
  border-left: 0;
}

.countdown .num {
  font-family: var(--font-display);
  font-size: clamp(2.1rem, 6.5vw, 4rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  display: block;
}

.countdown .unit {
  display: block;
  margin-top: 0.7rem;
  opacity: 0.75;
}

/* ==========================================================================
   Order of events — semantic timeline
   ========================================================================== */

.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
}

.timeline::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.75rem;
  bottom: 0.75rem;
  width: 1px;
  background: var(--hairline-light);
}

@media (min-width: 48rem) {
  .timeline::before {
    left: 8.5rem;
  }
}

.timeline>li {
  position: relative;
  padding: 0 0 clamp(2rem, 4vw, 3rem) 2rem;
  display: grid;
  gap: 0.3rem 2rem;
}

@media (min-width: 48rem) {
  .timeline>li {
    grid-template-columns: 8.5rem 1fr;
    padding-left: 0;
    align-items: center;
  }

  .timeline>li::before {
    top: 50%;
    margin-top: -3.5px;
  }

  .timeline-body {
    padding-left: 0.75rem;
  }

  .timeline>li>* {
    grid-column: 2;
  }

  .timeline>li>time {
    grid-column: 1;
    text-align: right;
    padding-right: 2rem;
  }
}

.timeline>li:last-child {
  padding-bottom: 0;
}

.timeline>li::before {
  content: '';
  position: absolute;
  left: -3px;
  top: 0.85rem;
  width: 7px;
  height: 7px;
  background: var(--cream);
  transform: rotate(45deg);
}

@media (min-width: 48rem) {
  .timeline>li::before {
    left: calc(8.5rem - 3px);
  }
}

/* Each entry carries the line-art icon the original uses for it. The icon and
   the text sit side by side inside the content column, so the time column and
   the marker on the rule are untouched. */
.timeline-body {
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2vw, 1.6rem);
}

.timeline-icon {
  flex: 0 0 auto;
  width: clamp(3.5rem, 5.8vw, 5rem);
  height: auto;
  opacity: 0.92;
}

.timeline time {
  font-family: var(--font-body);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  opacity: 0.72;
  font-variant-numeric: tabular-nums;
}

.timeline h3 {
  font-size: clamp(1.45rem, 2.6vw, 2rem);
}

.timeline p {
  margin: 0.35rem 0 0;
  font-size: 0.95rem;
  opacity: 0.78;
  max-width: 46ch;
}

/* Light-background variant — the cream marker disappears on sand. */
.timeline-light::before {
  background: var(--hairline);
}

.timeline-light>li::before {
  background: var(--ink);
  opacity: 0.55;
}

.timeline-light time {
  opacity: 0.65;
}

/* Highlighted on the day itself, driven by site.js */
.timeline>li[data-now="true"] h3 {
  color: var(--cream);
}

/* on the navy programme band, so the "now" marker has to be light */
.timeline>li[data-now="true"]::before {
  background: var(--amber);
  box-shadow: 0 0 0 5px rgba(246, 181, 87, 0.25);
}

/* ==========================================================================
   Cards
   ========================================================================== */

.card {
  background: var(--sand);
  border: 1px solid var(--hairline);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.card-body {
  padding: clamp(1.35rem, 2.6vw, 2rem);
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  flex: 1;
}

.on-dark .card {
  background: rgba(246, 245, 238, 0.05);
  border-color: var(--hairline-light);
}

/* ==========================================================================
   Sticky sub-navigation (details page)
   ========================================================================== */

.subnav {
  position: sticky;
  top: 4.75rem;
  z-index: 60;
  background: rgba(246, 245, 238, 0.94);
  backdrop-filter: blur(10px);
  border-block: 1px solid var(--hairline);
}

.subnav ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: clamp(1rem, 3vw, 2.5rem);
  overflow-x: auto;
  scrollbar-width: none;
  justify-content: flex-start;
  -webkit-overflow-scrolling: touch;
}

.subnav ol::-webkit-scrollbar {
  display: none;
}

@media (min-width: 62rem) {
  .subnav ol {
    justify-content: center;
  }
}

.subnav a {
  display: block;
  padding: 1.1rem 0;
  text-decoration: none;
  white-space: nowrap;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  opacity: 0.6;
  border-bottom: 1px solid transparent;
  transition: opacity 0.25s var(--ease), border-color 0.25s var(--ease);
}

.subnav a:hover {
  opacity: 1;
}

.subnav a.is-active {
  opacity: 1;
  border-bottom-color: var(--navy);
}

/* ==========================================================================
   Accordion (FAQ)
   ========================================================================== */

.accordion {
  border-top: 1px solid var(--hairline);
}

.accordion details {
  border-bottom: 1px solid var(--hairline);
}

.accordion summary {
  cursor: pointer;
  list-style: none;
  padding: clamp(1.1rem, 2.2vw, 1.6rem) 3rem clamp(1.1rem, 2.2vw, 1.6rem) 0;
  position: relative;
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 1.9vw, 1.5rem);
  line-height: 1.3;
  transition: color 0.25s var(--ease);
}

.accordion summary::-webkit-details-marker {
  display: none;
}

.accordion summary:hover {
  color: var(--navy);
}

.accordion summary::after {
  content: '';
  position: absolute;
  right: 0.5rem;
  top: 50%;
  width: 11px;
  height: 11px;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translateY(-70%) rotate(45deg);
  transition: transform 0.3s var(--ease);
  opacity: 0.6;
}

.accordion details[open] summary::after {
  transform: translateY(-30%) rotate(-135deg);
}

.accordion .answer {
  padding: 0 0 clamp(1.25rem, 2.4vw, 1.9rem);
  max-width: 62ch;
}

.accordion .answer p {
  margin: 0 0 0.9em;
}

.accordion .answer p:last-child {
  margin-bottom: 0;
}

/* ==========================================================================
   TBC flag — marks content the couple still owes us (plan §9)
   ========================================================================== */

.tbc {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  border: 1px dashed rgba(246, 181, 87, 0.75);
  color: var(--navy);
  background: rgba(246, 181, 87, 0.12);
  border-radius: 3px;
  padding: 0.15rem 0.55rem;
  font-family: var(--font-body);
  font-size: 0.62rem;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  vertical-align: middle;
  white-space: nowrap;
}

.on-dark .tbc {
  border-color: rgba(243, 235, 207, 0.6);
  color: var(--cream);
  background: rgba(243, 235, 207, 0.1);
}

.tbc-block {
  border: 1px dashed rgba(246, 181, 87, 0.65);
  background: rgba(246, 181, 87, 0.09);
  padding: clamp(1rem, 2.2vw, 1.5rem);
  border-radius: 3px;
}

.on-dark .tbc-block {
  border-color: rgba(243, 235, 207, 0.4);
  background: rgba(243, 235, 207, 0.07);
}

/* ==========================================================================
   Footer
   ========================================================================== */

.site-footer {
  background: var(--navy-deep);
  color: var(--sand);
}

.site-footer a {
  color: var(--cream);
}

/* ==========================================================================
   Reveal on scroll
   ========================================================================== */

/* Gated on html.js. The mock hid every .reveal element unconditionally and
   relied on an IntersectionObserver to bring it back, which meant a visitor
   whose JavaScript failed got a blank cream rectangle on every page. The `js`
   class is set by an inline script in <head>, before this stylesheet parses,
   so there is no flash for anyone else. */
html.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}

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

.reveal-d1 {
  transition-delay: 0.08s;
}

.reveal-d2 {
  transition-delay: 0.16s;
}

.reveal-d3 {
  transition-delay: 0.24s;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .reveal {
    opacity: 1;
    transform: none;
  }
}

/* ==========================================================================
   Print — guests do print the details page
   ========================================================================== */

@media print {

  body::before,
  .site-header,
  .rsvp-bar,
  .subnav,
  .mock-banner,
  .nav-drawer,
  .lightbox {
    display: none !important;
  }

  body {
    background: #fff;
    color: #000;
    font-size: 11pt;
  }

  .on-dark,
  .site-footer {
    background: #fff !important;
    color: #000 !important;
  }

  .on-dark h1,
  .on-dark h2,
  .on-dark h3 {
    color: #000 !important;
  }

  .section {
    padding-block: 1.5rem;
  }

  a[href^="http"]::after {
    content: ' (' attr(href) ')';
    font-size: 9pt;
  }
}
/* ---- parts/pages.css ---- */
/* ==========================================================================
   Page-level placement
   ==========================================================================

   These rules are the mock's inline style="" attributes, moved out of the
   markup unchanged. The values are copied verbatim, so nothing moves by a
   pixel; what changes is that the Content-Security-Policy no longer has to
   allow inline styles to render the site, and the same spacing decision is now
   made in one place instead of being retyped per page.

   Ordered roughly by where they appear: shared helpers first, then hero,
   sections, and the per-page pieces.
   ========================================================================== */

/* --- shared helpers ------------------------------------------------------- */

.centred {
  text-align: center;
}

.block {
  display: block;
}

.no-margin {
  margin: 0;
}

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

.surface-cream {
  background: var(--cream);
}

.muted-note {
  font-size: 1rem;
  opacity: 0.8;
}

/* A section that stacks a background image behind its own content. */
.band {
  position: relative;
  overflow: hidden;
}

.band-inner {
  position: relative;
  z-index: 2;
}

/* --- hero (home) ---------------------------------------------------------- */

.hero-light {
  color: var(--sand);
}

.hero-kicker {
  margin: 0 0 clamp(1.5rem, 4vw, 3rem);
}

.hero-title {
  margin: 0;
  line-height: 1.04;
}

.hero-amp {
  margin: 0.02em 0;
  opacity: 0.9;
}

.hero-ornament {
  margin: clamp(2rem, 5vw, 3.5rem) 0;
  color: var(--sand);
}

.hero-cta {
  margin: clamp(2.5rem, 6vw, 4rem) 0 0;
}

/* --- section furniture ---------------------------------------------------- */

.section-kicker {
  margin: 0 0 clamp(2rem, 4vw, 3rem);
  opacity: 0.7;
}

.prose-lead {
  font-size: clamp(1.1rem, 0.4vw + 1rem, 1.3rem);
}

.signoff,
.rsvp-cta {
  margin: clamp(2.5rem, 5vw, 3.5rem) 0 0;
}

.signoff-label {
  display: block;
  opacity: 0.65;
  margin-bottom: 1rem;
}

/* --- countdown ------------------------------------------------------------ */

.countdown-kicker {
  margin: 0 0 clamp(2rem, 4vw, 3rem);
  color: var(--cream);
}

/* --- RSVP band (home) ----------------------------------------------------- */

.rsvp-kindly {
  margin: 0;
  font-size: clamp(1.5rem, 3vw, 2rem);
  letter-spacing: 0;
  text-transform: none;
  opacity: 0.75;
}

/* The home page band and the RSVP page hero set this slightly differently. */
.rsvp-word-band {
  margin: 0.15em 0 0;
}

.rsvp-word {
  margin: 0.05em 0 0;
}

.rsvp-ornament {
  margin: clamp(2rem, 4vw, 3rem) 0;
}

/* --- editorial teaser ----------------------------------------------------- */

.teaser {
  min-height: min(72svh, 44rem);
}

.teaser-inner {
  color: var(--sand);
  padding-block: clamp(4rem, 10vw, 7rem);
  text-align: left;
  width: 100%;
}

.teaser-col {
  max-width: 34rem;
}

.teaser-kicker {
  margin: 0 0 0.5rem;
  opacity: 0.85;
}

.teaser-body {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  font-size: 1.05rem;
  max-width: 36ch;
}

.teaser-cta {
  margin: clamp(2rem, 4vw, 3rem) 0 0;
}

.pos-forest {
  object-position: 65% 30%;
}

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

.stack-gap {
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

.stack-gap-lg {
  margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
}

.map-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1rem;
  align-items: center;
  margin: 1.5rem 0 0;
}

.map-figure {
  margin: 0;
  border-radius: 3px;
  overflow: hidden;
}

.map-figure img {
  display: block;
  width: 100%;
  height: auto;
}

.notes-list {
  list-style: none;
  margin: clamp(1.5rem, 3vw, 2rem) 0 0;
  padding: 0;
  display: grid;
  gap: 0.75rem;
}

.notes-list li {
  padding-left: 1.4rem;
  position: relative;
}

.notes-list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 0.5rem;
  height: 1px;
  background: currentColor;
  opacity: 0.55;
}

/* --- gifts page ----------------------------------------------------------- */

.gift-shell {
  max-width: 64rem;
  margin-inline: auto;
}

.oyster {
  display: block;
  width: clamp(7rem, 16vw, 11rem);
  height: auto;
  margin: 0 auto clamp(1.5rem, 3vw, 2.5rem);
}

.bank-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.55rem;
  font-size: 0.95rem;
}

.bank-list dt {
  opacity: 0.65;
}

.bank-list dd {
  margin: 0 0 0.75rem;
  font-variant-numeric: tabular-nums;
}

/* --- FAQ page ------------------------------------------------------------- */

/* The FAQ has no hero, so its heading needs the space the header bar would
   otherwise sit over. */
.page-head {
  padding-top: clamp(7rem, 14vw, 10rem);
}

.faq-group {
  margin: 0 0 clamp(2.5rem, 5vw, 4rem);
}

.faq-group > h2 {
  margin: 0 0 1.25rem;
}

/* --- RSVP page ------------------------------------------------------------ */

.rsvp-intro {
  margin: 0 0 clamp(2rem, 4vw, 3rem);
}

.form-actions {
  margin: clamp(2rem, 4vw, 3rem) 0 0;
}

.receipt {
  text-align: center;
  padding: clamp(2rem, 5vw, 3.5rem) 0;
}

.receipt:focus {
  outline: none;
}

.receipt h1 {
  margin: 0 0 1rem;
}

.receipt p {
  margin: 0 auto 1.5rem;
  max-width: 40ch;
}

/* --- privacy / 404 -------------------------------------------------------- */

.plain-page {
  padding-top: clamp(7rem, 14vw, 10rem);
}

.plain-page .prose h2 {
  margin-top: clamp(2rem, 4vw, 2.75rem);
}

/* --- details page, continued ---------------------------------------------- */

.page-hero {
  min-height: min(62svh, 34rem);
}

.pos-wave {
  object-position: 50% 60%;
}

.hero-sub {
  margin: 0 0 1rem;
  opacity: 0.7;
}

.hero-light .hero-sub {
  opacity: 0.85;
}

/* On a dark band the kicker is cream at full strength rather than a faded ink.
   Scoped to .on-dark rather than given its own class: an earlier version used
   `.on-cream`, which already exists in parts/core.css as a SURFACE utility
   (background-color: var(--cream)). Every kicker so marked therefore rendered
   as a solid cream bar across the navy band. */
.on-dark .hero-sub {
  color: var(--cream);
  opacity: 1;
}

.section-head {
  margin: 0 0 clamp(2.5rem, 5vw, 4rem);
}

.section-head h2 {
  margin: 0;
}

.bordered {
  border: 1px solid var(--hairline);
}

.map-caption {
  margin: 0.85rem 0 0;
  opacity: 0.55;
  font-size: 0.6rem;
}

.note-caption {
  margin: 1.5rem 0 0;
  opacity: 0.6;
  font-size: 0.62rem;
}

.venue-address {
  font-style: normal;
  font-size: clamp(1.1rem, 0.5vw + 1rem, 1.3rem);
  line-height: 1.7;
  margin: 0;
}

.spaced {
  margin: 1.5rem 0 0;
}

.spaced-lg {
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
}

.card-kicker {
  margin: 0;
  opacity: 0.7;
}

.card-text {
  margin: 0;
  font-size: 0.95rem;
  opacity: 0.85;
}

/* auto pushes the button to the bottom so cards in a row line up */
.card-foot {
  margin: auto 0 0;
}

.card-foot-row {
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.tbc-heading {
  margin: 0 0 1rem;
}

.bullet-list {
  margin: 0;
  padding-left: 1.2rem;
  display: grid;
  gap: 0.6rem;
  font-size: 0.95rem;
}

.closing-line {
  text-align: center;
  margin: clamp(2.5rem, 5vw, 3.5rem) auto 0;
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 2vw, 1.6rem);
}

.measure-wide {
  max-width: 52rem;
  margin-inline: auto;
}

/* --- dress code ------------------------------------------------------------ */

/* Outfit sketches beside the wording. On a phone they sit between the heading
   and the text, narrowed so a tall portrait does not fill the screen. From
   64rem the sketches take a column on the left that spans the heading and the
   body. The text column then stays wide enough for the three notes. */
.dress-layout {
  display: grid;
  grid-template-areas: 'head' 'figure' 'body';
}

.dress-head {
  grid-area: head;
}

.dress-body {
  grid-area: body;
}

.dress-figure {
  grid-area: figure;
  justify-self: center;
  width: min(100%, 20rem);
  margin: 0 0 clamp(2.5rem, 5vw, 3.5rem);
}

/* The sketches are drawn on white, so they read as a card on the navy band
   rather than being cut out against it. */
.dress-figure img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.55);
}

@media (min-width: 64rem) {
  .dress-layout {
    grid-template-columns: minmax(16rem, 23rem) minmax(0, 1fr);
    grid-template-areas: 'figure head' 'figure body';
    grid-template-rows: auto 1fr;
    column-gap: clamp(3rem, 5vw, 5rem);
    align-items: start;
  }

  .dress-figure {
    width: 100%;
    margin: 0;
    align-self: center;
  }
}

.dress-kicker {
  margin: 1.25rem 0 0;
  opacity: 0.8;
  font-size: 0.62rem;
}

.dress-lede {
  margin: 0 auto clamp(2.5rem, 5vw, 3.5rem);
  max-width: 48ch;
  text-align: center;
}

/* The six colours of the day. Named classes rather than inline background
   declarations, so the palette is listed once and the CSP needs no exception
   for style attributes. */
.chip-navy {
  background: #304254;
}

.chip-mist {
  background: #cdcfdb;
}

.chip-coral {
  background: #ec7382;
}

.chip-amber {
  background: #f6b557;
}

.chip-rust {
  background: #c1521d;
}

.chip-sage {
  background: #b0b283;
}

/* --- kids ------------------------------------------------------------------ */

.kids-title {
  margin: 0 0 clamp(1.5rem, 3vw, 2.25rem);
}

/* The toys bob out of phase so the row reads as playful rather than mechanical.
   --tilt and --d were inline style attributes in the mock. */
.toy-1 {
  --tilt: -7deg;
  --d: 0s;
}

.toy-2 {
  --tilt: 5deg;
  --d: 0.22s;
}

.toy-3 {
  --tilt: -4deg;
  --d: 0.44s;
}

.toy-4 {
  --tilt: 8deg;
  --d: 0.66s;
}

.toy-5 {
  --tilt: -5deg;
  --d: 0.88s;
}

/* --- closing CTA ----------------------------------------------------------- */

.cta-heading {
  margin: 0 0 1.25rem;
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  justify-content: center;
}

/* --- FAQ, gifts, wedding party, RSVP ------------------------------------- */

.faq-lede {
  margin: clamp(1.5rem, 3vw, 2rem) auto 0;
  max-width: 44ch;
  font-size: 1rem;
  opacity: 0.85;
}

/* The heading section above already carries the page's top padding. */
.flush-top {
  padding-top: 0;
}

.faq-heading {
  margin: 0 0 1.25rem;
}

.gifts-hero {
  min-height: min(56svh, 30rem);
}

.gifts-sub {
  font-size: clamp(1.4rem, 3.5vw, 2.4rem);
  margin-top: 0.35em;
  opacity: 0.92;
}

.sub-note {
  margin: 1.25rem 0 0;
  font-size: 0.95rem;
  opacity: 0.8;
}

.card-title {
  margin: 0 0 0.5rem;
}

.card-note {
  margin: 1.25rem 0 0;
  font-size: 0.9rem;
  opacity: 0.8;
}

.tbc-note {
  margin: 1rem 0 0;
  opacity: 0.6;
  font-size: 0.62rem;
}

.gratitude {
  font-family: var(--font-display);
  font-size: clamp(1.3rem, 2.4vw, 1.9rem);
  line-height: 1.4;
}

.party-hero {
  min-height: min(50svh, 24rem);
}

.pos-party {
  object-position: 50% 35%;
}

/* A button that is present so the layout is right but has nothing to link to
   yet. aria-disabled tells assistive tech; this makes it look the part. */
.is-disabled {
  opacity: 0.55;
  cursor: not-allowed;
  pointer-events: none;
}

.bullet-list.tight {
  gap: 0.45rem;
}

/* --- RSVP page ------------------------------------------------------------ */

.rsvp-hero {
  min-height: min(52svh, 26rem);
}

.pos-rsvp {
  object-position: 50% 30%;
}

.rsvp-kindly-hero {
  margin: 0;
  font-size: clamp(1.4rem, 3vw, 2rem);
  opacity: 0.9;
}

.rsvp-deadline {
  margin: 1.5rem 0 0;
  opacity: 0.9;
}

.rsvp-legend {
  margin: 0 0 1.5rem;
}

.party-intro {
  margin: 0.45rem 0 1.5rem;
  font-size: 0.95rem;
  opacity: 0.8;
}

.party-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
}

.privacy-line {
  margin: 1rem 0 0;
  font-size: 0.85rem;
}

/* ---- parts/nojs.css ---- */
/* ==========================================================================
   The no-JavaScript floor, and the chrome classes that replaced inline styles
   ==========================================================================

   The mock injected its header, footer and sticky bar from site.js and styled
   several parts of them with inline style="" attributes. Both are gone: the
   chrome is now rendered into the HTML at build time, and the styles that were
   inline live here, which is also what lets the Content-Security-Policy stop
   needing 'unsafe-inline' for style attributes on these elements.

   Everything under html:not(.js) exists because a page must still be complete
   when JavaScript fails - not merely readable, but navigable. That was not true
   of the mock: with scripting off you got no header background, no navigation
   below 62rem, and nothing but em-dashes where the countdown should be.
   ========================================================================== */

/* --- header --------------------------------------------------------------- */

/* The mock wrapped these in style="display:flex;align-items:center;gap:.75rem" */
.nav-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.nav-links {
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav-drawer-close {
  position: absolute;
  top: 1.15rem;
  right: var(--gutter);
}

/* The header's background only ever arrived with the .is-scrolled class that
   the scroll listener adds. Without JavaScript the bar stayed transparent for
   the whole page and content scrolled underneath it. */
html:not(.js) .site-header {
  background: rgba(246, 245, 238, 0.94);
  box-shadow: 0 1px 0 var(--hairline);
}

html:not(.js) .site-header[data-over='dark'] {
  background: rgba(38, 51, 63, 0.82);
}

/* Below 62rem the drawer is the ONLY navigation there is, and it opens from a
   button that does nothing without a script. Show the links inline instead and
   let them scroll sideways; four items fit on any phone. */
html:not(.js) .nav-toggle {
  display: none;
}

html:not(.js) .nav-links {
  display: flex;
  gap: 1.25rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

html:not(.js) .nav-links::-webkit-scrollbar {
  display: none;
}

html:not(.js) .nav-drawer {
  display: none;
}

/* The sticky bar is translated off-screen until the scroll listener reveals it.
   The header already carries an RSVP button, so hiding it costs nothing. */
html:not(.js) .rsvp-bar {
  display: none;
}

/* A copy-to-clipboard button that cannot copy is worse than no button. The
   co-ordinates and the bank details are visible as text either way. */
html:not(.js) [data-copy] {
  display: none;
}

/* --- countdown ------------------------------------------------------------ */

/* Two states rendered into the markup, one of them always hidden. Without this
   the countdown was four em-dashes and a set of labels. */
html.js .cd-static {
  display: none;
}

html:not(.js) .cd-live {
  display: none;
}

.cd-static {
  grid-column: 1 / -1;
  margin: 0;
}

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

.footer-inner {
  text-align: center;
}

.footer-names {
  margin: 0;
}

.footer-meta {
  margin: 1.25rem 0 0;
  opacity: 0.8;
}

.footer-ornament {
  margin: 2.5rem 0;
}

.footer-note {
  margin: 0 0 1.5rem;
  font-size: 0.95rem;
  opacity: 0.85;
  max-width: 34rem;
  margin-inline: auto;
}

.footer-cta {
  margin: 0 0 2rem;
}

.footer-links {
  list-style: none;
  margin: 0 0 2rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.75rem;
  justify-content: center;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
}

.footer-deadline {
  margin: 0;
  opacity: 0.5;
  font-size: 0.62rem;
}

/* --- grid utilities ------------------------------------------------------- */

/* These five rules are the entire replacement for the Tailwind Play CDN. The
   mock loaded a runtime JIT compiler from a third party on every page in order
   to use `grid gap-6 md:grid-cols-2` five times. */
.cols {
  display: grid;
  gap: clamp(1.25rem, 3vw, 1.5rem);
}

.cols-wide {
  gap: clamp(1.5rem, 4vw, 3rem);
}

@media (min-width: 48rem) {
  .cols-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .cols-3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .cols-2-lg {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.cols-middle {
  align-items: center;
}

/* --- RSVP without JavaScript ---------------------------------------------- */

/* The yes/no branch is CSS, not script. The stylesheet already depends on
   :has() for the radio cards, so this costs nothing new and means the form
   works with scripting off - which the mock's hidden-attribute toggling did
   not. Browsers without :has() fall through to showing everything, which is
   degraded rather than broken. */
@supports selector(:has(*)) {
  #who,
  #details,
  #decline,
  #submit-row {
    display: none;
  }

  #rsvp:has(#att-yes:checked) #who,
  #rsvp:has(#att-no:checked) #who,
  #rsvp:has(#att-yes:checked) #submit-row,
  #rsvp:has(#att-no:checked) #submit-row,
  #rsvp:has(#att-yes:checked) #details,
  #rsvp:has(#att-no:checked) #decline {
    display: grid;
  }

  /* A round trip that failed validation must show everything it validated,
     whatever the radio state ended up as. */
  #rsvp[data-submitted] #who,
  #rsvp[data-submitted] #submit-row {
    display: grid;
  }
}

/* Without JavaScript the type of each pre-rendered guest row is chosen from a
   select rather than decided by which button added the row. */
.party-type-select {
  grid-column: 1 / -1;
}

html.js .party-type-select {
  display: none;
}

html:not(.js) .party-adders,
html:not(.js) .party-remove {
  display: none;
}

html.js #party-overflow {
  display: none;
}

/* --- error summary -------------------------------------------------------- */

/* Server-rendered on a 422. autofocus on a tabindex="-1" container moves focus
   here with no script at all. */
.error-summary {
  border: 1px solid var(--navy);
  border-left-width: 4px;
  background: rgba(48, 66, 84, 0.06);
  padding: clamp(1rem, 2.5vw, 1.4rem);
  border-radius: 3px;
  margin: 0 0 2rem;
}

.error-summary p {
  margin: 0 0 0.6rem;
  font-weight: 500;
}

.error-summary ul {
  margin: 0;
  padding-left: 1.1rem;
}

.error-summary a {
  color: var(--navy);
}

.error-summary:focus {
  outline: 2px solid var(--navy);
  outline-offset: 3px;
}

/* --- misc ----------------------------------------------------------------- */

/* The honeypot. The mock styled this inline although .hp already existed here,
   unused. */
.hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
}

.hint {
  font-size: 0.9rem;
  opacity: 0.8;
  margin: 0.75rem 0 0;
}

