/* Base styles - accessibility first. */
:root {
    /* Sage & Clay palette */
    --color-primary:       #7d9670;
    --color-primary-deep:  #5d7a52;
    --color-accent:        #b87d5e;
    --color-bg:            #f6f3ed;
    --color-surface:       #ffffff;
    --color-fg:            #2a2a26;
    --color-muted:         #6f6a5e;
    --color-border:        #d8d2c4;
    --color-cancelled:     #a64535;

    /* Radius */
    --radius:      8px;   /* legacy alias; new code uses --radius-md */
    --radius-sm:   4px;
    --radius-md:   8px;
    --radius-lg:   12px;
    --radius-pill: 999px;

    /* Warm-tinted shadows */
    --shadow-1: 0 1px 2px rgba(70, 60, 40, 0.07);
    --shadow-2: 0 4px 12px rgba(70, 60, 40, 0.10);
    --shadow-3: 0 12px 28px rgba(70, 60, 40, 0.14);

    /* Base type */
    --base-font-size: 18px;
    --target-min: 44px;
}

html { font-size: var(--base-font-size); overflow-x: clip; }

body {
    margin: 0;
    font-family: "Atkinson Hyperlegible", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    color: var(--color-fg);
    background: var(--color-bg);
    line-height: 1.55;
    /* Vertical flex so main can flex:1 to fill the space between header
       and footer. The home page's discovery layout relies on this to
       compute its own height without hardcoding chrome dimensions. */
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    /* Belt-and-suspenders horizontal-overflow guard. On iPad portrait
       (810px viewport) the persona-chip and time-bucket-pill rows
       overflow their pane by ~30-75px, which made the whole document
       horizontally pannable on iOS Safari ("the event card list, I
       can drag it left-right"). overflow-x: clip clips the overflow
       WITHOUT establishing a scroll container, so position: sticky on
       the chip bar keeps working -- unlike overflow-x: hidden which
       would break sticky in nested elements. Safari 16+ / Chrome 90+. */
    overflow-x: clip;
}

main {
    max-width: 1200px;
    width: 100%;
    box-sizing: border-box;
    margin: 0 auto;
    padding: 0.25rem 1.5rem 1.5rem;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
}

.site-header {
    max-width: 1200px;
    margin: 0 auto;
    /* Top padding matches bottom so the gap above the logo balances the
       gap from the testimonial's attribution line down to whatever
       follows (filter stack, etc.) -- otherwise the header reads as
       top-heavy with the logo crowded against the viewport edge. */
    padding: 1.75rem 1.5rem 1.75rem;
    text-align: center;
}
.site-header h1 { margin: 0; }
/* Visible button affordance on the logo: a soft persistent pill-shaped
   panel around the logo + a small "⌂ Home" label below it. The panel
   is barely visible at rest (just enough to read as "this is a tappable
   surface") and brightens on hover / focus. The 55+ audience tends NOT
   to assume "logo = home page" implicitly, so the explicit Home label
   under the wordmark anchors the action visually. */
