/* ==========================================================================
   Seven Hills Park — styles
   A pocket park on Capitol Hill.
   --------------------------------------------------------------------------
   Sections:
     1. Tokens (palette, type, space, layout)
     2. Reset & base
     3. Layout primitives (.container, header, buttons, chips, section heads)
     4. Hero (pop-up book diorama)
     5. About
     6. Calendar
     7. Dispatches
     8. Visit
     9. Footer
    10. Motion, responsive, reduced-motion
   --------------------------------------------------------------------------
   Conventions:
     - BEM-ish class names (block__element, block--modifier).
     - One source of truth for horizontal rhythm: `.container`. Sections only
       own their background + vertical padding; `.container` owns max-width
       and horizontal padding.
     - Spacing, color, motion all read from the tokens in :root.
   ========================================================================== */

/* ---------- 1. Tokens ---------- */
:root {
  /* Palette */
  --ink:          #141210;
  --ink-soft:     #2A251F;
  --moss:         #1F3B2C;
  --leaf:         #6B8E5A;
  --leaf-deep:    #4A6B3D;
  --cream:        #F4EFE6;
  --cream-deep:   #EDE5D4;
  --paper:        #FBF7EE;
  --stone:        #E8E2D5;
  --blossom:      #F4B8C6;
  --blossom-soft: #F7C7D2;
  --blossom-deep: #EFA0B4;
  --hot:          #E8527A;
  --ochre:        #D2A255;

  /* Falling "petal" color follows the seasonal accent automatically.
     Petals are drawn as small circles, so the shape works for any
     season — blossoms in spring, leaves in fall, snow in winter. */
  --petal-color:  var(--blossom);

  /* Type */
  --font-display: "DM Serif Display", "Cormorant Garamond", Georgia, serif;
  --font-body:    "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono:    "DM Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* Scale */
  --fs-xs:   clamp(12px, 0.78vw, 13px);
  --fs-sm:   clamp(13px, 0.88vw, 14px);
  --fs-body: clamp(16px, 1.05vw, 18px);
  --fs-lede: clamp(18px, 1.35vw, 22px);
  --fs-h3:   clamp(22px, 2vw, 30px);
  --fs-h2:   clamp(38px, 5.5vw, 84px);
  --fs-h1:   clamp(56px, 10vw, 168px);

  /* Space */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;
  --space-9: 96px;
  --space-10: 128px;

  /* Radius */
  --r-sm: 10px;
  --r-md: 18px;
  --r-lg: 28px;
  --r-xl: 40px;

  /* Lines */
  --hair: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);

  /* Layout */
  --container-max:    1400px;
  --container-narrow: 1200px;
  --container-wide:   1600px;
  --gutter:           clamp(20px, 4vw, 48px);

  /* Live measurements written by JS (see js/app.js). Fallbacks are tuned for
     the rendered design so the scene doesn't shift on font-swap. */
  --hero-marquee-h: 64px;

  /* Motion */
  --ease-out-soft: cubic-bezier(.22,.61,.36,1);
  --ease-pop:      cubic-bezier(.34,1.56,.64,1);
  --ease-settle:   cubic-bezier(.2,.8,.2,1);
}

/* ---------- Seasonal themes ----------
   `data-season` is set on <html> by a tiny inline script in the <head>
   (meteorological seasons, N. hemisphere). Spring is the site's default,
   so no override block needed for spring. Each season re-tints the
   accent tokens (--blossom family + --hot); every accent and gradient
   downstream (body atmosphere, italic emphasis, focus ring, marquee
   dots, link hovers, petals, map pin) picks up the new palette through
   the cascade — no per-component overrides required.

   Heads up: the hero cherry-blossom SVGs have hardcoded pink fills and
   remain "peak spring" in every season. We treat the cherry trees as
   the site's visual signature; to shift them too would need the SVG
   fills rewritten to use CSS custom properties. */

[data-season="summer"] {
  /* Sun-on-the-lawn palette: bright sunflower yellow (no orange) for
     accents + a leafy green re-tint of the cherry canopies below.
     --blossom-deep and --hot stay firmly in the yellow family — no
     amber/mustard drift at the darker end, so gradient stops read as
     "summer sun" rather than "late-afternoon orange". */
  --blossom:      #FFD93A;
  --blossom-soft: #FFE999;
  --blossom-deep: #F5B700;
  --hot:          #FFC928;
}

/* Re-tint the cherry tree canopies from spring pink to summer green.
   The front-row trees (.scene__tree) and the back-row silhouettes
   (.scene__back-trees) share a small palette of hardcoded pink fills
   in the SVG — we remap each one via attribute selectors so a seasonal
   swap stays a single-file change. Warm-shadow flecks (#E8527A) retain
   their inline opacity, we only swap the fill color. */
[data-season="summer"] .scene__tree circle[fill="#F4B8C6"],
[data-season="summer"] .scene__back-trees circle[fill="#F4B8C6"] {
  fill: #A9C988; /* main canopy leaf green */
}
[data-season="summer"] .scene__tree circle[fill="#EFA0B4"],
[data-season="summer"] .scene__back-trees circle[fill="#EFA0B4"] {
  fill: #7FA963; /* mid-tone shade */
}
[data-season="summer"] .scene__tree circle[fill="#F7C7D2"],
[data-season="summer"] .scene__back-trees circle[fill="#F7C7D2"] {
  fill: #C6DDA5; /* sunlit highlight */
}
[data-season="summer"] .scene__tree circle[fill="#E8527A"],
[data-season="summer"] .scene__back-trees circle[fill="#E8527A"] {
  fill: #4E7038; /* warm (now cool) shadow fleck */
}

/* Summer sky: same 5-stop gradient shape as the default (soft wash at
   the top, warmer glow near the grass line) but shifted into clear
   blues — high-noon blue up top, hazy horizon near the trees, a gentle
   warm kiss at the bottom where the sun catches the lawn. */
