:root {
  --green: #1f5e3b; --green-dark: #17482d; --red: #b3312d; --cream: #fbf7f0;
  --ink: #23272a; --muted: #6b6f73; --line: #e6e0d6; --card: #ffffff;
  /* The page behind the cards. Dark enough that white snow and lit bulbs
     both read against it. --cream stays what it was: it is still the chip
     and hover colour INSIDE the white cards, so it must not follow. */
  --page: #1b2430; --page-ink: #eef2f6; --page-muted: #a7b4c2;
}
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--page-ink); background: var(--page); line-height: 1.5; }
/* Light is now the DEFAULT, because headings, .lead and a back-link sit
   directly on the dark page on several templates. Anything on a white
   surface switches back to dark below. */
a { color: #86cfa3; }
.container { max-width: 760px; margin: 0 auto; padding: 0 1rem; }
.topbar { background: var(--green); color: #fff; }
.topbar nav { display: flex; align-items: center; gap: 1rem; min-height: 56px; flex-wrap: wrap; }
.topbar a { color: #fff; text-decoration: none; }
.topbar a:hover { text-decoration: underline; }
.brand { font-weight: 700; font-size: 1.1rem; margin-right: auto; }
.menu { display: flex; align-items: center; gap: 1rem; }
.menu form { display: inline; }
.menu-toggle { display: none; background: none; border: 1px solid rgba(255,255,255,.5); color: #fff; border-radius: 6px;
               min-width: 44px; min-height: 44px; font-size: 1.25rem; cursor: pointer; }
main { padding: 1.5rem 1rem 3rem; }
h1 { font-size: 1.75rem; margin: 0 0 .5rem; }
h2 { font-size: 1.2rem; margin: 0 0 .5rem; }
.lead { color: var(--page-muted); margin-top: 0; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 1.25rem; margin: 1rem 0;
        color: var(--ink); }
.card a { color: var(--green); }
.card .lead, .card .muted { color: var(--page-muted); }
.card.highlight { border-color: var(--red); border-width: 2px; }
.btn { display: inline-block; padding: .5rem 1rem; border: 1px solid var(--green); border-radius: 6px; background: var(--green); color: #fff; text-decoration: none; cursor: pointer; font-size: 1rem; font-family: inherit; }
.btn:hover { background: var(--green-dark); }
.btn-secondary { background: #fff; color: var(--green); }
.btn-secondary:hover { background: var(--cream); }
.btn-danger { background: #fff; color: var(--red); border-color: var(--red); }
.btn-danger:hover { background: #fbeeed; }
.btn-google { background: #fff; color: #222; border-color: #ccc; font-weight: 600; }
.btn-google:hover { background: #f4f4f4; }
.btn-link { background: none; border: none; color: #fff; padding: 0; text-decoration: underline; cursor: pointer; font-size: 1rem; font-family: inherit; }
.btn-sm { padding: .35rem .75rem; font-size: .9rem; }
form.inline { display: inline; }
.messages { list-style: none; padding: .75rem 1rem; margin: 0 0 1rem; background: #fff8e1; border: 1px solid #f0dca0; border-radius: 8px;
            color: var(--ink); }
.messages a { color: var(--green); }
.messages .error { color: var(--red); }
.muted { color: var(--page-muted); }
.small { font-size: .9rem; }
.reveal-box { text-align: center; padding: 2rem 1rem; }
.reveal-name { font-size: 2rem; font-weight: 700; color: var(--red); margin: .5rem 0; }
.items { list-style: none; padding: 0; margin: 0; }
.items li { padding: .75rem 0; border-top: 1px solid var(--line); display: flex; gap: 1rem; align-items: flex-start; }
.items li:first-child { border-top: none; }
.items .body { flex: 1; }
.items .title { font-weight: 600; }
.items .meta { color: var(--muted); font-size: .9rem; }
.items .actions { display: flex; gap: .5rem; white-space: nowrap; }
.links { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .4rem; }
.chip { display: inline-block; padding: .2rem .6rem; border: 1px solid var(--line); border-radius: 999px;
        background: var(--cream); color: var(--green); font-size: .85rem; text-decoration: none; max-width: 100%;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip:hover { border-color: var(--green); }
.field { margin-bottom: .75rem; }
.field label { display: block; font-size: .9rem; color: var(--muted); margin-bottom: .2rem; }
.field input, .field textarea { width: 100%; padding: .5rem .6rem; border: 1px solid #cfc8bc; border-radius: 6px; font: inherit; }
.errorlist { color: var(--red); list-style: none; padding: 0; margin: .2rem 0 0; font-size: .9rem; }
.members { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: .75rem; }
.members li { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: .9rem 1rem;
              color: var(--ink); }
.members li a { color: var(--green); }
.members li a { font-weight: 600; text-decoration: none; }
.badge { display: inline-block; font-size: .75rem; padding: .1rem .5rem; border-radius: 999px; background: #e8f1ec; color: var(--green); margin-left: .3rem; }
.badge.red { background: #fbeeed; color: var(--red); }
hr { border: none; border-top: 1px solid rgba(255,255,255,.14); margin: 1.25rem 0; }
.card hr { border-top-color: var(--line); }
.htmx-indicator { opacity: 0; transition: opacity .2s; }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }
[x-cloak] { display: none !important; }

/* --- Phones ------------------------------------------------------------- */
@media (max-width: 640px) {
  .topbar nav { min-height: 56px; }
  .menu-toggle { display: inline-flex; align-items: center; justify-content: center; }
  /* Collapsed by default; Alpine adds .open. Each entry is a full-width, 48px-tall tap target. */
  .menu { display: none; width: 100%; flex-direction: column; align-items: stretch; gap: 0;
          padding: .25rem 0 .75rem; border-top: 1px solid rgba(255,255,255,.25); }
  .menu.open { display: flex; }
  .menu a, .menu .btn-link { display: flex; align-items: center; min-height: 48px; padding: 0 .25rem;
                             text-decoration: none; width: 100%; text-align: left; }
  .menu form { display: block; }
  .menu .btn { background: none; border: none; }
  h1 { font-size: 1.5rem; }
  .card { padding: 1rem; }
  /* Comfortable tap targets everywhere. */
  .btn { min-height: 44px; padding: .6rem 1.1rem; }
  .btn-sm { min-height: 40px; padding: .5rem .9rem; font-size: .95rem; }
  .field input, .field textarea { padding: .65rem .7rem; }
  /* Item rows: text on top, actions on their own line so titles never get squeezed. */
  .items li { flex-wrap: wrap; }
  .items .actions { width: 100%; justify-content: flex-end; }
  .members { grid-template-columns: 1fr; }
  .reveal-name { font-size: 1.75rem; }
  /* Phone overrides for the festive decoration are NOT here: this block
     sits above those rules, and a media query adds no specificity, so they
     would lose on source order. See the phone block at the end of the
     festive section. */
}

/* === Festive decoration ==================================================
   Everything below here is ornament. It is additive: delete this block and
   the markup hooks in base.html / dashboard.html and the app is unchanged.
   The prefers-reduced-motion block at the very bottom switches all of the
   motion off in one place.
   ======================================================================== */

/* Snow sits BEHIND the content (z-index 0 vs 1) so it shows in the cream
   margins and never drifts across text people are trying to read. */
.topbar, main { position: relative; z-index: 1; }
/* Brick mantel below the top bar, with the garland draped over its front.
   Order: mantel 1, garland 2, snow 3 -- so the rope sits ON the shelf, and
   flakes drift in front of both. */
.mantel { position: relative; z-index: 1; height: 90px;
          background-image: url("../img/brick.de115e00e915.svg");
          background-repeat: repeat; background-size: 120px 44px; }
/* The shelf: a wooden slab across the bottom of the brickwork, with a lit
   top edge and a shadow cast down onto the page. */
.mantel::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 17px;
                 background: linear-gradient(#8a5630 0 3px, #6b3f23 3px, #512f1a);
                 box-shadow: 0 4px 10px -2px rgba(0,0,0,.55); }
/* The garland runs along the TOP of the brickwork, lifted slightly so it
   tucks under the green bar. Absolute inside .mantel, which keeps the
   full-width flex row (and so the fixed 60px bulb pitch) intact. */
.lights { position: absolute; top: -8px; left: 0; right: 0; z-index: 1; }

/* Stockings hung from the shelf, IN FRONT of the garland (z 2 vs 1). The
   hook is on the underside of the shelf and the garland is draped along its
   face, so the stocking passes in front of the rope. Hanging them behind it
   made the rope cut across their cuffs, which read as stockings pinned to
   the garland rather than hung from the mantel. The negative margin lifts
   the loops up under the shelf so they are not left floating. */
.stockings { position: relative; z-index: 2; margin-top: -16px; margin-bottom: 6px;
             display: flex; justify-content: center; align-items: flex-start;
             gap: 10px; padding: 0 1rem; overflow: hidden; }
/* Self-hosted, subset to A-Z only (7KB). See static/fonts/README.md.
   Linking to Google Fonts would be the only third-party request the app
   makes. swap: the initials are decoration, so show the fallback
   immediately rather than leaving blank stockings while a font loads. */
@font-face {
  font-family: "Great Vibes Subset";
  src: url("../fonts/great-vibes-az.ef1534f8dbb7.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+41-5A;
}

/* One variable drives the whole stocking: the artwork is 2:3, and the
   initial is sized from the same number so it cannot outgrow the leg when
   the stocking changes size. */
/* Fluid width: 8 stockings plus gaps always fit, from a 320px phone up,
   so the row can never overrun and get clipped at an awkward window size.
   clamp caps it at 60px so they do not become comical on a wide screen. */
.stocking { --w: clamp(32px, 6.5vw, 60px);
            position: relative; display: block; flex: 0 0 auto;
            width: var(--w); height: calc(var(--w) * 1.5);
            font-size: calc(var(--w) * 0.34);
            transform-origin: 50% 4px; animation: sway 5s ease-in-out infinite;
            background: url("../img/stocking.9535cd34955d.svg") no-repeat center / contain;
            filter: drop-shadow(0 4px 5px rgba(0,0,0,.45)); }
/* The initial sits on the leg, which is not centred in the artwork: in the
   SVG's 48x72 viewBox the leg runs down x=26, so 54%/44% rather than dead
   centre. Percentages keep it placed when the stocking shrinks on phones. */
/* The leg runs down x=26 of the artwork's 48-wide viewBox, so 54%/44%
   rather than dead centre. Percentages and the inherited em keep the letter
   in place and in proportion at any stocking size. */
.stocking-initial { position: absolute; left: 54%; top: 43%; transform: translate(-50%, -50%);
                    font-family: "Great Vibes Subset", "Snell Roundhand", "Apple Chancery",
                                 "Segoe Script", "Brush Script MT", cursive;
                    font-size: 1em; line-height: 1; color: #fff;
                    text-shadow: 0 1px 2px rgba(0,0,0,.45); }
/* Barely-there sway, staggered so they do not move as one block. */
@keyframes sway { 0%, 100% { transform: rotate(-1.4deg); } 50% { transform: rotate(1.4deg); } }
.stocking:nth-child(3n+1) { animation-duration: 5.4s; animation-delay: -1.1s; }
.stocking:nth-child(3n+2) { animation-duration: 6.2s; animation-delay: -3.4s; }
.stocking:nth-child(3n+3) { animation-duration: 4.8s; animation-delay: -2.2s; }

/* z-index 3 puts snow IN FRONT of the cards. Behind them it was invisible:
   white flakes on a cream background, hidden by opaque white cards. The ring
   and glow give each flake an edge so it reads against white card and cream
   page alike, without making it a grey dot. */
.snow { position: fixed; inset: 0; z-index: 3; overflow: hidden; pointer-events: none; }
.snow span { position: absolute; top: -14px; display: block; border-radius: 50%;
             background: #fff; opacity: .85; animation: snowfall linear infinite;
             box-shadow: 0 0 0 1px rgba(86,120,156,.38), 0 0 5px rgba(86,120,156,.3); }
@keyframes snowfall {
  0%   { transform: translate3d(0, -14px, 0) rotate(0deg); }
  100% { transform: translate3d(14px, 104vh, 0) rotate(260deg); }
}
/* Varied size, speed, delay and position, so it never reads as a loop. */
.snow span:nth-child(1)  { left:  4%; width: 7px; height: 7px; animation-duration: 15s; animation-delay:  0s; }
.snow span:nth-child(2)  { left: 12%; width: 4px; height: 4px; animation-duration: 21s; animation-delay: -4s; opacity: .55; }
.snow span:nth-child(3)  { left: 20%; width: 9px; height: 9px; animation-duration: 13s; animation-delay: -9s; }
.snow span:nth-child(4)  { left: 27%; width: 5px; height: 5px; animation-duration: 19s; animation-delay: -2s; opacity: .6; }
.snow span:nth-child(5)  { left: 35%; width: 6px; height: 6px; animation-duration: 17s; animation-delay: -12s; }
.snow span:nth-child(6)  { left: 43%; width: 8px; height: 8px; animation-duration: 23s; animation-delay: -6s; opacity: .5; }
.snow span:nth-child(7)  { left: 51%; width: 4px; height: 4px; animation-duration: 14s; animation-delay: -16s; }
.snow span:nth-child(8)  { left: 59%; width: 7px; height: 7px; animation-duration: 20s; animation-delay: -1s; opacity: .65; }
.snow span:nth-child(9)  { left: 66%; width: 5px; height: 5px; animation-duration: 16s; animation-delay: -11s; }
.snow span:nth-child(10) { left: 74%; width: 9px; height: 9px; animation-duration: 25s; animation-delay: -7s; opacity: .5; }
.snow span:nth-child(11) { left: 82%; width: 6px; height: 6px; animation-duration: 18s; animation-delay: -14s; }
.snow span:nth-child(12) { left: 89%; width: 4px; height: 4px; animation-duration: 22s; animation-delay: -3s; opacity: .6; }
.snow span:nth-child(13) { left: 95%; width: 8px; height: 8px; animation-duration: 15s; animation-delay: -18s; }
.snow span:nth-child(14) { left: 70%; width: 5px; height: 5px; animation-duration: 24s; animation-delay: -21s; opacity: .55; }
.snow span:nth-child(15) { left: 15%; width: 5px; height: 5px; animation-duration: 13s; animation-delay: -13s; opacity: .65; }
.snow span:nth-child(16) { left: 75%; width: 6px; height: 6px; animation-duration: 17s; animation-delay: -22s; opacity: .6; }
.snow span:nth-child(17) { left: 6%; width: 6px; height: 6px; animation-duration: 14s; animation-delay: -4s; opacity: .75; }
.snow span:nth-child(18) { left: 17%; width: 7px; height: 7px; animation-duration: 19s; animation-delay: -8s; opacity: .65; }
.snow span:nth-child(19) { left: 83%; width: 4px; height: 4px; animation-duration: 13s; animation-delay: -13s; opacity: .6; }
.snow span:nth-child(20) { left: 67%; width: 4px; height: 4px; animation-duration: 24s; animation-delay: -13s; opacity: .5; }
.snow span:nth-child(21) { left: 92%; width: 4px; height: 4px; animation-duration: 17s; animation-delay: -21s; opacity: .55; }
.snow span:nth-child(22) { left: 78%; width: 4px; height: 4px; animation-duration: 24s; animation-delay: -16s; opacity: .6; }
.snow span:nth-child(23) { left: 37%; width: 7px; height: 7px; animation-duration: 22s; animation-delay: -3s; opacity: .75; }
.snow span:nth-child(24) { left: 14%; width: 7px; height: 7px; animation-duration: 25s; animation-delay: -12s; opacity: .65; }
.snow span:nth-child(25) { left: 23%; width: 4px; height: 4px; animation-duration: 14s; animation-delay: -15s; opacity: .55; }
.snow span:nth-child(26) { left: 59%; width: 4px; height: 4px; animation-duration: 25s; animation-delay: -12s; opacity: .7; }

/* String lights hanging from the bottom edge of the green top bar.
   --drop is the length of the bulb's stem AND how far it hangs, so one
   custom property drives the whole swag. */
/* The rope is a 180px SVG swag tiled across. Bulbs use a FIXED 60px pitch
   (8px bulb + 52px gap) with a 26px lead-in, so bulb centres land on x = 30,
   90, 150 of every tile regardless of viewport width -- which is why the
   bulbs stay glued to the rope instead of drifting off it as the page
   resizes. Surplus bulbs are simply clipped. */
.lights { display: flex; align-items: flex-start; height: 54px;
          padding-left: 26px; gap: 52px; overflow: hidden;
          background-image: url("../img/garland.c174cb78fe5e.svg");
          background-repeat: repeat-x; background-position: 0 0;
          background-size: 180px 48px; }
.lights span { --drop: 2px; position: relative; flex: 0 0 auto; width: 8px; height: 8px;
               margin-top: var(--drop); border-radius: 50%;
               animation: blink 1.8s ease-in-out infinite; }
/* Short fixed stem: the bulb now hangs off the rope just above it, not off
   the top of the container. */
.lights span::before { content: ""; position: absolute; left: 50%; top: -7px;
                       width: 1px; height: 7px; background: rgba(16,46,24,.6);
                       transform: translateX(-50%); }
/* A seven-bulb cycle makes the wire look like it drapes. */
.lights span:nth-child(3n+1) { --drop: 33px; }  /* x=30  on the tile */
.lights span:nth-child(3n+2) { --drop: 41px; }  /* x=90, the low point   */
.lights span:nth-child(3n+3) { --drop: 33px; }  /* x=150                 */
/* Colours cycle on a different period from the drop, so the two patterns
   do not line up and repeat visibly. */
.lights span:nth-child(5n+1) { background: #e8b84b; box-shadow: 0 0 6px #e8b84b, 0 0 17px 4px rgba(232,184,75,.55); }
.lights span:nth-child(5n+2) { background: #d9534f; box-shadow: 0 0 6px #d9534f, 0 0 17px 4px rgba(217,83,79,.55); }
.lights span:nth-child(5n+3) { background: #4f9d69; box-shadow: 0 0 6px #4f9d69, 0 0 17px 4px rgba(79,157,105,.55); }
.lights span:nth-child(5n+4) { background: #5b8dd9; box-shadow: 0 0 6px #5b8dd9, 0 0 17px 4px rgba(91,141,217,.55); }
.lights span:nth-child(5n+5) { background: #e8e2d4; box-shadow: 0 0 6px #f0e9da, 0 0 17px 4px rgba(240,233,218,.55); }
/* Per-bulb timing. Each bulb gets its own duration and a negative delay, so
   it starts partway through its cycle and the string is already varied on the
   first frame. Because no two durations divide evenly, the bulbs drift apart
   permanently instead of resynchronising into a visible loop.
   Generated once with a fixed seed; nothing random happens at runtime.
   The shortest is 1.35s (~0.74Hz), comfortably under the 3Hz flash threshold
   associated with photosensitive seizures. */
.lights span:nth-child(1) { animation-duration: 1.95s; animation-delay: -0.63s; }
.lights span:nth-child(2) { animation-duration: 2.82s; animation-delay: -1.58s; }
.lights span:nth-child(3) { animation-duration: 1.88s; animation-delay: -0.01s; }
.lights span:nth-child(4) { animation-duration: 1.55s; animation-delay: -0.49s; }
.lights span:nth-child(5) { animation-duration: 1.99s; animation-delay: -0.62s; }
.lights span:nth-child(6) { animation-duration: 2.36s; animation-delay: -0.98s; }
.lights span:nth-child(7) { animation-duration: 2.3s; animation-delay: -0.15s; }
.lights span:nth-child(8) { animation-duration: 2.76s; animation-delay: -1.92s; }
.lights span:nth-child(9) { animation-duration: 2.47s; animation-delay: -1.97s; }
.lights span:nth-child(10) { animation-duration: 2.4s; animation-delay: -1.42s; }
.lights span:nth-child(11) { animation-duration: 2.59s; animation-delay: -0.13s; }
.lights span:nth-child(12) { animation-duration: 2.06s; animation-delay: -0.02s; }
.lights span:nth-child(13) { animation-duration: 2.05s; animation-delay: -0.74s; }
.lights span:nth-child(14) { animation-duration: 2.86s; animation-delay: -2.46s; }
.lights span:nth-child(15) { animation-duration: 1.99s; animation-delay: -0.43s; }
.lights span:nth-child(16) { animation-duration: 1.85s; animation-delay: -1.69s; }
.lights span:nth-child(17) { animation-duration: 1.44s; animation-delay: -0.83s; }
.lights span:nth-child(18) { animation-duration: 1.77s; animation-delay: -0.33s; }
.lights span:nth-child(19) { animation-duration: 1.96s; animation-delay: -1.54s; }
.lights span:nth-child(20) { animation-duration: 1.8s; animation-delay: -1.14s; }
.lights span:nth-child(21) { animation-duration: 1.76s; animation-delay: -1.59s; }
.lights span:nth-child(22) { animation-duration: 1.91s; animation-delay: -1.29s; }
.lights span:nth-child(23) { animation-duration: 1.61s; animation-delay: -0.6s; }
.lights span:nth-child(24) { animation-duration: 1.79s; animation-delay: -1.21s; }
.lights span:nth-child(25) { animation-duration: 1.55s; animation-delay: -1.41s; }
.lights span:nth-child(26) { animation-duration: 2.49s; animation-delay: -2.46s; }
.lights span:nth-child(27) { animation-duration: 2.8s; animation-delay: -1.09s; }
.lights span:nth-child(28) { animation-duration: 1.41s; animation-delay: -0.42s; }
.lights span:nth-child(29) { animation-duration: 1.38s; animation-delay: -0.85s; }
.lights span:nth-child(30) { animation-duration: 2.02s; animation-delay: -0.98s; }
.lights span:nth-child(31) { animation-duration: 2.57s; animation-delay: -1.28s; }
.lights span:nth-child(32) { animation-duration: 1.36s; animation-delay: -1.35s; }
.lights span:nth-child(33) { animation-duration: 2.91s; animation-delay: -1.9s; }
.lights span:nth-child(34) { animation-duration: 2.93s; animation-delay: -1.03s; }
.lights span:nth-child(35) { animation-duration: 1.46s; animation-delay: -1.02s; }
.lights span:nth-child(36) { animation-duration: 2.5s; animation-delay: -1.7s; }
.lights span:nth-child(37) { animation-duration: 1.86s; animation-delay: -0.71s; }
.lights span:nth-child(38) { animation-duration: 2.0s; animation-delay: -1.01s; }
.lights span:nth-child(39) { animation-duration: 1.86s; animation-delay: -0.65s; }
.lights span:nth-child(40) { animation-duration: 1.89s; animation-delay: -0.13s; }
@keyframes blink {
  0%,  44%  { opacity: 1; }
  50%, 94%  { opacity: .16; }
  100%      { opacity: 1; }
}

/* The gift box that replaces "Tap to reveal". */
.giftbox { position: relative; display: block; width: 128px; height: 112px;
           margin: .75rem auto .25rem; padding: 0; border: 0; background: none; cursor: pointer; }
.giftbox:focus-visible { outline: 3px solid var(--green); outline-offset: 6px; border-radius: 8px; }
.gift-body { position: absolute; left: 8px; right: 8px; bottom: 0; height: 74px;
             background: var(--red); border-radius: 4px; }
.gift-body::before { content: ""; position: absolute; left: 50%; top: 0; bottom: 0; width: 20px;
                     margin-left: -10px; background: #e8b84b; }
.gift-lid { position: absolute; left: 0; right: 0; top: 22px; height: 26px;
            background: #c9403b; border-radius: 4px;
            transition: transform .5s cubic-bezier(.34, -0.3, .45, 1.3), opacity .35s ease .18s; }
.gift-lid::before { content: ""; position: absolute; left: 50%; top: 0; bottom: 0; width: 20px;
                    margin-left: -10px; background: #e8b84b; }
/* Two loops of ribbon sitting on the lid. */
.gift-bow::before, .gift-bow::after { content: ""; position: absolute; top: -17px; width: 22px;
                                      height: 20px; border: 4px solid #e8b84b; border-radius: 50%;
                                      background: none; }
.gift-bow::before { left: 50%; margin-left: -23px; transform: rotate(-16deg); }
.gift-bow::after  { left: 50%; margin-left:   1px; transform: rotate( 16deg); }
.giftbox:hover:not(.open) { animation: nudge .6s ease-in-out infinite; }
.giftbox.open .gift-lid { transform: translateY(-58px) rotate(-14deg); opacity: 0; }
.giftbox.open .gift-body { animation: squash .5s ease; }
@keyframes nudge  { 0%, 100% { transform: rotate(-2deg); } 50% { transform: rotate(2deg); } }
@keyframes squash { 0% { transform: scaleY(1); } 35% { transform: scaleY(.93); } 100% { transform: scaleY(1); } }

.confetti-canvas { position: fixed; inset: 0; z-index: 60; pointer-events: none; }

/* Quiet inline button, for "Hide again" next to muted text. (.btn-link is
   white, for use on the green top bar, so it is invisible here.) */
.btn-quiet { background: none; border: 0; padding: 0 0 0 .25rem; font: inherit; font-size: .9rem;
             color: var(--muted); text-decoration: underline; cursor: pointer; }
.btn-quiet:hover { color: var(--ink); }

/* The name itself, once the lid is off. */
.reveal-name { animation: pop .55s cubic-bezier(.2, 1.5, .4, 1) both; }
@keyframes pop { 0% { transform: scale(.4); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }

/* Quiet polish elsewhere. */
.card { box-shadow: 0 1px 2px rgba(35,39,42,.04), 0 10px 20px -16px rgba(35,39,42,.35); }
.members li { transition: transform .15s ease, box-shadow .15s ease; }
.members li:hover { transform: translateY(-2px); box-shadow: 0 10px 18px -12px rgba(35,39,42,.45); }
.chip { transition: transform .12s ease, border-color .12s ease; }
.chip:hover { transform: translateY(-1px); }
.btn { transition: background .15s ease, transform .08s ease; }
.btn:active { transform: translateY(1px); }
/* htmx tags freshly swapped-in nodes, so new wish list items arrive gently. */
.items li.htmx-added { animation: slide-in .35s ease both; }
@keyframes slide-in { 0% { opacity: 0; transform: translateY(-6px); } 100% { opacity: 1; transform: none; } }

/* --- Festive decoration on phones ---------------------------------------
   This has to live AFTER the rules above, not in the phone block near the
   top of the file: media queries carry no extra specificity, so whichever
   comes last wins. Putting it up there left eight stockings rendering at
   full size and quietly clipped by the row's overflow. */
@media (max-width: 640px) {
  .mantel { height: 76px; }
  /* --w is fluid, so only the spacing needs adjusting down here. */
  .stockings { gap: 4px; margin-top: -14px; padding: 0 .5rem; }
}

/* --- Motion off ---------------------------------------------------------
   One switch for everything above. Moving decoration that cannot be turned
   off is an accessibility problem, not a flourish. */
@media (prefers-reduced-motion: reduce) {
  .snow { display: none; }
  .lights span { animation: none; }
  .giftbox:hover:not(.open) { animation: none; }
  .stocking { animation: none; }
  .gift-lid { transition: none; }
  .reveal-name, .items li.htmx-added, .giftbox.open .gift-body { animation: none; }
  .members li, .chip, .btn { transition: none; }
}