.wordmark a.wordmark__link {
    color: inherit;
    text-decoration: none;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    /* Generous padding on all sides so the pill reads as a deliberate
       frame around the wordmark rather than hugging it. Bottom stays a
       touch tighter than top so the "Home" hint label below the logo
       lands closer to the wordmark than to the pill edge. */
    padding: 14px 32px 12px;
    border-radius: var(--radius-pill, 999px);
    background: rgba(70, 60, 40, 0.04);
    /* Visible rest shadow so the pill clearly reads as a raised
       tappable surface even before any hover. The hover state below
       still escalates beyond this for a clear "lift" delta. */
    box-shadow: 0 3px 8px rgba(70, 60, 40, 0.22),
                0 1px 2px rgba(70, 60, 40, 0.14);
    line-height: 0;
    transition: background-color 120ms ease, transform 120ms ease,
                box-shadow 120ms ease;
}
.wordmark a.wordmark__link:hover {
    background: rgba(70, 60, 40, 0.10);
    /* Tactile lift: shift up 3px and stack a tight + wider shadow so
       the panel reads as floating above the page surface. The active
       state below collapses it back to baseline for a "press down"
       feel on click. */
    transform: translateY(-3px);
    box-shadow: 0 8px 18px rgba(70, 60, 40, 0.22),
                0 2px 4px rgba(70, 60, 40, 0.14);
}
.wordmark a.wordmark__link:focus-visible {
    outline: 2px solid var(--color-primary-deep, #3a5b34);
    outline-offset: 3px;
    background: rgba(70, 60, 40, 0.10);
    transform: translateY(-3px);
    box-shadow: 0 8px 18px rgba(70, 60, 40, 0.22),
                0 2px 4px rgba(70, 60, 40, 0.14);
}
.wordmark a.wordmark__link:active {
    /* Collapse back to baseline + drop the shadow so click reads as
       pressing the lifted panel back down onto the page. */
    transform: translateY(0);
    box-shadow: 0 1px 2px rgba(70, 60, 40, 0.18);
}
.wordmark__home-hint {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-top: 2px;
    color: rgba(70, 60, 40, 0.65);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1;
}
.wordmark a.wordmark__link:hover .wordmark__home-hint,
.wordmark a.wordmark__link:focus-visible .wordmark__home-hint {
    color: var(--color-primary-deep, #3a5b34);
}
.wordmark__home-hint-icon {
    font-size: 1.05em;
    line-height: 1;
}
.wordmark__img {
    height: 112px;
    width: auto;
    display: block;
}
.site-tagline {
    /* Increased from 1.1rem -> 2rem so the tagline doesn't crowd
       the logo pill above it. The pill's lift-on-hover treatment
       benefits from breathing room beneath. */
    /* Bottom margin opens the gap below the "reimagined" underline so it
       matches the 0.9rem gap from the testimonial pill down to the date.
       Collapses with the next sibling's top margin (takes the larger). */
    margin: 1rem 0 0.9rem;
    color: #4a4537;             /* darker than --color-muted, lighter than --color-fg */
    font-size: 1.4rem;          /* up from 1.125rem -- closer to a confident magazine deck */
    font-weight: 500;           /* a notch above body; not bold */
    line-height: 1.35;
    letter-spacing: -0.005em;   /* slight tightening for editorial feel */
}

/* Joke footnote BELOW the testimonial -- placing it under the quote
   lets the reader take the quote at face value for a half-beat
   before the wink lands. Small italic / lowercase so it reads as a
   footnote, not a heading. */
.site-testimonial__label {
    margin: -0.25rem auto 0;
    font-size: 1.05rem;
    color: #6b5d44;
    font-weight: 500;
    font-style: italic;
    text-align: center;
}

/* Rotating testimonial under the tagline. Italic + muted so it reads
   as an overheard quote, not site copy. Random per page load, only on
   the home page (rendered by events/list.html into the base template's
   `{% block testimonial %}` slot). */
.site-testimonial {
    /* Grid layout: row 1 = quote+emoji (spanning all cols), row 2 =
       [1fr spacer] [attribution] [refresh button]. Cols 1 and 3 are
       equal flex spacers so the attribution in col 2 is dead-center
       regardless of the refresh button's width. The button sits with
       justify-self: start in col 3, hugging the attribution's right
       edge -- text is centered, affordance is pulled in close, neither
       shifts the other. */
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    grid-template-rows: auto auto;
    column-gap: 0.25rem;
    /* row-gap pulls quote and attribution together; tightened so they
       read as a single "overheard quote" unit rather than two lines. */
    row-gap: 0.1rem;
    /* Tightened from 0.7rem -> 0.4rem so the quote pulls up closer to
       the tagline above (one block of brand chrome, not two). */
    margin: 0.4rem auto 0;
    max-width: 900px;
    padding: 0 1rem;
    color: #6b5d44;
    font-style: italic;
    font-size: 1.05rem;
    line-height: 1.3;
    text-align: center;
}

/* "Today: Wednesday, May 28 · 47 events in the next 3 hours →" line.
   Pulls the user from the brand intro into actionable content -- a
   vital sign that something is happening AND a date anchor so "today"
   appearing in the tagline + filter chips above ties to a real date. */
.site-pulse {
    /* Extra bottom margin sets the CTA apart from the pill/quote below it
       and lets the pill sit in a balanced band of whitespace. */
    margin: 1rem auto 1.3rem;
    text-align: center;
    color: #6b5d44;
}
/* Date reads as a quiet sign-off below the quote, closing the header's
   wide -> narrow cascade. Top margin sets it apart from the testimonial
   above; it centers on its own now that it's left the .site-pulse line. */
.site-pulse__date {
    margin: 0.9rem auto 0;
    font-size: 1.2rem;
    font-weight: 600;
    color: #6b5d44;
    text-align: center;
}
/* Centered wrapper for the header pulse CTA pill (moved up out of the shell
   bar). The pill reuses .shell-bar__pulse for its clay look; we just center
   it and cancel that class's margin-left:auto, which only made sense when it
   was the right-most flex child of the shell bar. */
.site-pulse__cta {
    margin: 0.7rem auto 0;
    text-align: center;
}
.site-pulse__cta-link {
    margin-left: 0;
    /* Promoted from the compact shell-bar size to the header-CTA scale --
       this is the hero's primary action + the fast path into the events,
       and a larger target also suits the 55+ audience. Padding/font bumped
       here only so the shared .shell-bar__pulse style stays compact. */
    padding: 0.6rem 1.4rem;
    font-size: 1.15rem;
}
/* The "X events in the next 3 hours" line is the header's primary action,
   so it carries full button weight: a solid rust fill, off-white text and
   a warm lift shadow echoing the wordmark pill -- unmistakably tappable. */
.site-pulse__link {
    display: inline-block;
    padding: 0.65rem 1.5rem;
    background: #7a3a1a;
    border: 1px solid #6a3215;
    border-radius: 999px;
    color: #fbf7ef;
    font-size: 1.18rem;
    font-weight: 700;
    line-height: 1.15;
    text-decoration: none;
    box-shadow: 0 3px 8px rgba(70, 60, 40, 0.20),
                0 1px 2px rgba(70, 60, 40, 0.12);
    transition: background-color 140ms ease, box-shadow 140ms ease,
                transform 140ms ease;
}
.site-pulse__link:hover,
.site-pulse__link:focus-visible {
    background: #8c4520;
    box-shadow: 0 6px 14px rgba(70, 60, 40, 0.24),
                0 2px 4px rgba(70, 60, 40, 0.14);
    transform: translateY(-1px);
    outline: none;
}
.site-pulse__link:active {
    transform: translateY(0);
    box-shadow: 0 1px 2px rgba(70, 60, 40, 0.18);
}
/* The body span is a transparent layout shim -- with display:contents
   its children (quote-line wrapper + attr) become DIRECT grid items of
   .site-testimonial so the grid placement above works naturally. The
   refresh button is also a sibling of body inside .site-testimonial,
   so it's a grid item too. */
.site-testimonial__body { display: contents; }
/* Quote + emoji wrapper. Spans full width on grid row 1. */
.site-testimonial__quote-line {
    grid-column: 1 / -1;
    grid-row: 1;
}
.site-testimonial__quote { display: inline; }
/* Curated face emoji that follows the quote -- maps the emotional
   tone of the line. Hand-picked per testimonial, not random.
   Rendered as an animated WebP <img> from Google's Noto Emoji
   Animation set (fall-back is a plain Unicode <span> for emojis
   without an animated counterpart). The size + vertical-align below
   covers both the <img> and the fallback <span>. */
.site-testimonial__emoji {
    display: inline-block;
    /* Bumped from 0.3rem -> 0.6rem so the glyph doesn't kiss the
       closing curly-quote. */
    margin-left: 0.6rem;
    /* Bumped from 1.4em x 1.4em (~50% larger) so the Noto Animation
       detail (eye twinkles, mouth motion) reads clearly even from
       arm's length -- 55+ audience. Vertical-align adjusted in
       proportion to keep the glyph baseline lined up with the
       quote text. */
    width: 2.1em;
    height: 2.1em;
    font-size: 1.15em;
    line-height: 1;
    vertical-align: -0.7em;
}
/* Span fallback (no animated URL) keeps the box-less inline size. */
span.site-testimonial__emoji {
    width: auto;
    height: auto;
    vertical-align: -0.05em;
}
.site-testimonial__attr {
    /* Grid item: col 2, row 2 (center column, second row). */
    grid-column: 2;
    grid-row: 2;
    color: #8a7a5e;
    font-style: normal;
    font-size: 0.95rem;
    letter-spacing: 0.01em;
}
/* Small refresh icon next to the attribution. Subtle by default so it
   reads as a footnote affordance, not a CTA. Spins once on click via
   the .is-spinning class added by the JS handler in list.html. */
.site-testimonial__refresh {
    /* Grid item: col 3, row 2. justify-self: start pins it to the
       LEFT edge of col 3 so it sits right next to the attribution,
       not floating in the middle of col 3. */
    grid-column: 3;
    grid-row: 2;
    justify-self: start;
    align-self: center;
    margin-left: 0;
    padding: 0.1rem 0.2rem;
    transform: translateY(-9px);
    background: none;
    border: none;
    cursor: pointer;
    color: #b8a888;
    font-size: 0.95rem;
    line-height: 1;
    border-radius: 999px;
    vertical-align: baseline;
    transition: color 150ms ease, background-color 150ms ease;
}
.site-testimonial__refresh:hover,
.site-testimonial__refresh:focus-visible {
    color: #6b5d44;
    background: rgba(70, 60, 40, 0.06);
    outline: none;
}
.site-testimonial__refresh.is-spinning {
    animation: site-testimonial-spin 380ms ease;
}
@keyframes site-testimonial-spin {
    from { transform: rotate(0); }
    to   { transform: rotate(360deg); }
}

/* Collapsed pill shown to return visitors. Hidden by default so first-
   time visitors and JS-off users always get the full gag; the early
   inline script in list.html adds .tv-testimonial-collapsed to <html>
   on later visits, which swaps the full quote for this pill. Warm-shadow
   edge so it reads as a tappable surface. */
.site-testimonial__pill {
    display: none;
    width: fit-content;
    margin: 0.4rem auto 0;
    align-items: center;
    gap: 0.4rem;
    padding: 0.3rem 0.95rem;
    background: rgba(70, 60, 40, 0.05);
    border: 1px solid rgba(70, 60, 40, 0.18);
    border-radius: 999px;
    color: #6b5d44;
    font-style: italic;
    font-size: 0.95rem;
    line-height: 1;
    cursor: pointer;
    transition: background-color 150ms ease, border-color 150ms ease;
}
.site-testimonial__pill:hover,
.site-testimonial__pill:focus-visible {
    background: rgba(70, 60, 40, 0.09);
    border-color: rgba(70, 60, 40, 0.28);
    outline: none;
}
.site-testimonial__pill-caret { color: #b8a888; }

/* Subtle collapse affordance mirroring the refresh button: it sits in
   the empty left spacer column (col 1, row 2) and hugs the attribution's
   left edge, so the centered attribution doesn't shift. */
.site-testimonial__collapse {
    grid-column: 1;
    grid-row: 2;
    justify-self: end;
    align-self: center;
    margin-right: 0;
    padding: 0.1rem 0.2rem;
    transform: translateY(-8px);
    background: none;
    border: none;
    cursor: pointer;
    color: #b8a888;
    font-size: 0.95rem;
    line-height: 1;
    border-radius: 999px;
    transition: color 150ms ease, background-color 150ms ease;
}
.site-testimonial__collapse:hover,
.site-testimonial__collapse:focus-visible {
    color: #6b5d44;
    background: rgba(70, 60, 40, 0.06);
    outline: none;
}

/* Collapsed state: swap the full quote block for the slim pill. */
html.tv-testimonial-collapsed .site-testimonial,
html.tv-testimonial-collapsed .site-testimonial__label {
    display: none;
}
html.tv-testimonial-collapsed .site-testimonial__pill {
    display: flex;
}

@media (max-width: 600px) {
    .site-testimonial { font-size: 1rem; }
    .site-testimonial__attr { font-size: 0.9rem; }
    .site-testimonial__pill { font-size: 0.9rem; }
    .site-pulse__date { font-size: 1.05rem; }
    .site-pulse__link { font-size: 1rem; padding: 0.5rem 1rem; }
}

@media (max-width: 640px) {
    /* Pull <main>'s horizontal padding way in so event cards can stretch
       almost to the screen edges. The summary bar's negative-margin
       trick is updated below to match. */
    main {
        padding-left: 0.5rem;
        padding-right: 0.5rem;
    }
    /* Wordmark image at desktop height (112px) renders wider than an
       iPhone viewport. Shrink it and cap to container width so it never
       forces horizontal overflow. Vertical rhythm also tightened so the
       header doesn't eat the whole first screen. */
    .site-header {
        padding: 0.2rem 1rem 0.4rem;
    }
    .wordmark__img {
        height: 56px;
        max-width: 100%;
    }
    .site-tagline {
        font-size: 1rem;
        line-height: 1.25;
        margin-top: 0.25rem;
        padding: 0 0.25rem;
    }
    /* Force the "Find something to do today." clause to its own line on
       mobile so the tagline breaks at the natural sentence boundary
       instead of wherever width happens to wrap. */
    .tagline-find { display: block; }
    .site-testimonial__label {
        font-size: 0.86rem;
        letter-spacing: 0.1em;
        margin-top: 0.4rem;
        padding: 0 0.5rem;
    }
    .site-testimonial {
        font-size: 0.88rem;
        line-height: 1.35;
        margin-top: 0.25rem;
        padding: 0 0.75rem;
    }
    .site-testimonial__attr { font-size: 0.78rem; }
}

/* "Reimagined" treatment -- a wand-drawn gold underline that draws
   once on page load (~1.3s) and then settles at low opacity as a
   static brand cue. Used to have a hue-shifting four-point starburst
   riding the leading edge + bursting at the right; removed because
   it fought with the live testimonial emoji a few inches away. */
.tagline-magic {
    position: relative;
    display: inline-block;
    padding-bottom: 4px;            /* room below the baseline for the underline */
    color: #8c5a3b;                 /* warm earth tint so the word reads as the focal point */
    font-style: italic;
    font-weight: 600;
}
.tagline-magic::before {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 2px;
    background: linear-gradient(90deg, #f0c75a 0%, #c9722a 100%);
    border-radius: 1px;
    transform: scaleX(0);
    transform-origin: left center;
    /* Runs ONCE on page load -- previously looped every 7s, but that
       fought with the live emoji nearby. forwards fill-mode keeps the
       final keyframe state so the underline stays softly visible after
       the draw completes. */
    animation: tagline-draw 7s ease-in-out 1 forwards;
}
@keyframes tagline-draw {
    /* One-shot version: draw fully visible, then settle to a soft
       muted opacity so the underline persists as a brand cue without
       continuing to draw attention. Previously this keyframe ended at
       opacity 0 because the next loop iteration would re-draw -- with
       iteration count = 1 we WANT the end state to stay visible. */
    0%   { transform: scaleX(0); opacity: 1; }
    18%  { transform: scaleX(1); opacity: 1; }
    78%  { transform: scaleX(1); opacity: 0.9; }
    100% { transform: scaleX(1); opacity: 0.65; }
}
@media (prefers-reduced-motion: reduce) {
    .tagline-magic::before {
        animation: none;
        transform: scaleX(1);       /* show the underline statically */
        opacity: 0.7;
    }
}
/* Tiny raised glyph for the registered-trademark symbol so it reads
   as a typographic superscript rather than a full-size character
   sitting on the baseline. Reusable anywhere on the site. */
.reg {
    font-size: 0.6em;
    vertical-align: super;
    line-height: 0;
    margin-left: 0.1em;
}

h1, h2, h3 { line-height: 1.25; }

a {
    color: var(--color-primary-deep);
    text-underline-offset: 3px;
}
a:focus, button:focus, input:focus, select:focus, textarea:focus {
    outline: 3px solid var(--color-primary);
    outline-offset: 2px;
}

button, .btn {
    min-height: var(--target-min);
    min-width: var(--target-min);
    padding: 0.5rem 1rem;
    background: var(--color-primary);
    color: white;
    border: 0;
    border-radius: var(--radius-sm);
    font-size: 1rem;
    cursor: pointer;
    text-decoration: none;
    display: inline-block;
    font-family: inherit;
}
button:hover, .btn:hover { filter: brightness(1.05); }

input:not([type=checkbox]):not([type=radio]), select, textarea {
    min-height: var(--target-min);
    padding: 0.5rem;
    font-size: 1rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-fg);
    font-family: inherit;
}

.event-card {
    background: var(--color-surface);
    /* container-type: inline-size lets @container queries inside this
       card respond to the card's own width. Used by the .time row to
       hide its " · " separator when the row wraps; see the @container
       block below. inline-size restricts the query axis to width only,
       which is the cheapest option and avoids the layout-cost concerns
       of full `size`. */
    container-type: inline-size;
    container-name: ecard;
    /* Own stacking context so the card's z-indexed bits (the ICS "+" FAB,
       glyph) stay contained -- otherwise they paint above the sticky feed
       band header (z-index:1) and peek over the pinned label as the card
       scrolls behind it. */
    isolation: isolate;
    /* Left padding accommodates the 7px stripe + ~0.55rem gap + 56px
       glyph + ~0.7rem gap before the title (~5rem total). has-thumb
       cards widen the right side further (via .event-card.has-thumb)
       to clear the venue thumb anchored at right: 1rem. */
    padding: 1rem 3rem 1rem 5rem;
    margin-bottom: 1rem;
    border-radius: var(--radius-md);
    /* Stripe color is per-card (set by .cat-X), drawn as an inset
       box-shadow so it follows the card's rounded corners naturally.
       Cards with no persona match keep --stripe-color transparent. */
    --stripe-color: transparent;
    /* 1px outline ring (via box-shadow so it doesn't expand the box and
       reflow inner text) gives the card a defined edge at rest. Hover
       and .is-current states fully override this shadow, so the ring is
       cleanly replaced by their sage-green focus rings. The inset
       7px stripe is appended last so it sits inside the ring. */
    box-shadow:
        0 0 0 1px rgba(70, 60, 40, 0.28),
        var(--shadow-2),
        inset 7px 0 0 0 var(--stripe-color);
    /* Pop-out hover: a small lift + deeper shadow signals 'this is the
       row you're pointing at'. Keeps the scroll-sync accent outline
       (.event-card.is-current) visible when both states are active. */
    transition: transform 160ms ease, box-shadow 160ms ease;
    /* Anchor for absolute-positioned children (left stripe, glyph,
       venue thumb). */
    position: relative;
    cursor: pointer;
}

/* Variant-C marker: 7px colored left stripe (inset box-shadow keyed off
   --stripe-color) plus a thin-line SVG glyph slot. The glyph color
   matches the stripe so the left edge reads as one composed margin
   instead of two stacked decorations. */
.event-card__glyph {
    position: absolute;
    left: 0.85rem;
    /* Nudged down off the title's top edge so the 56px glyph reads as
       visually centered on the title+time block (not just the title). */
    top: 1.35rem;
    width: 56px;
    height: 56px;
    color: var(--color-muted);
    pointer-events: none;
}
.event-card__glyph svg { display: block; width: 100%; height: 100%; }
/* Emoji variant: the primary scanning signal on the card. Sized bold
   enough to read as "what activity is this" before the user reads any
   text. The 56x56 box matches the venue thumbnail's anchor on the
   right side so the two visual anchors balance the card. The
   drop-shadow lifts the emoji off the card so it reads as an icon,
   not a sticker. */
.event-card__glyph--emoji {
    font-size: 44px;
    line-height: 56px;
    text-align: center;
    filter: drop-shadow(0 3px 4px rgba(40, 30, 20, 0.38));
    transition: filter 220ms ease;
}
/* Image variant: real photo in the same 56x56 absolutely-positioned
   slot the base .event-card__glyph rule defines. Don't re-set
   width/height here -- "width: 100%" on an absolutely-positioned img
   resolves against the nearest positioned ancestor (the card),
   stretching the icon across the whole row. Inheriting the explicit
   56x56 from the base rule keeps the slot tight to the upper-left. */
.event-card__glyph--img {
    object-fit: contain;
    filter: drop-shadow(0 3px 4px rgba(40, 30, 20, 0.38));
    transition: filter 220ms ease;
}
/* Per-persona stripe colors. Hue rotates ~20deg per rule around the
   full color wheel (HSL hues 0->340) while saturation and lightness
   stay roughly constant (~30% / ~55%), so the 18 personas read as
   distinct hues that all share the same "muted earth" intensity on
   the cream page background. Rules are unscoped on purpose: the
   .persona-X class on the drawer's <details> headers picks up the
   same --stripe-color so drawer dots and card stripes share a single
   source of truth for color. */
.persona-racquet_court      { --stripe-color: #b07d5e; } /* terracotta */
.persona-lawn_games         { --stripe-color: #b08a4a; } /* caramel */
.persona-active_lifestyle   { --stripe-color: #5fa060; } /* fern green */
.persona-water_sports       { --stripe-color: #50a07d; } /* sea foam */
.persona-dance              { --stripe-color: #50a095; } /* muted teal */
.persona-live_entertainment { --stripe-color: #5095a0; } /* slate blue */
.persona-music_lovers       { --stripe-color: #5080a0; } /* dusty blue */
.persona-arts_crafts        { --stripe-color: #6f60a8; } /* muted purple */
/* Bucket-level stripe overrides. These five buckets used to be their
   own personas (chips on the home discovery row); after the 2026-05
   consolidation they fold into Active Lifestyle / Entertainment but
   keep their distinct stripe color via this layer so event cards
   still read as Pickleball / Golf / etc. at a glance. */
.bucket-pickleball         { --stripe-color: #b07070; } /* dusty rose */
.bucket-table_tennis       { --stripe-color: #a89540; } /* olive gold */
.bucket-golf               { --stripe-color: #95a040; } /* muted lime */
.bucket-softball           { --stripe-color: #7da050; } /* moss green */
.bucket-movie              { --stripe-color: #5070a8; } /* periwinkle */
.persona-lifelong_learning  { --stripe-color: #8060a8; } /* heather */
.persona-game_room          { --stripe-color: #9560a0; } /* muted plum */
.persona-social_clubs       { --stripe-color: #a05f8c; } /* mauve */
.persona-markets            { --stripe-color: #a05f75; } /* dusty pink */
.persona-special_interests  { --stripe-color: #a06070; } /* antique rose */
/* Glyph color follows the per-persona stripe so the left edge reads as
   one composed margin instead of two stacked decorations. */
.event-card .event-card__glyph { color: var(--stripe-color); }
/* "Add to calendar" download trigger. The active --mini variant below
   (the only one used by templates today) renders inline at the end of
   the .time row -- the absolute-positioned base styles here are kept as
   reasonable defaults but are overridden by the --mini modifier. */
.event-card__ics {
    position: absolute;
    top: 1rem;
    right: 1rem;
    /* Sits above the venue thumbnail so the tooltip and the "+" glyph
       aren't clipped when the button overlaps the thumb corner. */
    z-index: 2;
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-fg);
    text-decoration: none;
    box-shadow: var(--shadow-1);
    transition: transform 120ms ease, box-shadow 120ms ease;
}
.event-card__ics:hover {
    transform: translateY(-1px);
    box-shadow: var(--shadow-2);
}
/* Mini variant: small inline "+" pill that sits at the trailing end of
   the time row, right after the date/time text. Inline placement is
   what makes the action self-explanatory -- the plus reads as "add this
   date/time to my calendar" because it is colocated with the data it
   operates on. position:relative anchors the ::after tooltip to this
   button instead of the card. */
.event-card__ics--mini {
    position: relative;
    top: auto;
    right: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    vertical-align: middle;
    width: 22px;
    height: 22px;
    margin-left: 0.5rem;
    /* Lift the button optically so it tracks the text x-height rather
       than the line's full vertical center, which sits slightly low. */
    transform: translateY(-3px);
    border-radius: 50%;
    background: var(--color-primary);
    color: white;
    border: none;
    box-shadow: 0 1px 2px rgba(40, 30, 20, 0.22);
}
/* Expand the tap target to the 44x44 Apple-HIG minimum without growing
   the 22px visual circle. The button is the trailing element on the time
   line, so there is empty space to its right and below (the venue link
   sits below-LEFT, horizontally disjoint). We extend the invisible hit
   area down-right only -- never left (time text) or up (title link) --
   so neighbouring taps are never stolen. */
.event-card__ics--mini::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 44px;
    height: 44px;
}
.event-card__ics--mini:hover {
    background: var(--color-primary-deep);
    box-shadow: 0 2px 4px rgba(40, 30, 20, 0.26);
}
/* Reset the inner span fully -- the base .event-card__ics-add rules
   give it its own absolute-positioned green oval, and the
   .event-card__ics:hover .event-card__ics-add rule was repainting it
   on hover (the "vertical squished" ghost effect). The mini variant
   needs the inner span to be a plain glyph holder. */
.event-card__ics--mini .event-card__ics-add,
.event-card__ics--mini:hover .event-card__ics-add {
    position: static;
    width: auto;
    height: auto;
    background: transparent;
    color: inherit;
    border-radius: 0;
    font-size: 1rem;
    font-weight: 700;
    line-height: 1;
    box-shadow: none;
    /* The "+" glyph's visual center sits slightly below the line-box
       center (typography rests at baseline, not math-center), so flex
       align-items:center renders it looking low. Nudging the inner
       span up 1px lands the visual center on the circle's center. */
    transform: translateY(-1px);
}
.event-card__ics-month {
    background: var(--color-accent);
    color: white;
    font-size: 0.65rem;
    font-weight: 700;
    line-height: 1;
    padding: 3px 0 4px;
    text-align: center;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.event-card__ics-day {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.4rem;
    font-weight: 700;
    line-height: 1;
}
/* "+" badge top-left of the date tile. Telegraphs the add-to-calendar
   affordance to users who otherwise read the tile as a passive date
   stamp. Slightly oversized circle that overflows the tile's corner
   so it reads as an attached action, not a label baked into the date. */
.event-card__ics-add {
    position: absolute;
    top: -7px;
    left: -7px;
    width: 18px;
    height: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--color-primary);
    color: white;
    border-radius: 50%;
    font-size: 0.85rem;
    font-weight: 700;
    line-height: 1;
    box-shadow: 0 1px 3px rgba(40, 30, 20, 0.30);
    pointer-events: none;
}
.event-card__ics:hover .event-card__ics-add {
    background: var(--color-primary-deep);
}
/* Instant tooltip: reads from the aria-label so the markup stays a
   single source of truth. Uses near-zero transition so it appears as
   soon as the hover lands. */
.event-card__ics::after {
    content: attr(aria-label);
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 6px;
    padding: 0.35rem 0.6rem;
    background: #4a3f2e;
    color: var(--color-bg);
    font-size: 0.85rem;
    font-weight: 500;
    line-height: 1.2;
    border-radius: var(--radius-sm);
    white-space: nowrap;
    box-shadow:
        0 6px 12px rgba(40, 30, 20, 0.18),
        0 20px 40px rgba(40, 30, 20, 0.40);
    opacity: 0;
    pointer-events: none;
    transition: opacity 60ms ease;
    z-index: 5;
}
.event-card__ics:hover::after,
.event-card__ics:focus-visible::after {
    opacity: 1;
}
.event-card:hover {
    /* Lift + scale gives the "picked up" feel; the 3px sage ring is
       what visually marks the active row. We deliberately do NOT layer
       a deep ambient drop-shadow here -- prior versions did, and the
       blur pooled out beneath the card as a smudgy greenish-warm halo
       (the green ring's blur intersecting the warm shadow under the
       card). A single tight contact shadow gives enough elevation
       without the smear. transition-delay 220ms suppresses false
       triggers from fast cursor scrubs; hover-OUT uses the base 0s
       delay and snaps back immediately. */
    transform: translateY(-6px) scale(1.03);
    transition-delay: 220ms;
    box-shadow:
        0 0 0 3px var(--color-primary),
        0 4px 8px rgba(40, 30, 20, 0.16),
        inset 7px 0 0 0 var(--stripe-color);
    z-index: 2;
    position: relative;
}
/* On hover, the persona glyph (left) and the venue thumb (right) both
   get a stronger drop-shadow so they read as "lifted with the card"
   instead of just along for the ride. Returns to base via the
   transitions defined on the base rules. */
.event-card:hover .event-card__glyph--emoji,
.event-card:hover .event-card__glyph--img {
    filter: drop-shadow(0 9px 14px rgba(40, 30, 20, 0.55));
    transition-delay: 220ms;
}
/* Treatment A only: mild lift on the floating tile to match the card's
   hover (the prior 0 22px 44px shadow extended past the card's bottom edge
   and contributed to a smudgy halo). Treatment C needs no thumb hover
   override -- the flush panel lifts with the card and keeps its seam. */
body.cards-thumb-A .event-card:hover .event-card__thumb {
    box-shadow:
        0 0 0 1px rgba(70, 60, 40, 0.22),
        0 4px 8px rgba(40, 30, 20, 0.32);
    transition-delay: 220ms;
}
/* Previously: body.is-scrolling .event-card { pointer-events: none }
   was set during scroll to suppress hover-lift firing on every card
   the cursor passed over. It caused a real bug: once is-scrolling
   cleared, CSS :hover re-engaged but JS mouseenter did NOT re-fire
   (events don't replay), so hover and the map fly-to silently broke
   until the user moved the cursor a pixel. The 220ms transition-delay
   on .event-card:hover above + the 220ms setTimeout in
   home-hover-preview.js's onEnter already filter brief cursor passes
   during fast scrolls -- the pointer-events kill was redundant. */
/* When the user is actively hovering one card, suppress the scroll-sync
   accent outline on any OTHER card that happens to be .is-current --
   otherwise both cards glow at once and the "you're pointing at this
   one" signal is muddled. The is-current outline still shows when no
   card is hovered (i.e., the user is just looking at the list). */
#home-list:has(.event-card:hover) .event-card.is-current:not(:hover) {
    box-shadow:
        0 0 0 1px rgba(70, 60, 40, 0.28),
        var(--shadow-2),
        inset 7px 0 0 0 var(--stripe-color);
}
/* Title / time row flow within the card's natural right padding now
   that .event-card.has-thumb reserves space for the full-height thumb.
   No per-element padding-right hack needed. */
.event-card h3 { margin: 0 0 0.25rem; }
.event-card .time {
    color: var(--color-primary-deep);
    font-weight: 600;
    /* Flex row with wrap so the day and the time-range break as a unit
       instead of mid-word. Each child has white-space: nowrap so neither
       segment can be split internally -- if there isn't room for both
       on one line, the time-range drops to the next line whole.
       column-gap supplies the spacing between segments; the .time__sep
       span sits between them as the visible "·" mark. When the @container
       query below hides the separator, the column-gap still provides
       breathing room so day + time don't collide. */
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 0.4em;
    row-gap: 0;
}
.event-card .time > * { white-space: nowrap; }
/* Plain inline-block so the "·" character renders consistently with the
   surrounding text baseline. column-gap on the parent handles spacing. */
.event-card .time__sep {
    display: inline-block;
}
/* When the card content area gets narrow enough that the day + " · " +
   time-range can no longer share a line, hide the separator so the
   second line starts cleanly at the time (matching the wrapped-state
   mockup). The @container query measures the card's CONTENT box width
   (the card defaults to box-sizing: content-box) -- not the card's
   outer width. A typical time row at 16px font is ~230px wide; we hide
   the separator just before that point so the wrap doesn't ever
   surface the middle-dot orphaned on its own line. 260px gives a tiny
   safety margin for the longest day-name + AM/PM combination. */
@container ecard (max-width: 260px) {
    .event-card .time__sep { display: none; }
}
/* Venue + pill(s) on one row (vertical compression). Wraps to a second line
   only when a long venue or multiple pills don't fit beside each other. */
.event-card__meta-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.25rem 0.6rem;
    /* Match the title's 0.25rem bottom margin so the three card lines
       (title / time / venue) sit at equal vertical gaps. */
    margin: 0.25rem 0 0;
}
.event-card .venue { color: var(--color-muted); margin: 0; }
.event-card .tags { margin: 0; }
/* AI lead-in teaser under the pills: small italic, clamped to 2 lines so the
   card never grows much; always trails with "..." (added in card_lead_in). */
.event-card__lead {
    margin: 0.35rem 0 0;
    font-size: 0.82rem;
    line-height: 1.3;
    color: #6b5d47;
    font-style: italic;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
/* End-of-list nudge shown when the LANDING_CAP render limit hid matches:
   states the honest count and points back up to the filters to narrow,
   instead of fetching more. Sits centered below the last card. */
.list-end-nudge {
    margin: 1.25rem 0 0.5rem;
    padding: 0 1rem;
    text-align: center;
    color: var(--color-muted);
    font-size: 0.95rem;
    line-height: 1.45;
}
.list-end-nudge__count { display: block; }
.list-end-nudge__refine {
    display: inline-block;
    margin-top: 0.3rem;
    color: var(--color-primary-deep);
    font-weight: 600;
    text-decoration: none;
}
.list-end-nudge__refine:hover,
.list-end-nudge__refine:focus-visible { text-decoration: underline; }
/* Generic tag-pill style; used by event cards and popups alike. */
.tag {
    display: inline-block;
    background: var(--color-bg);
    color: var(--color-fg);
    padding: 0.15rem 0.6rem;
    border-radius: var(--radius-pill);
    font-size: 0.85rem;
    margin-right: 0.25rem;
    /* Same warm-tan defined-edge treatment used on chips, cards, and the
       popup image so the pill reads as its own object instead of melting
       into the cream card background. */
    box-shadow: 0 0 0 1px rgba(70, 60, 40, 0.18);
}
/* Card subcategory tags are informational labels only -- no clickable /
   hover variant. (They were briefly wired to filter the feed on click,
   but that hid a hard-to-undo selection wipe behind a tiny pill; the
   filter shortcut lives in Quick Picks + the persona buckets instead.) */
/* Admin-only pencil link to the per-title bucket editor. Renders only
   when {% if user.is_staff %} so non-staff never see it; staff can
   click it during a scroll-through pass to retarget a row's interest
   without leaving the cards view. */
.event-card__admin-edit {
    display: inline-block;
    margin-left: 0.35rem;
    padding: 0.05rem 0.4rem;
    font-size: 0.95rem;
    color: var(--color-muted);
    text-decoration: none;
    border-radius: var(--radius-pill);
    transition: background 120ms ease, color 120ms ease;
}
.event-card__admin-edit:hover,
.event-card__admin-edit:focus-visible {
    background: rgba(70, 60, 40, 0.08);
    color: var(--color-accent);
    outline: none;
}
/* Venue thumbnail anchored to the right side of the card. TWO treatments
   are kept live on purpose so we can flip the whole site between them and
   A/B test later (determinant: does click-through rise when scrolling
   stops). See docs/ux-backlog.md. DO NOT delete either path.
     - Treatment C (DEFAULT, below): full-bleed photo panel flush to the
       card's right edge, full height, with a 1px inset seam where it
       meets the cream body.
     - Treatment A (opt-in): floating inset tile with a cream gutter and a
       lift shadow. To switch site-wide to A, add `cards-thumb-A` to the
       <body> tag (templates/base.html:26); the desktop-scoped
       body.cards-thumb-A rules further below then take over.
   The mobile @media block in home-discovery.css pins the compact 48px
   corner tile independently, so the treatment choice is desktop-only.
   pointer-events:none so clicks pass through to the popup-open handler. */
.event-card__thumb {
    position: absolute;
    width: 110px;
    overflow: hidden;
    background: var(--color-bg);
    transition: box-shadow 220ms ease;
    pointer-events: none;
    /* Treatment C: flush to the right edge, top to bottom. Square on the
       inner seam, rounded only on the card's outer corners. The 1px inset
       seam keeps a defined edge where photo meets cream body. */
    right: 0;
    top: 0;
    bottom: 0;
    border-radius: 0 var(--radius-md) var(--radius-md) 0;
    box-shadow: inset 1px 0 0 0 rgba(70, 60, 40, 0.30);
}
/* Treatment A (opt-in via body.cards-thumb-A). Desktop-scoped so it never
   competes with the mobile compact-tile geometry. 1px ring keeps the
   warm-tan edge; a tight contact shadow plus a larger soft ambient shadow
   lift the tile off the card so it reads as a destination tile. */
@media (min-width: 641px) {
    body.cards-thumb-A .event-card__thumb {
        right: 1rem;
        top: 1rem;
        bottom: 1rem;
        border-radius: 6px;
        box-shadow:
            0 0 0 1px rgba(70, 60, 40, 0.18),
            0 2px 4px rgba(40, 30, 20, 0.30),
            0 10px 22px rgba(40, 30, 20, 0.45);
    }
}
.event-card__thumb-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
}
/* Hover affordance: a dark scrim fades in over the bottom of the thumb
   and a "Details ->" label slides up on top of it, telling the user the
   card opens a rich detail popover. Both are decorative and let clicks
   pass through to the card (the popover open handler lives in
   home-popup.js). Timing matches the card's own hover lift: a 220ms
   delay on hover-in suppresses flicker during fast scroll-scrubs (same
   reason as .event-card:hover), instant on hover-out. */
.event-card__thumb-scrim {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 70%;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0));
    opacity: 0;
    transition: opacity 160ms ease;
    pointer-events: none;
}
.event-card__details-cue {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0.45rem;
    text-align: center;
    color: #fff;
    font-size: 0.8rem;
    font-weight: 800;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 160ms ease, transform 160ms ease;
    pointer-events: none;
}
.event-card:hover .event-card__thumb-scrim,
.event-card:hover .event-card__details-cue {
    opacity: 1;
    transition-delay: 220ms;
}
.event-card:hover .event-card__details-cue {
    transform: translateY(0);
}
/* Touch / no-hover devices have no hover to trigger the reveal, so show
   the scrim + label persistently instead - the cue is always visible on
   phones and tablets. */
@media (hover: none) {
    .event-card__thumb-scrim,
    .event-card__details-cue {
        opacity: 1;
        transform: none;
        transition: none;
    }
}
/* Respect reduced-motion: keep the opacity fade, drop the slide. */
@media (prefers-reduced-motion: reduce) {
    .event-card__details-cue {
        transform: none;
        transition: opacity 160ms ease;
    }
    .event-card:hover .event-card__details-cue {
        transform: none;
    }
}
/* Cards with a thumb reserve right-edge space so title / time / venue /
   tags all stay clear of the photo. Cards without a thumb keep their
   default right padding. */
.event-card.has-thumb {
    padding-right: calc(110px + 1.5rem);
}
/* Cancelled treatment: card desaturates + title/time strike through +
   a bold rotated "CANCELLED" stamp diagonally across the card body.
   Combination makes cancellation unmistakable at a glance without
   having to find the inline label. 25deg (not 45) so the stamp reads
   as "marked" rather than "voided / rejected" -- gentler at card
   scale. The legacy inline .cancelled-label is hidden since the stamp
   carries the message. */
/* No `opacity` on the card itself -- it would cascade into the
   ::after CANCELLED stamp and visibly drain the red. The "muted"
   feel comes from desaturating the venue thumbnail + dimmed text
   colors instead. */
.event-card.is-cancelled .event-card__thumb {
    filter: saturate(0.4);
    opacity: 0.65;
}
.event-card.is-cancelled h3 a,
.event-card.is-cancelled .time,
.event-card.is-cancelled .venue,
.event-card.is-cancelled .venue a,
.event-card.is-cancelled .tag {
    color: var(--color-muted);
}
.event-card.is-cancelled h3,
.event-card.is-cancelled .time {
    text-decoration: line-through;
    text-decoration-color: var(--color-cancelled);
    text-decoration-thickness: 2px;
}
.event-card.is-cancelled::after {
    content: "CANCELLED";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(-25deg);
    font-size: clamp(1.8rem, 4.2vw, 2.4rem);
    font-weight: 900;
    color: var(--color-cancelled);
    /* The parent doesn't dim the stamp anymore, so a small intentional
       opacity dial brings the red back from "shouting" to "marked". */
    opacity: 0.78;
    letter-spacing: 0.18em;
    pointer-events: none;
    /* Top-most on the card so the stamp reads over the venue thumbnail
       (z=0) AND the date-tile ICS download in the corner (z=2). The
       ICS link still clicks because the stamp has pointer-events: none. */
    z-index: 3;
    text-shadow: 0 1px 2px rgba(246, 243, 237, 0.6),
                 0 0 1px rgba(166, 69, 53, 0.5);
    white-space: nowrap;
}
.event-card.is-cancelled .cancelled-label { display: none; }
/* One-shot bounce on the persona glyph when an event card is hovered.
   Fires once per hover-enter (no infinite loop), 220ms delay matches
   the .event-card:hover transition-delay so the bounce starts in sync
   with the card's lift. Gated to (hover: hover) so it doesn't run on
   touch devices that briefly fake hover on tap, and respects
   prefers-reduced-motion. */
@keyframes event-card-glyph-bounce {
    0%   { transform: translateY(0)    scale(1); }
    25%  { transform: translateY(-12px) scale(1.1); }
    50%  { transform: translateY(0)    scale(1); }
    72%  { transform: translateY(-5px) scale(1.04); }
    100% { transform: translateY(0)    scale(1); }
}
/* Persona-specific glyph motions on hover. Each persona has its own
   vocabulary so the same card always animates the same way (intentional,
   not random). The standard vertical bounce above is the fallback for
   personas that don't have a special variant. Ball-bounce sits last so
   ball subcategories beat any persona variant via source order. */
@keyframes event-card-glyph-sway {
    /* Dance: hip-sway side to side with a matching tilt. */
    0%   { transform: translateX(0)    rotate(0); }
    25%  { transform: translateX(-5px) rotate(-7deg); }
    50%  { transform: translateX(0)    rotate(0); }
    75%  { transform: translateX(5px)  rotate(7deg); }
    100% { transform: translateX(0)    rotate(0); }
}
@keyframes event-card-glyph-pulse {
    /* Music / live entertainment: rhythmic scale beats, two on-beats
       then a softer third before resting. */
    0%   { transform: scale(1); }
    20%  { transform: scale(1.20); }
    35%  { transform: scale(1); }
    52%  { transform: scale(1.14); }
    67%  { transform: scale(1); }
    82%  { transform: scale(1.08); }
    100% { transform: scale(1); }
}
@keyframes event-card-glyph-tilt {
    /* Arts & crafts: a thoughtful tilt that swings past zero and
       settles back, like brushing a stroke and admiring the result. */
    0%   { transform: rotate(0)     translateY(0); }
    25%  { transform: rotate(-14deg) translateY(-5px); }
    50%  { transform: rotate(0)     translateY(-2px); }
    75%  { transform: rotate(10deg) translateY(-3px); }
    100% { transform: rotate(0)     translateY(0); }
}
@keyframes event-card-glyph-wave {
    /* Water sports: fluid wave, asymmetric so it reads as a current
       rather than a regular bounce. */
    0%   { transform: translate(0,    0)    rotate(0); }
    25%  { transform: translate(4px,  -4px) rotate(-6deg); }
    50%  { transform: translate(-4px, -2px) rotate(6deg); }
    75%  { transform: translate(4px,  -3px) rotate(-3deg); }
    100% { transform: translate(0,    0)    rotate(0); }
}
@keyframes event-card-glyph-jump {
    /* Active lifestyle: squat anticipation, big spring up, brief
       rebound. Fits the energy of fitness / cardio / martial arts. */
    0%   { transform: translateY(0)    scale(1); }
    15%  { transform: translateY(2px)  scale(0.92); }   /* squat */
    40%  { transform: translateY(-18px) scale(1.10); }  /* peak */
    60%  { transform: translateY(0)    scale(1); }      /* land */
    80%  { transform: translateY(-5px) scale(1.02); }   /* small rebound */
    100% { transform: translateY(0)    scale(1); }
}
@keyframes event-card-glyph-toss-arc {
    /* Lawn games: wind back, pitch forward in an arc, follow through.
       Reads as horseshoes / corn-toss / bean-bag tossing motion. */
    0%   { transform: translate(0,    0)    rotate(0); }
    22%  { transform: translate(-3px, 0)    rotate(-12deg); }  /* wind back */
    52%  { transform: translate(3px,  -10px) rotate(10deg); }  /* peak / release */
    78%  { transform: translate(5px,  -2px) rotate(4deg); }    /* follow through */
    100% { transform: translate(0,    0)    rotate(0); }
}
@keyframes event-card-glyph-nod {
    /* Lifelong learning: gentle forward bobs, like nodding while
       reading or listening to a lecture. Two small nods. */
    0%   { transform: rotate(0)    translateY(0); }
    28%  { transform: rotate(-9deg) translateY(-3px); }
    50%  { transform: rotate(0)    translateY(0); }
    74%  { transform: rotate(-5deg) translateY(-2px); }
    100% { transform: rotate(0)    translateY(0); }
}
@keyframes event-card-glyph-wiggle {
    /* Game room: quick playful shake. Cards, dice, mahjongg tiles --
       the rattle of a snug indoor game-room cluster. Pure rotation
       so it reads as "shake" rather than "bounce". */
    0%   { transform: rotate(0); }
    14%  { transform: rotate(-11deg); }
    28%  { transform: rotate(9deg); }
    42%  { transform: rotate(-7deg); }
    58%  { transform: rotate(5deg); }
    74%  { transform: rotate(-3deg); }
    100% { transform: rotate(0); }
}
@keyframes event-card-glyph-zoom-pulse {
    /* Movie bucket: brief iris-style zoom in/out, like a film
       projector momentarily flickering open wider. */
    0%   { transform: scale(1); }
    30%  { transform: scale(0.82); }
    60%  { transform: scale(1.16); }
    80%  { transform: scale(0.96); }
    100% { transform: scale(1); }
}
/* Ball-themed subcategories get a multi-axis bounce that arcs left and
   right between landings, so the icon reads as a real ball arcing in
   the slot. Lives after the persona keyframes so its hover rule wins
   for cards that are both glyph-ball AND have a persona variant. */
@keyframes event-card-glyph-ball-bounce {
    /* Parabolic arcs: lateral motion continues in the same direction
       on the way up AND the way down (one parabola each), then reverses
       on the next bounce. Goes right -> left -> right -> settles.
       Lateral travel kept modest (~60% of the original throw distance)
       so the eye reads "ball bouncing" without the icon visibly sliding
       around its slot. Vertical heights + scale pops are unchanged. */
    0%   { transform: translate(0,    0)    scale(1); }
    16%  { transform: translate(2px,  -14px) scale(1.10); }  /* apex going right */
    32%  { transform: translate(5px,  0)    scale(1); }      /* lands right */
    50%  { transform: translate(0,    -10px) scale(1.06); }  /* apex going left */
    68%  { transform: translate(-5px, 0)    scale(1); }      /* lands left */
    84%  { transform: translate(-2px, -5px) scale(1.03); }   /* apex going right */
    100% { transform: translate(0,    0)    scale(1); }      /* settles center */
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
    /* Default vertical bounce for any persona without a special variant. */
    .event-card:hover .event-card__glyph {
        animation: event-card-glyph-bounce 540ms ease-out 220ms 1;
    }
    /* Persona variants - same specificity as default; later source wins. */
    .event-card.persona-dance:hover .event-card__glyph {
        animation: event-card-glyph-sway 700ms ease-in-out 220ms 1;
    }
    .event-card.persona-music_lovers:hover .event-card__glyph,
    .event-card.persona-live_entertainment:hover .event-card__glyph {
        animation: event-card-glyph-pulse 540ms ease-out 220ms 1;
    }
    .event-card.persona-arts_crafts:hover .event-card__glyph {
        animation: event-card-glyph-tilt 620ms ease-out 220ms 1;
    }
    .event-card.persona-water_sports:hover .event-card__glyph {
        animation: event-card-glyph-wave 720ms ease-in-out 220ms 1;
    }
    .event-card.persona-active_lifestyle:hover .event-card__glyph {
        animation: event-card-glyph-jump 620ms ease-out 220ms 1;
    }
    .event-card.persona-lawn_games:hover .event-card__glyph {
        animation: event-card-glyph-toss-arc 660ms ease-out 220ms 1;
    }
    .event-card.persona-lifelong_learning:hover .event-card__glyph {
        animation: event-card-glyph-nod 620ms ease-in-out 220ms 1;
    }
    .event-card.persona-game_room:hover .event-card__glyph {
        animation: event-card-glyph-wiggle 500ms ease-out 220ms 1;
    }
    /* Movie bucket overrides its parent persona (live_entertainment ->
       pulse) with the iris zoom. Comes after persona variants in source
       so it wins for cards that are both. */
    .event-card.bucket-movie:hover .event-card__glyph {
        animation: event-card-glyph-zoom-pulse 580ms ease-out 220ms 1;
    }
    /* Ball-bounce wins for any ball subcategory regardless of persona. */
    .event-card.glyph-ball:hover .event-card__glyph {
        animation: event-card-glyph-ball-bounce 900ms ease-out 220ms 1;
    }
}

/* "Pop up, hold, settle back" emphasis used when the user clicks an event
   in a map popup. The card lifts ~12px with a strong drop shadow + sage
   ring, holds briefly so the user can't miss it, then descends. Class is
   removed by the popup handler after the animation completes. The
   `position: relative; z-index: 2` keeps the shadow on top of neighboring
   cards rather than being clipped by them. */
@keyframes event-card-flash {
    0% {
        transform: translateY(0) scale(1);
        box-shadow: var(--shadow-1), inset 7px 0 0 0 var(--stripe-color);
    }
    25%, 75% {
        transform: translateY(-12px) scale(1.02);
        box-shadow: 0 0 0 4px var(--color-primary),
                    0 22px 42px rgba(70, 60, 40, 0.28),
                    inset 7px 0 0 0 var(--stripe-color);
    }
    100% {
        transform: translateY(0) scale(1);
        box-shadow: var(--shadow-1), inset 7px 0 0 0 var(--stripe-color);
    }
}
/* Pre-HTMX-swap "poof" played on every card matching the subcategory
   the user just unchecked via a card-side tag pill. The cards fade
   while shrinking and drifting upward; once the animation finishes,
   HTMX replaces the list and the poofed cards are gone for real. */
.event-card.is-poofing {
    animation: event-card-poof 460ms ease-in forwards;
    pointer-events: none;
}
@keyframes event-card-poof {
    0%   { opacity: 1; transform: scale(1) translateY(0); }
    60%  { opacity: 0.45; transform: scale(0.96) translateY(-6px); }
    100% { opacity: 0; transform: scale(0.84) translateY(-14px); }
}
@media (prefers-reduced-motion: reduce) {
    .event-card.is-poofing { animation: none; opacity: 0; }
}
.event-card.is-flashing {
    position: relative;
    z-index: 2;
    animation: event-card-flash 2.5s ease-in-out;
}

.layout {
    display: grid;
    grid-template-columns: 280px 1fr;
    gap: 2rem;
}
@media (max-width: 800px) {
    .layout { grid-template-columns: 1fr; }
}

.filters label { display: block; margin: 0.5rem 0; }

header h1 a { color: var(--color-fg); text-decoration: none; }

.site-footer {
    margin-top: 3rem;
    padding: 1rem;
    background: transparent;
    border-top: 1px solid var(--color-border);
    color: var(--color-muted);
    text-align: center;
    font-size: 1rem;
}
.site-footer a { color: var(--color-accent); }
/* Breathing room above the "Run a club?" CTA so it isn't crowded against the
   testimonial pill that sits directly above it. */
.site-footer__cta { display: block; margin-top: 1.4rem; margin-bottom: 0.5rem; }
.site-footer__links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.4rem 0.6rem;
    font-size: 0.95rem;
}
/* Footer text reads in a centered, comfortable column even when the footer
   itself spans full width (body.page-home sets the container to max-width:none),
   instead of stretching edge-to-edge. */
.site-footer__cta,
.site-footer__links,
.site-footer__disclaimer,
.site-footer__copyright,
.site-footer__meta,
.site-footer__mission {
    max-width: 46rem;
    margin-inline: auto;
}
/* Legal fine print: the disclaimer + copyright share one quiet register and
   sit together as a single block. */
.site-footer__disclaimer {
    display: block;
    margin-top: 1.1rem;
    font-size: 0.9rem;
    line-height: 1.55;
    color: var(--color-muted);
    opacity: 0.85;
}
.site-footer__copyright {
    display: block;
    margin-top: 0.35rem;
    font-size: 0.9rem;
    line-height: 1.55;
    color: var(--color-muted);
    opacity: 0.85;
}
/* Data-freshness line: same quiet fine-print register as the copyright,
   a notch dimmer so it reads as a passive status note. */
.site-footer__freshness {
    display: block;
    margin-top: 0.25rem;
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--color-muted);
    opacity: 0.75;
}
/* Operator/utility chrome (admin, log out, build id): the most recessive line,
   set apart from the legal text above so it doesn't blur into it. */
.site-footer__meta {
    display: block;
    margin-top: 1rem;
    font-size: 0.8rem;
    letter-spacing: 0.01em;
    color: var(--color-muted);
    opacity: 0.6;
}
.site-footer__badge {
    margin-top: 1rem;
}
.site-footer__badge img {
    height: 64px;
    width: auto;
    vertical-align: middle;
    border: 1px solid rgba(70, 60, 40, 0.28);
    border-radius: var(--radius-sm, 6px);
}
.site-footer__mission {
    margin-block: 1.25rem 0;
    font-size: 1.05rem;
    color: var(--color-muted);
}
.site-footer__mission a {
    color: var(--color-accent);
    font-weight: 700;
}

/* --- Mission / "what we're building" page --- */
.mission {
    max-width: 880px;
    margin: 1.5rem auto 3rem;
    padding: 0 1.1rem;
    color: var(--color-fg);
    line-height: 1.6;
}
.mission__hero {
    margin: 1rem 0 2.5rem;
    text-align: center;
}
.mission__eyebrow {
    margin: 0 0 0.75rem;
    font-size: 0.8rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-accent);
}
.mission__headline {
    margin: 0 0 1rem;
    font-size: clamp(1.8rem, 4.5vw, 2.6rem);
    line-height: 1.15;
    color: var(--color-primary-deep);
}
.mission__lede {
    margin: 0 auto;
    max-width: 44rem;
    font-size: 1.15rem;
    color: var(--color-muted);
}
.mission__section {
    margin-top: 2.5rem;
}
.mission__section h2 {
    font-size: 1.45rem;
    color: var(--color-primary-deep);
    margin: 0 0 0.6rem;
}
.mission__big {
    font-size: 1.35rem;
    margin: 0 0 0.5rem;
}
.mission__nots {
    list-style: none;
    padding: 0;
    margin: 0.75rem 0;
}
.mission__nots li {
    position: relative;
    padding-left: 1.5rem;
    margin: 0.3rem 0;
    color: var(--color-fg);
}
.mission__nots li::before {
    content: "\00d7"; /* multiplication sign as a "no" mark */
    position: absolute;
    left: 0;
    color: var(--color-cancelled);
    font-weight: 700;
}
.mission__features {
    margin: 0.75rem 0;
    padding-left: 1.2rem;
}
.mission__features li {
    margin: 0.5rem 0;
}
.mission__wins {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 1rem;
    margin-top: 1.25rem;
}
.mission__win {
    background: var(--color-surface);
    border: 1px solid rgba(70, 60, 40, 0.22);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-2);
    padding: 1.1rem 1.15rem;
}
.mission__win h3 {
    margin: 0 0 0.4rem;
    font-size: 1.1rem;
    color: var(--color-primary-deep);
}
.mission__win p {
    margin: 0;
    font-size: 0.97rem;
    color: var(--color-muted);
}
.mission__cta {
    margin-top: 2.75rem;
    background: var(--color-surface);
    border: 1px solid rgba(70, 60, 40, 0.22);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    padding: 1.5rem 1.5rem 1.75rem;
}
.mission__cta-action {
    margin: 1.1rem 0 0;
}
.mission__cta-action a {
    display: inline-block;
    background: var(--color-accent);
    color: #fff;
    font-weight: 700;
    padding: 0.7rem 1.25rem;
    border-radius: var(--radius-sm);
    text-decoration: none;
    border: 1px solid rgba(70, 60, 40, 0.28);
    box-shadow: var(--shadow-1);
}
.mission__cta-action a:hover {
    background: #9a6648;
}
.mission__footnote {
    margin: 2.5rem auto 0;
    max-width: 40rem;
    text-align: center;
    font-size: 0.9rem;
    color: var(--color-muted);
    opacity: 0.9;
}

/* --- Static informational pages (about / privacy / terms / contact) --- */
body.page-static main {
    /* These pages don't use the home discovery layout, so the flex column
       isn't doing useful work here. Reset it so plain text can flow. */
    display: block;
}
.static-page {
    max-width: 720px;
    margin: 1.5rem auto;
    padding: 0 1rem;
    color: var(--color-fg);
    line-height: 1.55;
}
.static-page h1 {
    font-size: 1.85rem;
    margin: 0 0 0.5rem;
}
.static-page h2 {
    font-size: 1.2rem;
    margin: 1.75rem 0 0.5rem;
    color: var(--color-primary-deep);
}
.static-page h3 {
    font-size: 1rem;
    margin: 1.2rem 0 0.35rem;
    color: var(--color-fg);
}
.static-page p,
.static-page ul {
    margin: 0.5rem 0 0.8rem;
}
.static-page ul { padding-left: 1.4rem; }
.static-page li { margin: 0.2rem 0; }
.static-page code {
    background: rgba(70, 60, 40, 0.08);
    padding: 0.05em 0.35em;
    border-radius: var(--radius-sm);
    font-size: 0.9em;
}
.static-page__meta {
    color: var(--color-muted);
    font-size: 0.85rem;
    margin: -0.25rem 0 1.25rem;
}

/* --- Venue detail page --- */
body.page-venue main { display: block; }
.venue-detail { max-width: 960px; margin: 0 auto; }

.venue-detail__back {
    display: inline-block;
    margin-bottom: 0.5rem;
    color: var(--color-muted);
    font-size: 0.95rem;
    text-decoration: none;
}
.venue-detail__back:hover { color: var(--color-primary-deep); text-decoration: underline; }

.venue-detail__name {
    font-size: 2.25rem;
    margin: 0 0 1rem;
    letter-spacing: -0.01em;
}

.venue-detail__hero {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    margin-bottom: 1.5rem;
}

.venue-detail__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1.25rem;
    margin-bottom: 2rem;
}

.venue-card {
    background: var(--color-surface);
    border-radius: var(--radius-md);
    padding: 1.25rem 1.5rem;
    box-shadow: var(--shadow-1);
}
.venue-card__title {
    margin: 0 0 0.85rem;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-primary-deep);
    font-weight: 700;
}
.venue-card__description {
    margin: 1rem 0 0;
    padding-top: 0.85rem;
    border-top: 1px solid var(--color-border);
    color: var(--color-fg);
    line-height: 1.55;
}

.venue-fact {
    display: flex;
    gap: 0.75rem;
    align-items: baseline;
    padding: 0.45rem 0;
    border-bottom: 1px dashed var(--color-border);
}
.venue-fact:last-of-type { border-bottom: 0; padding-bottom: 0.25rem; }
.venue-fact__label {
    flex: 0 0 4.5rem;
    color: var(--color-muted);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.venue-fact__value { font-size: 1rem; }
.venue-fact__value a { font-weight: 600; }
.venue-fact__directions {
    display: inline-block;
    margin-top: 4px;
    margin-left: 8px;
    color: #c9722a;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
}
.venue-fact__directions:hover { text-decoration: underline; }

.venue-hours {
    border-collapse: collapse;
    width: 100%;
    font-size: 0.95rem;
}
.venue-hours th,
.venue-hours td {
    padding: 0.4rem 0.5rem;
    text-align: left;
    border-bottom: 1px dashed var(--color-border);
    font-weight: 400;
}
.venue-hours tr:last-child th,
.venue-hours tr:last-child td { border-bottom: 0; }
.venue-hours th {
    color: var(--color-muted);
    font-weight: 500;
    width: 7.5rem;
}
.venue-hours tr.is-today th,
.venue-hours tr.is-today td {
    background: rgba(125, 150, 112, 0.12);
    color: var(--color-fg);
    font-weight: 700;
}
.venue-hours tr.is-today th { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.venue-hours tr.is-today td { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.venue-hours__today-pill {
    display: inline-block;
    margin-left: 0.4rem;
    padding: 0.05rem 0.45rem;
    background: var(--color-primary);
    color: white;
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    border-radius: var(--radius-pill);
    vertical-align: middle;
}

.venue-detail__section-title {
    font-size: 1.15rem;
    margin: 0.5rem 0 1rem;
    color: var(--color-fg);
}
/* Full-bleed variant of .venue-card, used for the "top activities" block
   that sits below the two-column grid. */
.venue-card--full { margin-bottom: 1.5rem; }

.venue-activity-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    list-style: none;
    margin: 0;
    padding: 0;
}
.venue-activity {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.3rem 0.55rem 0.3rem 0.75rem;
    background: var(--color-bg);
    border-radius: var(--radius-pill);
    font-size: 0.95rem;
}
.venue-activity__count {
    display: inline-block;
    min-width: 1.5rem;
    padding: 0.05rem 0.45rem;
    background: var(--color-primary);
    color: white;
    border-radius: var(--radius-pill);
    font-size: 0.78rem;
    font-weight: 700;
    text-align: center;
    line-height: 1.3;
}

/* "Venues like this one" -- recommendation strip. Each item is a small
   card: thumbnail on the left, name + the 1-2 shared activity names
   below. Grid auto-fits so the strip wraps cleanly on narrow widths. */
.venue-similar-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 0.75rem;
}
.venue-similar {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem;
    border-radius: var(--radius-md);
    text-decoration: none;
    color: var(--color-fg);
    background: var(--color-bg);
    transition: background-color 120ms ease, transform 120ms ease;
}
.venue-similar:hover {
    background: rgba(125, 150, 112, 0.14);
    transform: translateY(-1px);
}
.venue-similar__image {
    flex: 0 0 auto;
    width: 56px;
    height: 56px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-1);
}
.venue-similar__image--placeholder {
    background:
        linear-gradient(135deg, var(--color-border), var(--color-bg));
}
.venue-similar__body {
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.venue-similar__name {
    font-weight: 700;
    color: var(--color-primary-deep);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.venue-similar__shared {
    color: var(--color-muted);
    font-size: 0.82rem;
    margin-top: 0.1rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --- Venues browse page: photo grid of every venue --------------- */
.venues-page {
    max-width: 1100px;
    margin: 0 auto;
    padding: 1.5rem 1rem 3rem;
}
.venues-page__head {
    text-align: center;
    margin-bottom: 1.5rem;
}
.venues-page__intro {
    color: var(--color-muted);
    max-width: 40rem;
    margin: 0.5rem auto 0;
}
.venues-grid {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 1rem;
}
.venue-tile__link {
    display: flex;
    flex-direction: column;
    height: 100%;
    text-decoration: none;
    color: var(--color-fg);
    border-radius: var(--radius-md);
    background: var(--color-bg);
    /* Warm-shadow edge on the card (house style for cards/popup images). */
    box-shadow: var(--shadow-1), 0 0 0 1px rgba(70, 60, 40, 0.18);
    overflow: hidden;
    transition: transform 120ms ease, box-shadow 120ms ease;
}
.venue-tile__link:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-2), 0 0 0 1px rgba(70, 60, 40, 0.28);
}
.venue-tile__thumb {
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-bottom: 1px solid rgba(70, 60, 40, 0.18);
}
.venue-tile__thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.venue-tile__name {
    padding: 0.6rem 0.7rem;
    font-weight: 700;
    color: var(--color-primary-deep);
}
.venue-tile--empty {
    color: var(--color-muted);
}

/* --- Error pages (404 / 500 / 403) -------------------------------- */
.error-page {
    max-width: 36rem;
    margin: 4rem auto;
    padding: 0 1.5rem;
    text-align: center;
}
.error-page__code {
    font-size: clamp(6rem, 18vw, 12rem);
    line-height: 0.9;
    font-weight: 700;
    margin: 0 0 0.5rem;
    color: var(--color-accent);
    /* Subtle warm shadow so the big number reads as raised rather than
       flat -- consistent with the chip / popup edge treatment elsewhere. */
    text-shadow: 0 4px 0 rgba(70, 60, 40, 0.08),
                 0 14px 30px rgba(70, 60, 40, 0.18);
    letter-spacing: -0.04em;
}
.error-page__gag {
    font-size: clamp(1.25rem, 3vw, 1.625rem);
    line-height: 1.35;
    font-weight: 400;
    margin: 0 0 2rem;
    color: var(--color-fg);
}
.error-page__back {
    margin: 0;
    font-size: 1rem;
    color: var(--color-muted);
}
.error-page__back a {
    color: var(--color-primary-deep);
}

/* --- Connectivity banner ----------------------------------------- */
/* Sticky at top of the viewport so it stays visible while the user
   scrolls. Clay-red ground + warm-shadow edge ring per the chip/card
   family so it reads as site chrome, not a foreign alert. */
.connectivity-banner {
    position: sticky;
    top: 0;
    z-index: 100;
    background: var(--color-cancelled);
    color: white;
    padding: 0.6rem 1rem;
    text-align: center;
    font-size: 0.95rem;
    font-weight: 600;
    box-shadow: 0 0 0 1px rgba(70, 60, 40, 0.28), var(--shadow-2);
}