[data-season="summer"] .hero {
  background: linear-gradient(180deg,
    #CFE7F4  0%,
    #B9DCEF 30%,
    #A9D3EA 55%,
    #CDE1E9 82%,
    #E8E3CE 100%);
}

/* Dispatch card media wash. The base rule is
     linear-gradient(135deg, var(--blossom), var(--blossom-deep))
   which reads yellow in summer — not the vibe we want once the hero
   sky has gone blue. Pull the two sky mid-stops into the 135° wash
   so empty cards look like "a patch of summer sky" alongside the
   hero. Only overrides the gradient; the card frame, tag, title, and
   body all keep their base styling. */
[data-season="summer"] .dispatch__media {
  background: linear-gradient(135deg, #CFE7F4 0%, #A9D3EA 100%);
}

/* The italic emphasis in the hero title uses var(--hot), the same
   bright marigold that drives every other yellow accent (eyebrow
   pill dot, map pin, etc). At display scale against the summer blue
   sky, the bright yellow on its own is hard to read — but darkening
   the color means "Hills" stops matching the rest of the palette.
   Keep the exact same yellow and add a soft, tight dark shadow
   behind the letterforms for edge definition. Stacked shadows: an
   inner crisp outline for legibility + a looser dropped shadow for
   a subtle pop-off-the-sky depth cue. */
[data-season="summer"] .hero__word em {
  text-shadow:
    0 0 1px rgba(40, 32, 20, 0.35),
    0 1px 0 rgba(40, 32, 20, 0.22),
    0 4px 12px rgba(40, 32, 20, 0.12);
}

/* Brand mark (header + footer): seasonally re-tint the pink logo so it
   stays on-theme. Same technique as the tree canopies — match the
   hardcoded SVG fills by attribute selector and swap them. Spring is
   the site default, so it keeps the original pink fills. The black
   center dot and the small stamen stay as-is in every season. */
[data-season="summer"] .brand__mark circle[fill="#F4B8C6"] { fill: #C6DDA5; }
[data-season="summer"] .brand__mark circle[fill="#E8527A"] { fill: #4E7038; }

[data-season="fall"] .brand__mark circle[fill="#F4B8C6"]   { fill: #F4BB85; }
[data-season="fall"] .brand__mark circle[fill="#E8527A"]   { fill: #B1422A; }

[data-season="winter"] .brand__mark circle[fill="#F4B8C6"] { fill: #E2E9F0; }
[data-season="winter"] .brand__mark circle[fill="#E8527A"] { fill: #88A1BC; }

[data-season="fall"] {
  --blossom:      #E89A5A;
  --blossom-soft: #F4BB85;
  --blossom-deep: #BD6B30;
  --hot:          #B1422A;
}

/* Re-tint the cherry tree canopies for peak fall color — pink blossoms
   become a mix of amber, rust, and mahogany. Same attribute-selector
   technique as the summer/winter swaps. */
[data-season="fall"] .scene__tree circle[fill="#F4B8C6"],
[data-season="fall"] .scene__back-trees circle[fill="#F4B8C6"] {
  fill: #E89A5A; /* main canopy amber */
}
[data-season="fall"] .scene__tree circle[fill="#EFA0B4"],
[data-season="fall"] .scene__back-trees circle[fill="#EFA0B4"] {
  fill: #BD6B30; /* burnt-orange shade */
}
[data-season="fall"] .scene__tree circle[fill="#F7C7D2"],
[data-season="fall"] .scene__back-trees circle[fill="#F7C7D2"] {
  fill: #F4C596; /* pale peach highlight */
}
[data-season="fall"] .scene__tree circle[fill="#E8527A"],
[data-season="fall"] .scene__back-trees circle[fill="#E8527A"] {
  fill: #8C3A1E; /* deep mahogany fleck */
}

/* Fall sky: crisp autumn afternoon — cool blue up top fading into a
   warm golden-amber horizon where the low sun rakes across the lawn.
   Same 5-stop shape as summer/winter for visual consistency. */
[data-season="fall"] .hero {
  background: linear-gradient(180deg,
    #C9DBE5  0%,
    #D2DBE2 30%,
    #DCCFB3 55%,
    #EAC08E 82%,
    #DBA45E 100%);
}

/* Dispatch card media wash — warm amber-to-rust gradient so empty
   cards read as "a patch of autumn light" and harmonize with the
   sunset horizon of the hero. */
[data-season="fall"] .dispatch__media {
  background: linear-gradient(135deg, #F4BB85 0%, #BD6B30 100%);
}

[data-season="winter"] {
  /* Snowy-afternoon palette: cool slate blues for accents + a cranberry
     "holly berry" --hot so focus states, the eyebrow dot, and italic
     emphasis still pop against a mostly-white diorama. */
  --blossom:      #CDD9E4;
  --blossom-soft: #E2E9F0;
  --blossom-deep: #88A1BC;
  --hot:          #B84455;
}

/* Re-tint the cherry tree canopies into snow-on-the-branches. Same
   attribute-selector technique as the summer swap — map each hardcoded
   pink fill to a cool snow palette. Palette is tuned to stay clearly
   distinct from the overcast sky stops (the earlier mid-tone was the
   same value as the sky and the trees disappeared). We keep a touch
   of cranberry (#C54A5C) in the warm-shadow flecks so the canopies
   read as "snow dusted with winter berries". */
[data-season="winter"] .scene__tree circle[fill="#F4B8C6"],
[data-season="winter"] .scene__back-trees circle[fill="#F4B8C6"] {
  fill: #FAFCFE; /* main snow mass — nearly pure white for sky contrast */
}
[data-season="winter"] .scene__tree circle[fill="#EFA0B4"],
[data-season="winter"] .scene__back-trees circle[fill="#EFA0B4"] {
  fill: #7E94AE; /* deep cool shade, clearly darker than any sky stop */
}
[data-season="winter"] .scene__tree circle[fill="#F7C7D2"],
[data-season="winter"] .scene__back-trees circle[fill="#F7C7D2"] {
  fill: #FFFFFF; /* pure snow highlight */
}
[data-season="winter"] .scene__tree circle[fill="#E8527A"],
[data-season="winter"] .scene__back-trees circle[fill="#E8527A"] {
  fill: #C54A5C; /* winter-berry accent fleck */
}

/* Winter sky: same 5-stop shape, pulled into a cool overcast afternoon.
   Pale ice at the top, denser slate in the middle where snow clouds sit
   heaviest, and a faint warm glow at the horizon where the low winter
   sun hits the snow. */
[data-season="winter"] .hero {
  background: linear-gradient(180deg,
    #DEE7EF  0%,
    #CBD5E0 30%,
    #B7C3D3 55%,
    #C5CEDA 82%,
    #E0DED4 100%);
}

/* Dispatch card media wash — icy gradient that echoes the winter sky
   so empty cards look like "a patch of overcast afternoon" instead of
   the default yellow. Deeper stop is a cool slate, lighter stop a
   near-white snow. */
[data-season="winter"] .dispatch__media {
  background: linear-gradient(135deg, #DEE7EF 0%, #8FA4B9 100%);
}

/* ---------- Night mode ----------
   `data-time="night"` is set on <html> by the same inline script that
   sets data-season. Each season-night combo re-paints the hero sky
   into a matching after-dark gradient (moonlit pink for spring, deep
   indigo for summer, purple-amber for fall, cold navy for winter) and
   flips the hero copy to a light palette for readability on the dark
   background. Day is the default, so no override block needed. */

/* Flip hero copy to light tones whenever night is active, regardless
   of season. Title keeps its italic --hot accent color; only the
   surrounding text flips. Layered shadow gives the text edge
   definition against deep skies. */
[data-time="night"] .hero__title,
[data-time="night"] .hero__lede {
  color: #F4EFE6;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35), 0 6px 18px rgba(0, 0, 0, 0.28);
}
/* Italic emphasis — in day-summer we add the layered shadow to read
   on blue sky; at night that shadow logic still holds (even more so),
   so duplicate it here for every night season. */
[data-time="night"] .hero__word em {
  text-shadow:
    0 0 1px rgba(0, 0, 0, 0.55),
    0 1px 0 rgba(0, 0, 0, 0.4),
    0 4px 14px rgba(0, 0, 0, 0.32);
}
/* Eyebrow pill sits on a darker background now — lighten its border,
   fill, and text so it stays visible against deep skies. */
[data-time="night"] .eyebrow {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(255, 255, 255, 0.25);
  color: #F4EFE6;
}

/* Site header and hero marquee both use translucent cream backgrounds
   that are lovely over the daytime sky gradients but pick up and
   tint the dark night sky, so they read as "darkened" bands. Force
   them to near-opaque cream at night so they keep their warm paper
   tone and feel like distinct pieces of chrome above/below the
   diorama, not extensions of the night sky. */
[data-time="night"] .site-header {
  background: color-mix(in srgb, var(--cream) 96%, transparent);
}
[data-time="night"] .hero__marquee {
  background: var(--cream);
}

/* Per-season night sky gradients. Same 5-stop shape used everywhere
   else so the diorama's visual grammar stays consistent. */
[data-time="night"][data-season="spring"] .hero {
  background: linear-gradient(180deg,
    #1B1428  0%,
    #28203A 30%,
    #382A48 55%,
    #4C3050 82%,
    #5E3A5A 100%);
}
[data-time="night"][data-season="summer"] .hero {
  background: linear-gradient(180deg,
    #0D1A2E  0%,
    #152440 30%,
    #1F3250 55%,
    #2E3A54 82%,
    #433E54 100%);
}
[data-time="night"][data-season="fall"] .hero {
  background: linear-gradient(180deg,
    #1B1628  0%,
    #261A30 30%,
    #30203A 55%,
    #3A2A34 82%,
    #4A342C 100%);
}
[data-time="night"][data-season="winter"] .hero {
  background: linear-gradient(180deg,
    #0A1220  0%,
    #121D30 30%,
    #1A253A 55%,
    #232E42 82%,
    #2E3442 100%);
}

/* Dispatch card night washes — echo each sky so empty cards feel like
   a patch of the night sky. */
[data-time="night"][data-season="spring"] .dispatch__media {
  background: linear-gradient(135deg, #28203A 0%, #5E3A5A 100%);
}
[data-time="night"][data-season="summer"] .dispatch__media {
  background: linear-gradient(135deg, #152440 0%, #3A3B54 100%);
}
[data-time="night"][data-season="fall"] .dispatch__media {
  background: linear-gradient(135deg, #261A30 0%, #4A342C 100%);
}
[data-time="night"][data-season="winter"] .dispatch__media {
  background: linear-gradient(135deg, #121D30 0%, #2E3442 100%);
}

/* ---------- 2. Reset & base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

html {
  color-scheme: light;
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.55;
  color: var(--ink);
  background: var(--cream);
  font-feature-settings: "ss01", "cv11";
  overflow-x: clip;

  /* Reduce layout shift when web fonts swap in: align the fallback font's
     x-height to the web font's so the rendered box size stays close to the
     same. Progressive — unsupported browsers just ignore it. */
  font-size-adjust: 0.52;

  /* subtle paper texture */
  background-image:
    radial-gradient(1200px 600px at 20% -10%, color-mix(in srgb, var(--blossom) 18%, transparent), transparent 60%),
    radial-gradient(900px 500px at 110% 10%, color-mix(in srgb, var(--leaf) 12%, transparent), transparent 60%);
}

/* Grain overlay */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 999;
  opacity: .05;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml;utf8,<svg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.08  0 0 0 0 0.07  0 0 0 0 0.06  0 0 0 1 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}

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

a {
  color: inherit;
  text-decoration: none;
  text-underline-offset: 3px;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: -0.01em;
  margin: 0;
  /* Balance long display-font titles so line breaks are predictable and
     don't hop around as the web font swaps in. */
  text-wrap: balance;
  font-size-adjust: 0.48;
}
h1 em, h2 em, h3 em { font-style: italic; color: var(--hot); }

p { margin: 0; }

.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--ink); color: var(--cream);
  padding: 10px 14px; border-radius: 0 0 10px 0; z-index: 1000;
}
.skip-link:focus { left: 0; }

/* Focus */
:focus-visible {
  outline: 2px solid var(--hot);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ---------- 3. Layout primitives ---------- */

/* Single source of truth for page content widths. Sections own backgrounds
   and vertical rhythm; .container owns max-width and horizontal gutter. */
.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container--narrow { --container-max: var(--container-narrow); }
.container--wide   { --container-max: var(--container-wide); }

.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--cream) 75%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-bottom: var(--hair);
  transition: transform .5s var(--ease-out-soft), background .3s ease;
}
.site-header.is-hidden { transform: translateY(-110%); }

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-block: 16px;
}

.brand { display: inline-flex; align-items: center; gap: 12px; }
.brand__mark { display: inline-block; line-height: 0; }
.brand__wordmark { display: flex; flex-direction: column; line-height: 1; }
.brand__name {
  font-family: var(--font-display);
  font-size: clamp(18px, 1.4vw, 22px);
  letter-spacing: -0.01em;
  white-space: nowrap;
}
/* Long + short versions of the wordmark. Default: show the full name and
   hide the abbreviation. At very narrow widths we swap them (see the
   .brand__name-short rule inside the 480px media query below). */
.brand__name-full  { display: inline; }
.brand__name-short { display: none; }

.brand__sub {
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: color-mix(in srgb, var(--ink) 60%, transparent);
  margin-top: 3px;
}

.nav { display: flex; gap: clamp(14px, 2vw, 28px); }
.nav a {
  font-size: 14px;
  font-weight: 500;
  position: relative;
  padding: 6px 2px;
}
.nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: var(--hot);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .4s var(--ease-out-soft);
}
.nav a:hover::after { transform: scaleX(1); }

/* Hamburger toggle. Hidden on desktop (display:none below); the
   tablet media query flips it to inline-flex and hides the inline
   nav. Three <span> bars morph into an X when aria-expanded="true",
   so the same button reads as both "open" and "close". */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  margin: 0;
  background: transparent;
  border: 0;
  border-radius: 8px;
  color: var(--ink);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.nav-toggle:focus-visible {
  outline: 2px solid var(--hot);
  outline-offset: 2px;
}
.nav-toggle__bars {
  position: relative;
  display: block;
  width: 22px;
  height: 14px;
}
.nav-toggle__bars span {
  position: absolute;
  left: 0; right: 0;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  transition:
    top .25s var(--ease-out-soft),
    transform .25s var(--ease-out-soft),
    opacity .15s ease;
}
.nav-toggle__bars span:nth-child(1) { top: 0; }
.nav-toggle__bars span:nth-child(2) { top: 6px; }
.nav-toggle__bars span:nth-child(3) { top: 12px; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(1) {
  top: 6px;
  transform: rotate(45deg);
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(2) {
  opacity: 0;
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(3) {
  top: 6px;
  transform: rotate(-45deg);
}

/* Buttons */
.btn {
  --bg: var(--ink);
  --fg: var(--cream);
  --bd: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 22px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 15px;
  letter-spacing: 0.01em;
  background: var(--bg);
  color: var(--fg);
  border: 1.5px solid var(--bd);
  cursor: pointer;
  transition: transform .3s var(--ease-out-soft), background .3s ease, color .3s ease, box-shadow .3s ease;
  will-change: transform;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 14px 30px -16px rgba(20,18,16,.35); }
.btn--primary { --bg: var(--ink); --fg: var(--cream); }
.btn--ghost {
  --bg: var(--cream);
  --fg: var(--ink);
  --bd: var(--ink);
}
.btn--ghost:hover {
  --bg: color-mix(in srgb, var(--cream) 90%, var(--ink));
  --bd: var(--ink);
}
.btn--sm { padding: 10px 16px; font-size: 13px; }

/* Section head */
.section-label {
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: color-mix(in srgb, var(--ink) 55%, transparent);
  margin: 0 0 18px;
}
.section-title {
  font-size: var(--fs-h2);
  max-width: 18ch;
}
.section-lede {
  font-size: var(--fs-lede);
  max-width: 48ch;
  color: color-mix(in srgb, var(--ink) 75%, transparent);
  margin-top: 20px;
}
.section-head {
  margin-bottom: clamp(40px, 5vw, 72px);
}

/* Chips */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: var(--paper);
  border: var(--hair);
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
}
.chip__dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--c, var(--hot));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c, var(--hot)) 18%, transparent);
}

/* ---------- 4. Hero ---------- */
.hero {
  position: relative;
  /* Height sits comfortably below the fold on most viewports. Capped
     at 100svh so the banner ALWAYS fits above the fold on short
     screens, and floored with a min-height so it doesn't collapse on
     very tall desktops where 85svh would leave the scene cramped. */
  height: clamp(620px, 85svh, 920px);
  max-height: 100svh;
  padding-top: 80px; /* clears the fixed header */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  isolation: isolate;
  /* Softer, more painterly gradient (no sun, so sky is quieter) */
  background: linear-gradient(180deg,
    #FBE9DF  0%,
    #F7D9CE 30%,
    #F4D4C5 55%,
    #E8D4B8 82%,
    #D9CCA8 100%);
}

/* Copy overlays the scene — title & lede blend with the diorama.
   Sits in normal flow at the top of the hero; scene is behind. */
.hero__copy {
  position: relative;
  z-index: 6;
  /* No top padding — eyebrow sits flush against .hero padding-top (the
     header clearance). Bottom padding kept modest; the scene is
     isolated and has its own fixed height anchored to the bottom,
     so copy doesn't need a big bottom pad to clear the illustration. */
  padding-block: 0 10%;
  /* max-width + horizontal padding come from `.container` on the element. */
}
@media (min-width: 1080px) {
  .hero__copy { padding-bottom: 6%; }
}

/* Mid-size zone: shrink the front trees so canopies stop reaching up
   past the buttons. */
@media (max-width: 1080px) {
  .scene__tree { height: clamp(260px, 52%, 440px); }
}

/* Scene is its own isolated container anchored to the bottom of the
   hero, with a viewport-based height that's independent of anything
   that happens in .hero__copy (padding, title size, lede length,
   button wrap, etc). The sky above the scene is filled by the .hero
   background gradient, so "more sky" or "more illustration" is a
   single decision made here — not a side-effect of copy tweaks. */
.hero__scene {
  position: absolute;
  left: 0; right: 0;
  /* Pinned above the marquee */
  bottom: var(--hero-marquee-h, 64px);
  /* Stable height, viewport-based. Taller = scene top rises closer to
     the buttons, shrinking the empty sky band between the CTAs and
     the illustration. Clamped so the scene can't dominate a tall
     desktop or disappear on a short laptop. */
  height: clamp(320px, 45svh, 560px);
  pointer-events: none;
  z-index: 1;
  overflow: hidden;
}

/* ----- Sky elements (no sun — just abstract clouds drifting) ----- */
.scene__sky { position: absolute; inset: 0; overflow: hidden; }

.cloud {
  position: absolute;
  opacity: .78;
  mix-blend-mode: screen;
}
.cloud--1 {
  top: 10%; left: -18%;
  width: clamp(140px, 16vw, 240px);
  animation: drift 105s linear infinite;
  opacity: .88;
}
.cloud--2 {
  top: 30%; left: -18%;
  width: clamp(100px, 11vw, 170px);
  animation: drift 145s linear infinite;
  animation-delay: -48s;
  opacity: .72;
}
.cloud--3 {
  top: 18%; left: -18%;
  width: clamp(160px, 19vw, 270px);
  animation: drift 120s linear infinite;
  animation-delay: -78s;
  opacity: .82;
}
.cloud--4 {
  top: 40%; left: -18%;
  width: clamp(80px, 9vw, 140px);
  animation: drift 90s linear infinite;
  animation-delay: -30s;
  opacity: .65;
}
.cloud--5 {
  top: 5%; left: -18%;
  width: clamp(120px, 13vw, 200px);
  animation: drift 160s linear infinite;
  animation-delay: -110s;
  opacity: .78;
}
.cloud--6 {
  top: 44%; left: -22%;
  width: clamp(150px, 17vw, 240px);
  animation: drift 135s linear infinite;
  animation-delay: -60s;
  opacity: .7;
}
.cloud--7 {
  top: 50%; left: -12%;
  width: clamp(110px, 12vw, 180px);
  animation: drift 150s linear infinite;
  animation-delay: -95s;
  opacity: .65;
}
@keyframes drift {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(100vw + 40vw)); }
}

/* ----- Buildings (edge-anchored, always visible) ----- */
.scene__church {
  position: absolute;
  bottom: 0;
  right: clamp(-10px, 0.5vw, 20px);
  width:  clamp(190px, 28vw, 380px);
  height: clamp(300px, 68%, 560px);
  z-index: 2;
  overflow: visible;
}
.scene__apartments {
  position: absolute;
  bottom: 0;
  left: clamp(40px, 7vw, 120px);
  width:  clamp(220px, 36vw, 480px);
  height: clamp(240px, 56%, 460px);
  z-index: 2;
}

/* ----- Back row of trees ----- */
.scene__back-trees {
  position: absolute;
  left: 0; right: 0;
  bottom: 15%;
  width: 100%;
  height: clamp(160px, 36%, 340px);
  z-index: 3;
  overflow: visible;
}

/* ----- Grass planes ----- */
.scene__grass {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  width: 100%;
  height: clamp(100px, 22%, 200px);
  z-index: 4;
}
.scene__grass-detail {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  width: 100%;
  height: clamp(60px, 14%, 120px);
  z-index: 5;
  overflow: visible;
}

/* Back-layer prop container: sits above grass but BEHIND the front trees
   (z-index: 5 < front trees' z-index: 6). */
.scene__back-boulder {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
}

/* ----- Front trees (edge-anchored, always visible) ----- */
.scene__tree {
  position: absolute;
  bottom: 0;
  width:  clamp(170px, 24vw, 340px);
  height: clamp(300px, 62%, 540px);
  z-index: 6;
  overflow: visible;
}
.scene__tree--left  { left: -2%;  }
.scene__tree--right { right: -2%; }

/* Pop-in reveals.
   Split into two groups on purpose:
   - Layers that also receive GSAP parallax (church/apartments/back-trees)
     ONLY fade in. GSAP writes an inline `transform` every frame for the
     scrub, so any CSS transform-transition on those elements would just
     get overwritten and look broken.
   - Layers that are static (front trees) still get the full rise-up
     pop-in because nothing is fighting their transform. */
.scene__church,
.scene__apartments,
.scene__back-trees {
  opacity: 0;
  transition: opacity .75s ease;
}
.scene__tree {
  opacity: 0;
  transform: translateY(42px) scale(.96);
  transition:
    opacity .75s ease,
    transform 1.2s var(--ease-pop);
}
.scene__tree--left  { transform-origin: bottom left; }
.scene__tree--right { transform-origin: bottom right; }
.scene__back-trees  { transform-origin: bottom center; }

.is-ready .scene__church,
.is-ready .scene__apartments,
.is-ready .scene__back-trees {
  opacity: 1;
}
.is-ready .scene__tree {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.is-ready .scene__church      { transition-delay: .1s;  }
.is-ready .scene__apartments  { transition-delay: .18s; }
.is-ready .scene__back-trees  { transition-delay: .24s; }
.is-ready .scene__tree--left  { transition-delay: .36s; }
.is-ready .scene__tree--right { transition-delay: .44s; }

/* Tree sway (inner group, doesn't conflict with the rise transition) */
.tree-inner,
.tree {
  transform-box: fill-box;
  transform-origin: 50% 100%;
}
.is-ready .tree-inner {
  animation: sway 10s ease-in-out infinite alternate;
}
.is-ready .scene__tree--left  .tree-inner { animation-delay: .9s;  transform-origin: 0% 100%; }
.is-ready .scene__tree--right .tree-inner { animation-delay: 1.6s; transform-origin: 100% 100%; animation-duration: 11s; }
.is-ready .scene__back-trees .tree-inner {
  animation: sway 9s ease-in-out infinite alternate;
  animation-delay: var(--sway-delay, 0s);
}
/* Scale the back-trees up slightly from their base (anchored bottom-
   center via transform-origin above). `scale` is an independent
   property, so it composes with the sway `transform: rotate()`
   keyframes without clobbering them. */
.scene__back-trees .tree-inner {
  scale: 1.18;
}
@keyframes sway {
  0%   { transform: rotate(-0.9deg); }
  100% { transform: rotate( 0.9deg); }
}

/* ----- Props (boulders, bench, dog) ----- */
.scene__props {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 34%;
  z-index: 7;
  pointer-events: none;
}

/* Scene cast: swappable human/animal/stuff layer. Boulders live outside
   these wrappers so they stay put across every scene. At boot, JS picks
   one cast eligible for the current season + time and flips .is-active
   on it. `inset: 0` makes each cast layer span the full scene so the
   absolute-positioned props inside keep their existing left/right/bottom
   percentages (measured against the whole scene, not the wrapper). */
.scene__cast {
  position: absolute;
  inset: 0;
  pointer-events: none;
  display: none;
}
.scene__cast.is-active {
  display: block;
}
.prop {
  position: absolute;
  bottom: 0;
  /* Sit above the front trees (z-index: 6) so boulders and the bench
     overlap trunk bases instead of getting cut through by them. Reads
     as: back-trees → buildings → front trees → foreground props in the
     grass. Matters most at tablet/mobile widths where the tree
     container is wide enough to intersect the prop column. */
  z-index: 7;
  opacity: 0;
  transform: translateY(22px) scale(.9);
  transform-origin: bottom center;
  transition:
    opacity .6s ease,
    transform 1s var(--ease-pop);
}
.is-ready .prop {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.prop--boulder-1 {
  left: clamp(22%, 24vw, 32%);
  bottom: 3%;
  width: clamp(80px, 12vw, 180px);
}
.prop--boulder-2 {
  left: clamp(3%, 5vw, 10%);
  bottom: 1.5%;
  width: clamp(62px, 10vw, 140px);
}
.prop--boulder-3 {
  right: clamp(26%, 22vw, 32%);
  bottom: 2%;
  width: clamp(90px, 13vw, 190px);
}
.prop--bench {
  left: 50%;
  transform-origin: bottom center;
  transform: translate(-40%, 22px) scale(.9);
  bottom: 7%;
  width: clamp(170px, 22vw, 300px);
}
.is-ready .prop--bench {
  transform: translate(-40%, 0) scale(1);
}
.prop--dog {
  left: clamp(32%, 34vw, 42%);
  bottom: 2%;
  width: clamp(72px, 11vw, 150px);
}

/* Tail wag: rotates the tail group around its base (where it attaches
   to the body). `transform-box: fill-box` scopes transform-origin to
   the path's bounding box so 100%/100% lands at its right-bottom
   corner — which is exactly the base of the tail in SVG coordinates. */
.prop--dog .dog-tail {
  transform-box: fill-box;
  transform-origin: 100% 100%;
  animation: dog-tail-wag 0.55s ease-in-out infinite alternate;
}
@keyframes dog-tail-wag {
  from { transform: rotate(-14deg); }
  to   { transform: rotate(16deg); }
}
.is-ready .prop--boulder-1 { transition-delay: .55s; }
.is-ready .prop--boulder-2 { transition-delay: .60s; }
.is-ready .prop--boulder-3 { transition-delay: .65s; }
.is-ready .prop--bench     { transition-delay: .70s; }
.is-ready .prop--dog       { transition-delay: .85s; }

/* ----- Petals (abstract flat blossoms) ----- */
/* Rabbit cameo (easter egg). Positioned absolutely inside .hero__scene,
   hidden off-screen until JS fires the cameo. JS animates `left` or
   `right` via GSAP to hop it in, nibble, and hop out; `y` is driven by
   a yoyo tween to get the bouncing hop feel. `.is-flipped` mirrors
   the SVG horizontally when the rabbit enters from the right. */
.scene__rabbit {
  position: absolute;
  bottom: 11%;
  left: -15%;
  width: clamp(44px, 6vw, 64px);
  pointer-events: none;
  /* Scene layer stack (reminder):
       z-2 buildings, z-3 back trees, z-4 grass plane,
       z-5 grass blades & back-boulder, z-6 front trees,
       z-7 props container (rocks/bench/dog/people).
     Rabbit lives nestled between the two grass layers:
       - z-4 ties the grass plane (DOM source order puts the rabbit
         after it, so the rabbit paints ON TOP of the plane);
       - grass blades (z-5), both boulder sets, the foreground trees,
         and everything in the props container strictly beat z-4, so
         they all occlude the rabbit as it hops past. */
  z-index: 4;
  opacity: 0;
  visibility: hidden;
}
.scene__rabbit svg {
  width: 100%;
  height: auto;
  display: block;
  transform: scaleX(1);
}
.scene__rabbit.is-flipped svg {
  transform: scaleX(-1);
}

.petals {
  position: absolute; inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 8;
}
.petal {
  position: absolute;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--petal-color, #F4B8C6);
  opacity: 0;
  will-change: transform;
}
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink);
  padding: 8px 14px;
  border: 1px solid color-mix(in srgb, var(--ink) 30%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--cream) 60%, transparent);
  backdrop-filter: blur(6px);
}
.eyebrow__dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--hot);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--hot) 25%, transparent);
  animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.25); }
}

.hero__title {
  font-size: var(--fs-h1);
  margin: 18px 0 24px;
  display: flex;
  flex-wrap: wrap;
  gap: 0 0.25em;
  line-height: 0.92;
}
.hero__word {
  display: inline-block;
  transform: translateY(110%);
  opacity: 0;
  will-change: transform, opacity;
}
.hero__word em {
  color: var(--hot);
  font-style: italic;
}
.is-ready .hero__word {
  animation: wordRise 1.1s var(--ease-settle) forwards;
}
.is-ready .hero__word:nth-child(1) { animation-delay: .2s; }
.is-ready .hero__word:nth-child(2) { animation-delay: .35s; }
.is-ready .hero__word:nth-child(3) { animation-delay: .5s; }
@keyframes wordRise {
  to { transform: translateY(0); opacity: 1; }
}

.hero__lede {
  max-width: 46ch;
  font-size: var(--fs-lede);
  color: var(--ink);
  margin-bottom: 28px;
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .8s ease .9s, transform .8s var(--ease-out-soft) .9s;
}
.is-ready .hero__lede { opacity: 1; transform: translateY(0); }

.hero__actions { display: flex; gap: 12px; flex-wrap: wrap;
  opacity: 0; transform: translateY(12px);
  transition: opacity .8s ease 1.05s, transform .8s var(--ease-out-soft) 1.05s;
}
.is-ready .hero__actions { opacity: 1; transform: translateY(0); }

/* Marquee */
.hero__marquee {
  position: relative;
  z-index: 4;
  margin-top: auto;
  padding: 14px 0;
  border-top: 1px solid color-mix(in srgb, var(--ink) 15%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 15%, transparent);
  background: color-mix(in srgb, var(--cream) 55%, transparent);
  backdrop-filter: blur(6px);
  overflow: hidden;
}
/* Text hard-clips at the container's overflow:hidden boundary, same
   way a news ticker works — cleaner than a mask against the solid
   cream band (a fade-to-cream reads as "running out of ink"). The
   opaque band carries its own visual rhythm. */
.marquee__track {
  display: flex;
  gap: 40px;
  align-items: center;
  font-family: var(--font-display);
  font-size: clamp(20px, 2.3vw, 32px);
  white-space: nowrap;
  animation: marquee 38s linear infinite;
}
.marquee__track .dot { color: var(--hot); font-size: 0.8em; }
@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* ---------- 5. About ---------- */
/* Top padding intentionally ~40px tighter than the bottom so the
   first section peeks above the fold under the hero. The bottom keeps
   the full clamp() so the rhythm into Calendar still breathes. */
.about { padding-block: clamp(40px, 7vw, 120px) clamp(80px, 10vw, 160px); }
.about__grid {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: clamp(24px, 5vw, 80px);
  align-items: start;
}
.about__title {
  grid-column: 1;
  font-size: clamp(34px, 4.6vw, 72px);
  max-width: 17ch;
  padding-top: clamp(16px, 2.5vw, 36px);
}
.about__body {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  gap: 18px;
  color: var(--ink-soft);
  font-size: var(--fs-lede);
  max-width: 52ch;
}
.about__body strong { color: var(--ink); font-weight: 600; }
.about .section-label { grid-column: 1; }

/* ---------- 6. Calendar ---------- */
.calendar {
  padding-block: clamp(60px, 8vw, 120px);
  background: var(--moss);
  color: var(--cream);
  position: relative;
  overflow: hidden;
}
.calendar::before {
  /* Decorative petal scatter */
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(3px 2px at 10% 20%, var(--blossom), transparent),
    radial-gradient(2px 2px at 80% 60%, var(--blossom-soft), transparent),
    radial-gradient(3px 2px at 30% 80%, var(--hot), transparent),
    radial-gradient(2px 2px at 65% 15%, var(--blossom), transparent),
    radial-gradient(2px 2px at 90% 85%, var(--blossom-soft), transparent);
  opacity: .3;
  pointer-events: none;
}
.calendar .section-label { color: color-mix(in srgb, var(--cream) 65%, transparent); }
.calendar .section-title { color: var(--cream); }
.calendar .section-lede  { color: color-mix(in srgb, var(--cream) 80%, transparent); }

.calendar__card {
  background: var(--cream);
  color: var(--ink);
  border-radius: var(--r-lg);
  padding: clamp(20px, 3vw, 32px);
  box-shadow:
    0 30px 60px -20px rgba(0,0,0,.35),
    0 10px 20px -10px rgba(0,0,0,.2);
  position: relative;
  z-index: 1;
}
/* Top bar of the calendar card: hint label on the left, category
   legend on the right. Wraps cleanly on narrow screens. */
.calendar__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 14px 20px;
  margin-bottom: clamp(24px, 3vw, 36px);
}
.calendar__hint {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--ink) 55%, transparent);
}
.calendar__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

/* Iframe wrapper for the Google Calendar embed. Aspect ratio holds a
   stable box so the card doesn't reflow while Google's iframe paints;
   min-height ensures legibility — the MONTH grid clips below ~800px,
   which is why the 800px breakpoint swaps to the subscribe card. */
.calendar__frame {
  position: relative;
  border-radius: var(--r-md);
  overflow: hidden;
  border: var(--hair);
  background: var(--paper);
  aspect-ratio: 4/3;
  min-height: 520px;
}
.calendar__frame iframe {
  width: 100%;
  height: 100%;
  border: 0;
}

/* Desktop / tablet: actions under the embed (hidden on narrow viewports; see
   @media (max-width: 800px) which toggles to .calendar__subscribe). */
.calendar__sync {
  display: none;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 10px 14px;
  margin-top: clamp(16px, 2.2vw, 28px);
  padding-top: clamp(12px, 1.5vw, 20px);
  border-top: var(--hair);
}
@media (min-width: 801px) {
  .calendar__sync { display: flex; }
}

/* Subscribe card — shown in place of the iframe below 800px.
   Toggled by the @media block further down; hidden by default so
   the desktop path has no stray DOM cost. */
.calendar__subscribe {
  display: none;
  text-align: center;
  padding: clamp(28px, 7vw, 40px) clamp(20px, 5vw, 32px);
  border-radius: var(--r-md);
  border: var(--hair);
  background:
    radial-gradient(120% 140% at 50% -20%,
      color-mix(in srgb, var(--blossom) 60%, transparent) 0%,
      transparent 55%),
    var(--paper);
}
.calendar__subscribe-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--ink) 55%, transparent);
  margin: 0 0 14px;
}
.calendar__subscribe-title {
  font-family: var(--font-display);
  font-size: clamp(28px, 5vw, 44px);
  line-height: 1.05;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0 0 14px;
  text-wrap: balance;
}
.calendar__subscribe-title em {
  color: var(--hot);
  font-style: italic;
}
.calendar__subscribe-body {
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 38ch;
  margin: 0 auto 22px;
}
.calendar__subscribe-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-bottom: 20px;
}
.calendar__subscribe-actions .btn {
  flex: 1 1 140px;
  min-width: 140px;
  justify-content: center;
}
/* Subscribe link only renders on mobile, where the card chrome is
   stripped and it sits directly on the moss section background —
   so it needs to be cream-weighted (not ink) to stay readable. */
.calendar__subscribe-link {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.08em;
  color: color-mix(in srgb, var(--cream) 70%, transparent);
  text-decoration: underline;
  text-underline-offset: 4px;
  transition: color .2s ease;
}
.calendar__subscribe-link:hover { color: var(--blossom); }
.calendar__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 12px;
  margin-top: 20px;
}

/* ---------- 7. Dispatches ---------- */
/* ---------- 6b. Get involved (page closer) ----------
   Full section using the site's standard section-head / section-title /
   section-lede / btn conventions so it reads typographically alongside
   the rest of the page. Ink palette is what marks it as the closer;
   the slim site-footer sits flush beneath it in the same ink.

   Italic `em` inside the title picks up --blossom (seasonal accent)
   same as the calendar subscribe card. */
.involve {
  padding-block: clamp(80px, 10vw, 160px) clamp(56px, 7vw, 96px);
  background: var(--ink);
  color: var(--cream);
  text-align: center;
}
.involve .section-head   { margin-inline: auto; }
.involve .section-label  { color: color-mix(in srgb, var(--cream) 55%, transparent); }
.involve .section-title  { color: var(--cream); margin-inline: auto; }
.involve .section-title em { color: var(--blossom); font-style: italic; }
.involve .section-lede   { color: color-mix(in srgb, var(--cream) 80%, transparent); margin-inline: auto; }

.involve__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 12px 16px;
}

/* Invert the buttons for the dark bg — the default primary is
   ink-on-cream (invisible here), and the ghost is cream-on-cream. */
.involve .btn--primary {
  --bg: var(--cream);
  --fg: var(--ink);
  --bd: var(--cream);
}
.involve .btn--primary:hover {
  --bg: var(--blossom);
  --bd: var(--blossom);
}
.involve .btn--ghost {
  --bg: transparent;
  --fg: var(--cream);
  --bd: color-mix(in srgb, var(--cream) 30%, transparent);
}
.involve .btn--ghost:hover {
  --bg: color-mix(in srgb, var(--cream) 10%, transparent);
  --bd: var(--cream);
}

.dispatches {
  padding-block: clamp(80px, 10vw, 160px) clamp(20px, 2.5vw, 40px);
}
.dispatches__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: clamp(20px, 2.5vw, 32px);
}

/* Initial render shows the first DISPATCHES_INITIAL cards; any card past
   that count is tagged `.dispatch--more` and hidden. Clicking the
   "Show more" button flips `.is-expanded` on the grid so they reveal
   without re-rendering the DOM. */
.dispatches__grid .dispatch--more { display: none; }
.dispatches__grid.is-expanded .dispatch--more { display: flex; }

.dispatches__more {
  display: flex;
  justify-content: center;
  margin-top: clamp(28px, 3vw, 44px);
}

.dispatch {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: var(--hair);
  border-radius: var(--r-md);
  overflow: hidden;
  transition: transform .4s var(--ease-out-soft), box-shadow .4s ease, border-color .3s;
}
.dispatch:hover {
  transform: translateY(-6px) rotate(-0.3deg);
  box-shadow: 0 28px 50px -24px rgba(20,18,16,.25);
  border-color: var(--ink);
}
.dispatch__media {
  aspect-ratio: 4/3;
  background: linear-gradient(135deg, var(--blossom) 0%, var(--blossom-deep) 100%);
  overflow: hidden;
  position: relative;
}
.dispatch__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform .8s var(--ease-out-soft);
}
.dispatch:hover .dispatch__media img { transform: scale(1.05); }

.dispatch__media--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 48px;
  color: var(--cream);
}

.dispatch__body {
  padding: 22px 22px 26px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 1;
}
.dispatch__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: color-mix(in srgb, var(--ink) 60%, transparent);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.dispatch__tag {
  padding: 3px 8px;
  border-radius: 4px;
  background: var(--c, var(--hot));
  color: var(--cream);
  font-weight: 500;
}
.dispatch__title {
  font-family: var(--font-display);
  font-size: clamp(22px, 2vw, 30px);
  line-height: 1.05;
  letter-spacing: -0.01em;
}
.dispatch__excerpt {
  color: var(--ink-soft);
  font-size: 15px;
  line-height: 1.55;
}

/* Skeleton */
.dispatch--skeleton {
  min-height: 360px;
  background: linear-gradient(110deg, var(--paper) 20%, var(--cream-deep) 50%, var(--paper) 80%);
  background-size: 200% 100%;
  animation: shimmer 1.6s ease-in-out infinite;
}
@keyframes shimmer {
  from { background-position: 100% 0; }
  to   { background-position: -100% 0; }
}

/* ---------- 8. Visit ---------- */
.visit { padding-block: clamp(60px, 7vw, 112px) clamp(80px, 10vw, 160px); }
/* Grid areas: the section head (eyebrow + title + lede) spans both
   columns on top; the address list and map share the row below. That
   way the map's top edge aligns with the list's top — i.e. sits right
   below the lede — without any magic margin numbers. */
.visit__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-areas:
    "head head"
    "list map";
  column-gap: clamp(24px, 4vw, 60px);
  row-gap: clamp(24px, 3vw, 40px);
  align-items: start;
}
.visit__head { grid-area: head; }
.visit__list { grid-area: list; }
.visit__map  { grid-area: map; }
.visit__lede {
  font-size: var(--fs-lede);
  color: var(--ink-soft);
  max-width: 42ch;
  margin: 20px 0 32px;
}
.visit__list {
  display: grid;
  gap: 18px;
  margin: 0;
}
.visit__list > div {
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: 20px;
  padding: 18px 0;
  border-top: var(--hair);
}
.visit__list > div:last-child { border-bottom: var(--hair); }
.visit__list dt {
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: color-mix(in srgb, var(--ink) 60%, transparent);
  padding-top: 2px;
}
.visit__list dd {
  margin: 0;
  font-size: 16px;
  color: var(--ink);
}
.visit__map {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  border: var(--hair);
  aspect-ratio: 4/5;
  background: var(--paper);
  box-shadow: 0 30px 60px -24px rgba(20,18,16,.25);
  /* Top-align with the left column so both columns start at the same
     grid line (eyebrow / title on the left, map top edge on the right).
     Clean editorial alignment — any offset would float between two
     landmarks and read as unaligned. */
  margin-top: 0;
}
.visit__map iframe {
  width: 100%;
  height: 100%;
  border: 0;
  /* Subtle tint so Google's default vibrance sits inside our warmer,
     cream-forward palette without clashing. */
  filter: saturate(0.88) contrast(1.01);
}
.map__link {
  position: absolute;
  bottom: 16px; left: 16px;
  padding: 10px 14px;
  background: var(--ink);
  color: var(--cream);
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
}
.map__link:hover { background: var(--hot); }

/* ---------- 9. Footer ----------
   Slim ink copyright bar sitting flush under the humble Get involved
   prompt; together they form the page's footer. */
.site-footer {
  background: var(--ink);
  color: var(--cream);
  padding-block: clamp(36px, 4.5vw, 64px) 32px;
}

.footer__base {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 6px;
  color: color-mix(in srgb, var(--cream) 55%, transparent);
  font-size: 13px;
  font-family: var(--font-mono);
}
.footer__base p { margin: 0; }

/* ---------- 10. Scroll reveals ---------- */
.reveal {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity .9s var(--ease-out-soft), transform .9s var(--ease-out-soft);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }

/* ---------- Responsive ---------- */

/* Mid-size hero tuning. Kicks in well before the layout goes to a
   single column (≤880) because the hero-specific issues here show up
   earlier:
     - As the viewport narrows below ~1080px the copy column takes
       enough of the hero width that the buttons start landing over
       the bench + people vertically. Push the bench down so the
       people clear under the buttons.
     - The back-tree row sits at bottom: 15% by default, which roots
       their trunks nicely into grass on tall desktop heroes but
       floats them above the grass once the hero shortens. Pull them
       down so their trunks tuck into the grass band again.
   Front trees stay planted at the scene floor at this breakpoint —
   lifting them here makes them read as floating relative to the
   buildings, which are still at bottom: 0. */
/* The back-trees SVG aspect-locks, so as the viewport narrows the rendered
   tree gets shorter — a fixed bottom: % leaves the trunks above the grass
   hills and the canopies read as floating. Step the lift down as width
   shrinks so the trunks stay tucked into the grass band. */
@media (max-width: 1080px) {
  .prop--bench       { bottom: 3%; }
  .scene__back-trees { bottom: 9%; }
}

@media (max-width: 880px) {
  /* Swap the inline nav for a drawer-style hamburger menu. The
     toggle button takes the right-hand slot in the header, and the
     <nav> drops out of normal flow into an absolutely-positioned
     panel that hangs off the bottom of the fixed header. The header
     is position:fixed already, so it acts as the positioning
     ancestor without us having to set position:relative anywhere. */
  .nav-toggle { display: inline-flex; }

  .nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    gap: 0;
    padding: 10px clamp(16px, 4vw, 32px) 18px;
    background: color-mix(in srgb, var(--cream) 92%, transparent);
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    border-bottom: var(--hair);
    box-shadow: 0 18px 24px -18px rgba(0,0,0,.18);
    /* Hidden by default. visibility lags by the transition duration
       so the panel stays focusable while it's animating in but is
       fully removed from the a11y tree once it finishes animating
       out. */
    visibility: hidden;
    opacity: 0;
    transform: translateY(-6px);
    pointer-events: none;
    transition:
      opacity .25s var(--ease-out-soft),
      transform .25s var(--ease-out-soft),
      visibility 0s linear .25s;
  }
  .site-header[data-menu="open"] .nav {
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
    transition:
      opacity .25s var(--ease-out-soft),
      transform .25s var(--ease-out-soft);
  }
  .nav a {
    font-size: 16px;
    font-weight: 500;
    padding: 14px 4px;
    border-bottom: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  }
  .nav a:last-child { border-bottom: 0; }
  /* Kill the desktop hover underline inside the drawer — links read
     as full-width rows now, with the row border doing the divider
     work. */
  .nav a::after { display: none; }

  .brand__sub { display: none; }

  .about__grid { grid-template-columns: 1fr; }
  .about__title, .about__body { grid-column: 1; }
  /* Title cap + extra top pad only make sense in the 2-column desktop
     layout; on a single column let the heading flow full-width. */
  .about__title {
    max-width: none;
    padding-top: 0;
  }

  .visit__grid {
    grid-template-columns: 1fr;
    grid-template-areas:
      "head"
      "list"
      "map";
  }
  .visit__list > div { grid-template-columns: 110px 1fr; gap: 12px; }
  /* In single-column the map stacks below the list; give it real
     breathing room from the "Dogs" row above (the desktop offset was
     for aligning with the section eyebrow, which doesn't apply here).
     Flatten from 4/5 portrait to 3/2 landscape so the card doesn't
     eat most of a phone screen. */
  .visit__map {
    margin-top: clamp(56px, 9vw, 88px);
    aspect-ratio: 3/2;
  }

  .footer__brand { flex-direction: column; align-items: center; text-align: center; gap: 14px; }

  /* Hero typography scales down in the tablet zone so "Seven Hills"
     + "Park." can breathe without the h1 eating the whole viewport.
     text-wrap: balance keeps the wrap visually tidy. */
  .hero__title {
    font-size: clamp(52px, 12vw, 84px);
    text-wrap: balance;
  }
  .hero__lede { font-size: clamp(16px, 2.2vw, 19px); }
  .hero__copy { padding-block: 0 10%; }
  .scene__tree { height: clamp(240px, 48%, 380px); }

  /* Tablet/phone banners feel too tall at 85svh once the URL bar,
     eyebrow, title, lede, and buttons are all stacked — bring the
     overall hero down a bit at these widths. */
  .hero { height: clamp(560px, 72svh, 780px); }

  /* Scene composition at tablet widths. The desktop base has apartments
     stretching from ~7vw to ~43vw and the church on the right, which
     leaves only ~250px between them at 800px width — they look crowded.
     Pull the apartments left and shrink both so the middle breathes.
     Back-trees and grass tile the rest. */
  .scene__church {
    width:  clamp(170px, 26vw, 260px);
    height: clamp(260px, 60%, 420px);
    right: 0;
  }
  .scene__apartments {
    left: clamp(8px, 2vw, 40px);
    width:  clamp(180px, 32vw, 300px);
    height: clamp(220px, 50%, 360px);
  }
  .prop--boulder-1 { left: clamp(14%, 18vw, 24%); }
  .prop--boulder-3 { right: clamp(18%, 20vw, 26%); }

  /* At tablet widths the scene gets cramped enough that the front
     trees' trunks end up right next to the boulder row instead of
     tucking behind it. A tiny lift (only 1%) is enough for the
     boulders (z-index: 7) to occlude the trunk base (z-index: 6)
     where they overlap — larger lifts read as "tree floating above
     the grass" when paired with the buildings still at bottom: 0.
     Above ~880px the scene has enough room we leave trees at the floor. */
  .scene__tree { bottom: 1%; }

  /* Back-trees sit higher at this width so the canopies clear the
     rooflines instead of hiding behind buildings. */
  .scene__back-trees { bottom: 9%; }

  /* Dispatch cards go full-width on mobile, which makes a 4/3 media
     card ~75% of the viewport in height — a giant block of pink
     before the actual copy. Flatten the aspect + cap the absolute
     height so the image is a nice banner, not the whole card. */
  .dispatch__media {
    aspect-ratio: 16 / 9;
    max-height: 260px;
  }
  .dispatch__media--empty { font-size: 36px; }
}

/* Below 800px, Google's MONTH grid starts breaking (cell widths
   collapse, day names truncate, headers wrap awkwardly). Rather
   than show a broken iframe, hide it + its label/legend bar (which
   only make sense when paired with an actual calendar below them)
   and surface the subscribe CTA instead — the useful action at
   that width isn't "look at a calendar", it's "get these events
   into my calendar app."

   We also strip the nested-card visuals (the cream outer card, the
   pink inner card, the duplicate heading and body) because the
   section already leads with "The calendar." + "Add it to your own
   calendar with one click." right above. So on mobile we're left
   with just two buttons and a link, sitting naturally in the
   section background. */
@media (max-width: 800px) {
  .calendar__bar             { display: none; }
  .calendar__frame           { display: none; }
  .calendar__actions         { display: none; }
  .calendar__subscribe-title { display: none; }
  .calendar__subscribe-body  { display: none; }

  .calendar__card {
    background: none;
    padding: 0;
    box-shadow: none;
  }
  .calendar__subscribe {
    display: block;
    background: none;
    border: 0;
    padding: 0;
  }
  .calendar__subscribe-actions { margin-bottom: 16px; }
}

@media (max-width: 560px) {
  .site-header__inner { padding-block: 12px; }
  .brand__name { font-size: 17px; }
  /* The 880px block already turned .nav into a column drawer with
     gap:0 and per-link borders — don't reintroduce a gap here, it
     just adds awkward space between the divider lines. */

  /* Stretched + flex-centered isn't enough once the label wraps to two
     lines — the lines themselves still hug the left edge. Force the
     text to center so a two-line "See the / calendar" looks right. */
  .hero__actions .btn {
    flex: 1;
    justify-content: center;
    text-align: center;
  }
  .marquee__track { font-size: 20px; gap: 28px; }

  /* Scene gets too cramped for the desktop footprint below ~560px:
     - Church & apartments share the horizontal band and start to overlap.
     - The foreground boulders (1 on left-of-center, 3 on right-of-center)
       drift under the bench / dog / people. Pull them outward and shrink
       everything a notch so each element still has breathing room. */
  .scene__church {
    width:  clamp(140px, 34vw, 220px);
    height: clamp(240px, 64%, 420px);
    right: 0;
  }
  .scene__apartments {
    left: clamp(4px, 2vw, 20px);
    width:  clamp(150px, 40vw, 240px);
    height: clamp(200px, 52%, 340px);
  }
  .prop--boulder-1 {
    left: clamp(2%, 4vw, 8%);
    width: clamp(56px, 14vw, 96px);
  }
  .prop--boulder-3 {
    right: clamp(2%, 4vw, 8%);
    width: clamp(64px, 15vw, 110px);
  }
  /* Hero CTA buttons are full-width on phones (flex: 1), so they span
     the center column — right where the bench sits (left: 50%).
     Shrink the bench and tuck it + the dog closer to the grass line
     so they read as "sitting on the grass" rather than being covered
     mid-body by the buttons above. */
  .prop--bench {
    bottom: 1.5%;
    width: clamp(130px, 40vw, 190px);
  }
  .prop--dog {
    left: clamp(28%, 32vw, 38%);
    bottom: 0;
    width: clamp(56px, 14vw, 100px);
  }

  /* Tighten the hero headline + lede + buttons so the buttons sit in
     the cloud/canopy band instead of overlapping the bench. The lede
     is the main height culprit (wraps to 4–6 lines on a narrow column),
     so tighten its size + line-height alongside the title shrinkage.
     Top padding stays generous though — on phones the eyebrow +
     title feel cramped right under the header, and the extra sky
     above reads as "ah, a sky" rather than "oh, tight margin". */
  .hero__copy {
    padding-block: 0 10%;
  }
  .hero__title {
    font-size: clamp(40px, 10vw, 64px);
    margin: 8px 0 14px;
  }
  .hero__lede {
    font-size: 15px;
    line-height: 1.35;
    margin-bottom: 18px;
  }

  /* At phone widths the boulders are pulled to the outer edges
     (boulder-1 hugs left, boulder-3 hugs right — see above in this
     block), which *does* overlap with the front tree trunks. A small
     lift (2%) keeps the boulders occluding the trunk base so the
     tree reads behind the rock, without floating the canopy high off
     the grass. We still slide the trees further off the viewport so
     they become narrow bookends rather than dominating the scene. */
  .scene__tree {
    bottom: 2%;
  }
  .scene__tree--left  { left:  -6%; }
  .scene__tree--right { right: -6%; }

  /* Phone widths shrink the back-trees SVG even further (aspect 3.89:1
     means a 380px-wide container renders the tree ~98px tall), so we
     need a larger lift to keep the canopies above the rooflines. */
  .scene__back-trees { bottom: 7%; }

  /* The calendar card is already flattened below 800px (iframe/bar/
     actions/titles all hidden, card chrome stripped), so no further
     phone tuning is needed here. */

  /* Even tighter on real phones: cards sit very narrow, so a 16/9
     media still ends up tall. Trim the cap further. */
  .dispatch__media { max-height: 200px; }
}

/* Really narrow phones: the full "Friends of Seven Hills Park" starts
   colliding with the nav, so swap in the FoSHP abbreviation. Screen
   readers still get the full name via the brand link's aria-label. */
@media (max-width: 480px) {
  .brand__name-full  { display: none; }
  .brand__name-short { display: inline; }
  .brand__name { font-size: 20px; }
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.2s !important;
    scroll-behavior: auto !important;
  }
  .tree, .boulder, .dog { transform: none !important; opacity: 1 !important; }
  .hero__word { transform: none !important; opacity: 1 !important; }
  .petal { display: none; }
}
