/* ==========================================================================
   Keiki Board: shared styles
   ------------------------------------------------------------------------
   Design notes (for whoever edits this next):

   Loosely structured after a borrowed base (topbar, card, pill, and button
   patterns), recolored to be specific to Hawai'i Island rather than either
   generic SaaS-blue or tourist-tropical cliche. Second pass (July 2026):
   the original palette led with Kilauea ember on warm sand, which read
   more high-desert than island; it now leads with water and forest. Still
   drawn from the island's own materials:

     ocean : deep Pacific teal. Primary accent / CTAs.
     fern  : 'ohi'a / rainforest green. Secondary accent, "open"/success.
     lehua : 'ohi'a lehua blossom red. Small warm accents only (the pua
             marks, dance category, attention states), never large fills.
     ochre : turmeric ('olena). Caution / needs-attention states.
     brick : muted red-brown, darker than lehua. Error / hidden states.

   Neutrals carry the shift: sea-mist background with a green cast,
   basalt-green text, instead of either warm sand or corporate gray. No
   palm trees, no hibiscus clipart, no sunset gradients: the motifs are
   the horizon mark (twin peaks meeting the ocean), a faint topographic
   contour on the hero, and a small five-petal pua line mark (.pua) used
   exactly twice on the homepage. Montserrat throughout, per house style.
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,500;0,600;0,700;0,800;1,500&display=swap');

:root {
  --font: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* Neutrals: sea-mist, a green cast instead of warm sand */
  --bg: #f3f7f2;
  --surface: #fcfdfb;
  --surface-alt: #e8efe6;
  --border: #d7e0d3;

  --text: #1f2721;
  --text-muted: #5c6b60;
  --text-faint: #8a9789;

  /* Primary accent: deep Pacific */
  --ocean: #17697a;
  --ocean-dark: #114c58;
  --ocean-light: #ddedf0;

  /* Secondary accent, open / success: fern ('ohi'a rainforest) */
  --fern: #35704a;
  --fern-dark: #26543a;
  --fern-light: #e2efe2;

  /* Small warm accent: 'ohi'a lehua blossom (was --ember; renamed when it
     stopped being the primary and became the island's touch of red) */
  --lehua: #b13f3a;
  --lehua-dark: #8a2f2b;
  --lehua-light: #f8e7e3;

  /* Status: caution / needs review: ochre ('olena) */
  --ochre: #a8791f;
  --ochre-dark: #7d5a16;
  --ochre-light: #f5ecd9;

  /* Status: error / suspended: brick (darker + browner than lehua) */
  --brick: #7e352a;
  --brick-dark: #612820;
  --brick-light: #f2e2de;

  /* Category accents: six distinct, muted, earth/water-derived hues */
  --cat-team-sports: #17697a;         /* ocean */
  --cat-individual-sports: #3a5a78;   /* basalt blue */
  --cat-dance: #b13f3a;               /* lehua */
  --cat-music-performing-arts: #a8791f; /* ochre */
  --cat-general-enrichment: #6b4d6e;  /* taro */
  --cat-parent-and-me: #35704a;       /* fern */

  --radius: 16px;
  --radius-sm: 10px;
  --shadow: 0 2px 12px rgba(31, 39, 33, 0.07);
  --shadow-lg: 0 14px 34px rgba(31, 39, 33, 0.16);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.6;
}

body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

main { flex: 1; }

h1, h2, h3, h4 { font-family: var(--font); font-weight: 800; letter-spacing: -0.01em; margin: 0 0 12px; }
h1 { font-size: 2.1rem; }
h2 { font-size: 1.5rem; font-weight: 700; }
h3 { font-size: 1.1rem; font-weight: 700; }
p { margin: 0 0 14px; }
a { color: var(--ocean-dark); text-decoration: none; }
a:hover { text-decoration: underline; }
ul { padding-left: 20px; }

.container { max-width: 1080px; margin: 0 auto; padding: 0 20px; }
.container-narrow { max-width: 720px; margin: 0 auto; padding: 0 20px; }

/* ---------- Logo mark (horizon / twin peaks) ---------- */

.logo-mark { display: inline-flex; flex: none; line-height: 0; }
.logo-mark svg,
.logo-mark img { display: block; }
/* Sized here rather than left to the img's width/height attributes so the
   narrow breakpoint further down can scale it back without touching the
   markup in partials.js. */
.logo-mark img { width: 44px; height: 44px; }

/* ---------- Pua mark ----------
   Small five-petal flower line mark, inlined as SVG where used (hero
   eyebrow and region banner on index.html). Lehua red, and deliberately
   scarce: if it starts appearing everywhere it turns into the tourist
   clipart this theme is avoiding. */

.pua { display: inline-block; vertical-align: -3px; color: var(--lehua); }
.region-banner .pua { flex: none; vertical-align: middle; }

/* ---------- Top bar ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 20px;
  max-width: 1080px;
  margin: 0 auto;
}
.topbar .home-link {
  display: flex;
  align-items: center;
  gap: 12px;
  font-weight: 800;
  font-size: 1.45rem;
  letter-spacing: -0.01em;
  color: var(--text);
  white-space: nowrap;
}
.topbar .home-link:hover { text-decoration: none; color: var(--ocean); }

.topbar-nav {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.topbar-nav a {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-muted);
  padding: 8px 14px;
  border-radius: 999px;
  transition: background .12s ease, color .12s ease;
  white-space: nowrap;
}
.topbar-nav a:hover { background: var(--surface-alt); color: var(--text); text-decoration: none; }
.topbar-nav a.current { background: var(--ocean-light); color: var(--ocean-dark); }
.topbar-nav a.cta {
  background: var(--ocean);
  color: #fff;
}
.topbar-nav a.cta:hover { background: var(--ocean-dark); color: #fff; }

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: none;
  background: var(--ocean);
  color: #fff;
  font-family: var(--font);
  font-weight: 700;
  font-size: 14.5px;
  padding: 11px 20px;
  border-radius: 999px;
  cursor: pointer;
  transition: background .15s ease, color .15s ease, border-color .15s ease, opacity .15s ease, box-shadow .15s ease, transform .12s ease;
  white-space: nowrap;
}
.btn:hover { background: var(--ocean-dark); text-decoration: none; color: #fff; }
.btn.btn-ghost {
  background: transparent;
  color: var(--text);
  border: 1.5px solid var(--border);
}
.btn.btn-ghost:hover { border-color: var(--ocean); color: var(--ocean-dark); background: var(--ocean-light); }
.btn.btn-ocean { background: var(--ocean); }
.btn.btn-ocean:hover { background: var(--ocean-dark); }
.btn.btn-danger { background: var(--brick); }
.btn.btn-danger:hover { background: var(--brick-dark); }

/* Outline variant: same ocean scheme as the solid default, inverted fill.
   Hover highlights in place (tint fill, deeper border); it does not trade
   looks with its sibling. An earlier version did exactly that trade and
   read as the buttons switching identities mid-hover. */
.btn.btn-outline {
  background: var(--surface);
  color: var(--ocean-dark);
  border: 1.5px solid var(--ocean);
}
.btn.btn-outline:hover {
  background: var(--ocean-light);
  color: var(--ocean-dark);
  border-color: var(--ocean-dark);
}

/* Hero CTA pair: hover one and it pops (lift + shadow + its own normal
   hover color) while the sibling drops back, so attention lands on one
   action at a time. The :has() gate keeps the fade from firing while the
   cursor is only in the gap between the buttons; browsers without :has()
   just skip the fade and keep the plain hovers. The transparent border on
   the solid button keeps both buttons the same height. */
.hero-actions .btn { border: 1.5px solid transparent; }
.hero-actions .btn-outline { border-color: var(--ocean); }
.hero-actions .btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 20px rgba(17, 76, 88, 0.22);
}
.hero-actions:has(.btn:hover) .btn:not(:hover) { opacity: 0.45; }
@media (prefers-reduced-motion: reduce) {
  .hero-actions .btn:hover { transform: none; }
}
.btn:disabled, .btn[aria-disabled="true"] {
  background: var(--border);
  color: var(--text-faint);
  cursor: not-allowed;
}

/* ---------- Hero (home) ---------- */

.hero {
  position: relative;
  overflow: hidden;
  background: linear-gradient(180deg, var(--surface-alt) 0%, var(--bg) 100%);
  border-bottom: 1px solid var(--border);
  padding: 64px 20px 56px;
}
.hero-contour {
  position: absolute;
  inset: 0;
  opacity: 0.5;
  pointer-events: none;
}
.hero-inner {
  position: relative;
  max-width: 780px;
  margin: 0 auto;
  text-align: center;
}
.hero .eyebrow {
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--ocean-dark);
  margin-bottom: 10px;
}
.hero h1 { font-size: 2.5rem; margin-bottom: 14px; }
.hero p.lede { font-size: 17px; color: var(--text-muted); max-width: 560px; margin: 0 auto 26px; }
.hero-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

/* ---------- Region banner ---------- */

.region-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ocean-dark);
  background: var(--ocean-light);
  border-bottom: 1px solid var(--border);
  padding: 9px 16px;
  text-align: center;
}

/* ---------- Sample-content notice ---------- */

.sample-notice {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  background: var(--ochre-light);
  border: 1px solid #e3cf9c;
  color: var(--ochre-dark);
  border-radius: var(--radius-sm);
  padding: 12px 16px;
  font-size: 13.5px;
  margin: 20px 0;
}
.sample-notice strong { color: var(--ochre-dark); }

/* ---------- Category tiles (home) ---------- */

.section { padding: 48px 0; }
.section-heading { text-align: center; margin-bottom: 32px; }
.section-heading h2 { margin-bottom: 6px; }
.section-heading p { color: var(--text-muted); margin: 0; }

.category-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
}
@media (min-width: 860px) {
  .category-grid {
    grid-template-columns: repeat(4, 1fr);
    grid-auto-rows: 1fr;
  }
  .category-tile--open {
    grid-row: 1 / 3;
    grid-column: 1;
  }
}
.category-tile {
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-top: 4px solid var(--tile-color, var(--ocean));
  border-radius: var(--radius);
  padding: 22px;
  box-shadow: var(--shadow);
  transition: transform .15s ease, box-shadow .15s ease;
  color: var(--text);
}
.category-tile:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); text-decoration: none; color: var(--text); }
.category-tile .cat-icon { color: var(--tile-color, var(--ocean)); }
/* Icons ship at width/height 22 in the markup; CSS wins over the attribute.
   38px so the hover motion below is actually legible: the animations are
   expressed in viewBox units, so at 22px a 3-unit move landed under 3 real
   pixels and read as nothing. The mobile block near the end of this file
   overrides back down to 20px. */
.category-tile .cat-icon svg { width: 38px; height: 38px; }
.category-tile h3 { margin: 0; }
.category-tile .count { font-size: 13px; color: var(--text-faint); }

.category-tile--open {
  justify-content: center;
  border: 2px solid #c2c2c2;
  border-top: 6px solid #8f8f8f;
  border-bottom: 6px solid #8f8f8f;
  background: linear-gradient(180deg, #f3f3f3, var(--surface) 30%, var(--surface) 70%, #f3f3f3);
}
.category-tile--open .cat-icon { color: var(--text-muted); }
.category-tile--open h3 { margin: 0; }
.category-tile--open .count { color: var(--text-faint); }

/* ---------- Category icon hover motion ----------
   One small animation per category, each keyed to that category's idea:
   a single lap around the check for open signups, a shove-and-follow for team
   sports, a scope settling onto its lock for individual sports, and so on. The
   markup hooks (.ki-*) live in js/partials.js (and index.html for the open tile).

   Every one of these runs exactly once and stops, so each keyframe set either
   ends where it began or uses `forwards` to hold a deliberate final pose.
   Nothing here loops.

   SVG children need `transform-box: fill-box` so percentage transform
   origins resolve against the shape's own box rather than the viewport.
   Amplitudes are in viewBox units, so they scale with the icon size set
   above -- shrink the icon and the motion shrinks with it. */

.category-tile .ki,
.category-tile .ki * { transform-box: fill-box; transform-origin: center; }

/* Open signups: a gap opens in the ring, travels around the check exactly
   once, and closes. Rotation and the gap are one keyframe set rather than two
   parallel animations so the gap is guaranteed to shut at the same instant the
   lap completes -- the final frame is then identical to the resting icon. */
@keyframes ki-lap {
  0%   { stroke-dasharray: 50.3 0; transform: rotate(0deg); }
  14%  { stroke-dasharray: 13 37.3; }
  86%  { stroke-dasharray: 13 37.3; }
  100% { stroke-dasharray: 50.3 0; transform: rotate(360deg); }
}
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-check .ki-ring {
  animation: ki-lap 1.4s cubic-bezier(.45, .05, .35, 1) forwards;
}

/* Team sports: one circle slides into the other, which yields and follows it
   along the same line -- a shove and a give, not a spring. The lead circle
   starts moving first, so the gap between them compresses on the way out and
   opens back up as the second one catches up. */
@keyframes ki-shove-a {
  0%   { transform: translate(0, 0); }
  42%  { transform: translate(3px, 3px); }
  100% { transform: translate(0, 0); }
}
@keyframes ki-shove-b {
  0%, 16% { transform: translate(0, 0); }
  58% { transform: translate(3px, 3px); }
  100% { transform: translate(0, 0); }
}
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-team .ki-a { animation: ki-shove-a 1.05s cubic-bezier(.4, 0, .3, 1) forwards; }
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-team .ki-b { animation: ki-shove-b 1.05s cubic-bezier(.4, 0, .3, 1) forwards; }

/* Individual sports: the scope drifts, each swing shorter than the last, then
   locks on and narrows. Travel is capped at 3 because the scope's box spans
   x 4-20 and the root <svg> clips at the viewBox edge -- the old version threw
   it 7 units and half the icon left the frame. The dip past the final scale at
   88% is the snap of the lock. */
@keyframes ki-lock {
  0%   { transform: translateX(-3px) scale(1); }
  22%  { transform: translateX(2.4px) scale(1); }
  42%  { transform: translateX(-1.5px) scale(1); }
  58%  { transform: translateX(.8px) scale(1); }
  70%  { transform: translateX(0) scale(1); }
  88%  { transform: translateX(0) scale(.58); }
  100% { transform: translateX(0) scale(.64); }
}
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-target .ki-scope {
  animation: ki-lock 1.9s cubic-bezier(.35, 0, .25, 1) forwards;
}

/* Dance: bumps twice to the left, twice to the right, then settles upright.
   A bump is swing out, fall back partway, swing out again to the same side --
   the 22% and 68% steps are that partial recovery, and keeping them well short
   of centre is what separates a double bump from two ordinary sways. Pivots
   from the top edge of the shape, not its middle, so the wide bottom of the
   arc carries the motion. */
@keyframes ki-hula {
  0%   { transform: rotate(0deg); }
  12%  { transform: rotate(-16deg); }
  22%  { transform: rotate(-7deg); }
  32%  { transform: rotate(-16deg); }
  46%  { transform: rotate(-2deg); }
  58%  { transform: rotate(16deg); }
  68%  { transform: rotate(7deg); }
  78%  { transform: rotate(16deg); }
  100% { transform: rotate(0deg); }
}
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-dance .ki-skirt {
  transform-origin: 50% 0%;
  animation: ki-hula 1.7s ease-in-out forwards;
}

/* Music: bars bounce on staggered clocks like an audio meter. Each bar caps
   at its own --eq, since a tall bar scaled as hard as a short one would grow
   straight out through the top of the 24x24 viewBox and get clipped. */
@keyframes ki-eq {
  0%, 100% { transform: scaleY(1); }
  50% { transform: scaleY(var(--eq, 1.7)); }
}
/* Each bar runs a whole number of cycles so it finishes back at scaleY(1),
   and the stagger uses positive delays rather than negative ones: a negative
   delay starts a bar mid-cycle, which means a finite run also *ends*
   mid-cycle, leaving the bar frozen at some arbitrary height. The counts are
   chosen so all four land within ~250ms of each other, around 1.9s. */
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-music .ki-bar {
  transform-origin: 50% 100%;
  animation: ki-eq .52s ease-in-out forwards;
}
.category-tile .ki-music .ki-bar:nth-child(1) { --eq: 3.6; }
.category-tile .ki-music .ki-bar:nth-child(2) { --eq: 1.9; }
.category-tile .ki-music .ki-bar:nth-child(3) { --eq: 2.4; }
.category-tile .ki-music .ki-bar:nth-child(4) { --eq: 1.6; }
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-music .ki-bar:nth-child(1) { animation-duration: .44s; animation-iteration-count: 4; }
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-music .ki-bar:nth-child(2) { animation-duration: .66s; animation-iteration-count: 3; animation-delay: .05s; }
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-music .ki-bar:nth-child(3) { animation-duration: .38s; animation-iteration-count: 5; animation-delay: .03s; }
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-music .ki-bar:nth-child(4) { animation-duration: .58s; animation-iteration-count: 3; animation-delay: .08s; }

/* General enrichment: the star winds up, pops, and reappears. The scale
   ramp is doing real work, not decoration -- an 8-spoke mark repeats every
   90deg, so past a certain speed the rotation alone reads as static. The
   shrink-then-burst is what makes the acceleration legible.

   Timing is linear by design. An ease-in curve on top of already-accelerating
   keyframe values double-counts the acceleration, which crushed the burst into
   the last ~120ms and read as a glitch rather than a pop. All the acceleration
   now lives in the rotation values, and the burst gets its own ~330ms. */
@keyframes ki-spin-pop {
  0%   { transform: rotate(0) scale(1); opacity: 1; }
  30%  { transform: rotate(120deg) scale(.95); opacity: 1; }
  55%  { transform: rotate(420deg) scale(.86); opacity: 1; }
  75%  { transform: rotate(900deg) scale(.74); opacity: 1; }
  85%  { transform: rotate(1250deg) scale(.66); opacity: 1; }
  91%  { transform: rotate(1350deg) scale(1.28); opacity: .5; }
  95%  { transform: rotate(1400deg) scale(1.45); opacity: 0; }
  97%  { transform: rotate(1440deg) scale(.9); opacity: 0; }
  100% { transform: rotate(1440deg) scale(1); opacity: 1; }
}
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-star .ki-spark {
  animation: ki-spin-pop 2.2s linear forwards;
}

/* Parent and me: crouch, toss, catch, then pull the little one in for a hug
   and settle. Lift is capped at 4.6 -- the child rests at cy 8.5 with r 3.5,
   and at the apex it is also scaled to .86, so anything past that puts its
   stroked top edge through the top of the viewBox, which clips. */
@keyframes ki-toss {
  0%   { transform: translate(0, 0) scale(1); }
  12%  { transform: translate(-.7px, 1.6px) scale(1.06); }
  40%  { transform: translate(1.4px, -4.6px) scale(.86); }
  52%  { transform: translate(1.4px, -4.6px) scale(.86); }
  68%  { transform: translate(-.4px, 1.4px) scale(1.05); }
  80%  { transform: translate(-1.3px, .9px) scale(.93); }
  89%  { transform: translate(-1.3px, .9px) scale(.93); }
  100% { transform: translate(0, 0) scale(1); }
}
@keyframes ki-catch {
  0%   { transform: translate(0, 0) scale(1); }
  12%  { transform: translate(0, .9px) scale(1); }
  40%  { transform: translate(0, -.5px) scale(1); }
  52%  { transform: translate(0, -.5px) scale(1); }
  68%  { transform: translate(0, 1.1px) scale(1); }
  80%  { transform: translate(.5px, -.2px) scale(1.07); }
  89%  { transform: translate(.5px, -.2px) scale(1.07); }
  100% { transform: translate(0, 0) scale(1); }
}
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-pair .ki-child {
  animation: ki-toss 2s cubic-bezier(.4, 0, .4, 1) forwards;
}
.category-tile:is(:hover, :focus-visible, .ki-run) .ki-pair .ki-parent {
  animation: ki-catch 2s cubic-bezier(.4, 0, .4, 1) forwards;
}

/* Deliberately NOT gated on prefers-reduced-motion, per an explicit call on
   31 Jul 2026. Note the tradeoff if this is ever revisited: visitors who set
   that preference (often for vestibular or motion-sensitivity reasons) will
   still get them. Each runs once, is local to a small icon, and fires only on
   hover, focus, or scrolling into view, which is what makes that an acceptable
   call here. To restore the guard, add back:
     @media (prefers-reduced-motion: reduce) {
       .category-tile:is(:hover, :focus-visible, .ki-run) .ki * {
         animation: none !important;
       }
     } */

/* ---------- How it works ---------- */

.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 24px;
}
.step { text-align: center; }
.step-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  background: var(--fern-light);
  color: var(--fern-dark);
  font-weight: 800;
  margin-bottom: 12px;
}
.step h3 { font-size: 15.5px; }
.step p { font-size: 14px; color: var(--text-muted); margin: 0; }

/* ---------- Board / filters ---------- */

.board-header { padding: 32px 0 12px; }
.board-header h1 { margin-bottom: 4px; }
.board-header p { color: var(--text-muted); margin: 0; }

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
  align-items: flex-end;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px;
  margin-bottom: 24px;
}
.filter-field { display: flex; flex-direction: column; gap: 5px; min-width: 150px; }
.filter-field label {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-faint);
}
.filter-field select,
.filter-field input {
  font-family: var(--font);
  font-size: 14.5px;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  background: var(--surface);
  color: var(--text);
}
.filter-field select:focus,
.filter-field input:focus { outline: none; border-color: var(--ocean); }
.filter-checkbox {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-muted);
  padding-bottom: 9px;
}
.filter-reset {
  font-size: 13px;
  font-weight: 700;
  color: var(--ocean-dark);
  background: none;
  border: none;
  cursor: pointer;
  padding-bottom: 9px;
  font-family: var(--font);
}
.filter-reset:hover { text-decoration: underline; }

.result-count { font-size: 13.5px; color: var(--text-faint); margin: 0 0 14px; }

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 18px;
  padding-bottom: 48px;
}

/* ---------- Sponsor placements ----------
   Paid placements above and below the activity listings, never inside the
   grid (see board.html and netlify/functions/sponsors-public.mjs). The
   styling is intentionally NOT a variant of .activity-card: the ochre palette
   and the dashed border are used nowhere else on a card, so a placement can't
   be skimmed as a real listing even by someone who never reads the label. If
   these two ever start to converge visually, that's a bug in the thing this
   whole feature is supposed to protect. */

/* Wrapping row, not a column: a banner claims its own full-width line, while
   card placements flow beside each other up to three across and wrap after
   that. A plain column made two card placements stack as narrow left-aligned
   tiles with a screen's worth of dead space beside them, which read worse than
   either one alone. The 1fr-of-three basis is what caps it at three: at the
   container's ~1040px that lands each card just under its 340px max, so a
   fourth wraps to a new line at the same width instead of shrinking the row. */
.sponsor-slots { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px; }
.sponsor-slots:not(:empty) { margin: 0 0 20px; }
#sponsor-slots-bottom:not(:empty) { margin: 8px 0 48px; }
.sponsor-slot--banner { flex: 1 0 100%; }
.sponsor-slot--card { flex: 1 1 calc((100% - 24px) / 3); min-width: 240px; }

.sponsor-slot {
  display: flex;
  align-items: center;
  gap: 14px;
  position: relative;
  background: var(--ochre-light);
  border: 1px dashed #d8bd82;
  border-radius: var(--radius);
  padding: 16px 18px;
  color: var(--text);
  text-decoration: none;
}
a.sponsor-slot:hover { border-color: var(--ochre); color: var(--text); text-decoration: none; }

/* Reads as a label, not as a status pill: uppercase and small, in the ochre
   used for nothing else here, so "this is an ad" registers before the name. */
.sponsor-tag {
  flex: none;
  align-self: flex-start;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--ochre-dark);
  background: var(--surface);
  border: 1px solid #e0cb99;
  border-radius: 999px;
  padding: 3px 8px;
}

.sponsor-image {
  flex: none;
  width: 88px;
  height: 60px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.sponsor-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.sponsor-name { font-weight: 700; font-size: 15.5px; }
.sponsor-blurb { font-size: 13.5px; color: var(--text-muted); }

/* A card placement is narrower than a banner so it reads as one tile rather
   than a full-width strip, and stacks its parts the way the grid's cards do,
   while keeping the ochre/dashed treatment that separates it from them. */
.sponsor-slot--card {
  flex-direction: column;
  align-items: flex-start;
  max-width: 340px;
}
.sponsor-slot--card .sponsor-image { width: 100%; height: 130px; }

/* Full-bleed: the sponsor's own artwork IS the placement. Padding drops to
   zero so the image reaches all four edges, the label is absolutely positioned
   over it, and nothing else is drawn on top (board.js omits the name/blurb
   entirely in this mode). The dashed ochre border stays: the frame is doing the
   same "this is paid, the cards below are not" work it does on a text
   placement, and a sponsor's finished graphic is exactly the case where that
   distinction is easiest to lose. */
.sponsor-slot--full { display: block; padding: 0; overflow: hidden; }
.sponsor-slot--full .sponsor-tag {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1;
}
/* aspect-ratio, not a fixed height: the box reserves its space before the
   image loads (no reflow jolting the board as a sponsor's artwork arrives) and
   still scales down cleanly on a phone. object-fit: cover means artwork at a
   different ratio than the slot is cropped rather than distorted, since a
   stretched logo is worse than a trimmed one. 970x250 is the standard wide
   banner a sponsor's designer will already have; the card variant below is
   squarer because a 340px-wide tile at 8:1 would be a sliver. */
.sponsor-slot--full .sponsor-image {
  width: 100%;
  height: auto;
  aspect-ratio: 970 / 250;
  object-fit: cover;
  border-radius: 0;
  display: block;
}
.sponsor-slot--card.sponsor-slot--full .sponsor-image { aspect-ratio: 4 / 3; }

/* ---------- Card component ---------- */

.activity-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px 20px 18px;
  box-shadow: var(--shadow);
  transition: transform .15s ease, box-shadow .15s ease;
  color: var(--text);
}
.activity-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); text-decoration: none; color: var(--text); }
.activity-card::before {
  content: "";
  position: absolute;
  top: 16px; left: 0;
  width: 4px; height: 22px;
  background: var(--card-color, var(--ocean));
  border-radius: 0 3px 3px 0;
}
.activity-card.is-trying-to-start {
  border-style: dashed;
  border-color: var(--card-color, var(--lehua));
}
.card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}
.district-chip {
  display: inline-flex;
  align-items: center;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--text-faint);
  background: var(--surface-alt);
  padding: 4px 10px;
  border-radius: 999px;
  white-space: nowrap;
}
.category-chip {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--card-color, var(--ocean));
  background: color-mix(in srgb, var(--card-color, var(--ocean)) 12%, white);
  padding: 3px 9px;
  border-radius: 999px;
  width: fit-content;
}
.activity-card h3 { font-size: 17px; margin: 2px 0 0; }
.card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  font-size: 13px;
  color: var(--text-faint);
}
.card-meta span { display: inline-flex; align-items: center; gap: 4px; }
.card-excerpt { font-size: 14px; color: var(--text-muted); margin: 2px 0 0; }

/* ---------- Status pills ---------- */

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 700;
  padding: 4px 10px;
  border-radius: 999px;
  white-space: nowrap;
}
.status-pill.status-open_for_signups { background: var(--fern-light); color: var(--fern-dark); }
.status-pill.status-active { background: var(--surface-alt); color: var(--text-muted); }
.status-pill.status-tryouts_required { background: var(--ocean-light); color: var(--ocean-dark); }
.status-pill.status-full { background: var(--surface-alt); color: var(--text-faint); }
.status-pill.status-trying_to_start { background: var(--lehua-light); color: var(--lehua-dark); border: 1px dashed var(--lehua); }
.status-pill.status-needs_review { background: var(--ochre-light); color: var(--ochre-dark); }
.status-pill.status-suspended { background: var(--brick-light); color: var(--brick); }

/* Photo pill: the admin dashboard's at-a-glance "does this card have an
   image yet" marker, sitting beside the status pill on every card row (see
   dashboard.js's photoPillHtml). Its own class rather than a reused
   .status-pill.status-* variant because it isn't a card status: a card can
   be Open for Sign-Ups and photoless at the same time, and the two pills
   have to be readable side by side as separate facts. Same geometry as
   .status-pill so the pair sits on one visual line. */
.photo-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 700;
  padding: 4px 10px;
  border-radius: 999px;
  white-space: nowrap;
}
.photo-pill.photo-pill-has { background: var(--fern-light); color: var(--fern-dark); }
/* Dashed, like .status-pill.status-trying_to_start: the missing-photo state
   is a to-do rather than a settled condition, and the outline makes it
   findable when scanning a long list for what still needs chasing. */
.photo-pill.photo-pill-missing {
  background: var(--ochre-light);
  color: var(--ochre-dark);
  border: 1px dashed var(--ochre);
}

/* ---------- Empty state ---------- */

.empty-state {
  text-align: center;
  padding: 60px 20px;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
}
.empty-state h3 { color: var(--text); }

/* ---------- Card detail page ---------- */

.card-detail-header { padding: 30px 0 6px; }
.back-link { font-size: 13.5px; font-weight: 600; color: var(--text-muted); display: inline-flex; align-items: center; gap: 5px; }
.detail-title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  margin-top: 14px;
}
.detail-title-row h1 { margin-bottom: 8px; }

.detail-grid {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(240px, 1fr);
  gap: 28px;
  padding: 24px 0 56px;
  align-items: start;
}
@media (max-width: 800px) {
  .detail-grid { grid-template-columns: 1fr; }
}

.fact-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 16px 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px 22px;
  margin: 6px 0 22px;
}
/* Grid items default to min-width:auto, which lets an unbroken-space
   value (a URL, a long email) size its cell to the full content width
   instead of wrapping, pushing the cell past its column and over its
   neighbor. min-width:0 lets the cell actually shrink to the grid
   track's width so overflow-wrap below has room to do its job. */
.fact-item { min-width: 0; }
.fact-item .fact-label {
  font-size: 11.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-faint);
  margin-bottom: 3px;
}
.fact-item .fact-value {
  font-size: 14.5px;
  color: var(--text);
  font-weight: 600;
  overflow-wrap: anywhere;
  /* HTML collapses newlines by default; a multi-paragraph description
     (or schedule, or any other free-text field with its own line breaks)
     came through as one run-on block because of that, even though the
     line breaks were preserved correctly in the database and the
     original submission email the whole way through. pre-wrap keeps
     those line breaks and still wraps long lines normally. */
  white-space: pre-wrap;
}

.connect-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 22px;
  box-shadow: var(--shadow);
  position: sticky;
  top: 90px;
}
.connect-panel h3 { margin-bottom: 4px; }
.connect-panel .connect-name { font-size: 15.5px; font-weight: 700; color: var(--text); margin: 0 0 6px; }
.connect-panel .connect-sub { font-size: 13px; color: var(--text-faint); margin-bottom: 16px; }
.connect-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 6px; }
.connect-item {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 10px;
  padding: 10px 12px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  background: var(--surface);
  transition: border-color .12s ease, background .12s ease;
}
.connect-item:hover { border-color: var(--ocean); background: var(--ocean-light); text-decoration: none; }
.connect-item.is-placeholder { cursor: help; }
.connect-item .connect-icon { flex: none; color: var(--ocean-dark); }
/* The value itself (email/phone/handle) gets the growable, wrappable
   share of the row; flex:1 + min-width:0 is what lets overflow-wrap
   actually kick in instead of the row just growing wider than its
   panel. copy-hint stays a fixed-width label off to the side instead of
   fighting the value for space, which is what let a long email force
   the whole pill oversized before. */
.connect-item .connect-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.updated-note { font-size: 12.5px; color: var(--text-faint); margin-top: 16px; }
.report-link { font-size: 12.5px; color: var(--text-faint); }

/* Same reasoning as .fact-item .fact-value's white-space: pre-wrap: the
   description's own line breaks (paragraph breaks a coach typed in)
   were getting collapsed into one run-on block by HTML's default
   whitespace handling. */
.card-description { white-space: pre-wrap; }

.photo-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 160px;
  background: var(--surface-alt);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  color: var(--text-faint);
  margin-bottom: 20px;
}

/* ---------- Forms (start-a-group, coach edit, admin) ---------- */

.form-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 28px;
  box-shadow: var(--shadow);
  margin: 24px 0 60px;
}
.form-heading {
  font-size: 19px;
  margin: 0 0 4px;
  padding-top: 28px;
  border-top: 1px solid var(--border);
}
/* :first-of-type (not :first-child): the honeypot field and hidden
   form-name input sit before the first heading in the markup, so
   :first-child never matched it and the divider line kept showing up
   above "Activity Card Info (public)" with nothing above it to divide
   from. :first-of-type looks at the first .form-heading among its
   siblings regardless of what non-heading elements come before it. */
.form-heading:first-of-type { padding-top: 0; border-top: none; }
.form-heading .form-heading-note {
  display: block;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-faint);
  margin-top: 4px;
}
.form-subheading {
  font-size: 14.5px;
  font-weight: 700;
  color: var(--ocean-dark);
  text-transform: uppercase;
  letter-spacing: .03em;
  margin: 34px 0 14px;
}
.form-field { margin-bottom: 20px; }
.form-field label {
  display: block;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 6px;
}
.form-field .field-hint { font-size: 12.5px; color: var(--text-faint); margin-top: 5px; }
.form-field input[type="text"],
.form-field input[type="email"],
.form-field input[type="tel"],
.form-field input[type="number"],
.form-field select,
.form-field textarea {
  width: 100%;
  font-family: var(--font);
  font-size: 15px;
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  background: var(--surface);
  color: var(--text);
}
.form-field textarea { resize: vertical; min-height: 100px; }
.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus {
  outline: none;
  border-color: var(--ocean);
  box-shadow: 0 0 0 3px var(--ocean-light);
}
.char-counter { font-size: 12px; color: var(--text-faint); text-align: right; margin-top: 4px; }
.char-counter.is-over { color: var(--brick); font-weight: 700; }
.checkbox-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 8px 16px;
}
.checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 500; }

/* honeypot field, kept off-screen but present in DOM/tab order excluded */
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form-success {
  text-align: center;
  padding: 60px 20px;
}
.form-success .big-mark { font-size: 40px; margin-bottom: 14px; }

/* ---------- Auth / gated pages ---------- */

.auth-panel {
  max-width: 440px;
  margin: 60px auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 32px;
  text-align: center;
  box-shadow: var(--shadow);
}
.auth-panel p { color: var(--text-muted); }
.auth-panel .field-hint { font-size: 13px; color: var(--text-faint); }
.role-picker { display: flex; flex-direction: column; gap: 10px; margin: 20px 0; }
.role-picker button {
  text-align: left;
  padding: 14px 16px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-family: var(--font);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.role-picker button:hover { border-color: var(--ocean); background: var(--ocean-light); }

/* ---------- Admin dashboard tabs ---------- */

.dash-tabs {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
  margin: 22px 0 24px;
}
/* Each tab is its own bordered/backgrounded chip at every screen size
   (not just plain text with an underline), so the bar reads as a row of
   distinct, clickable tabs even before it ever wraps or scrolls. The
   active tab keeps the underline+bold treatment on top of that. */
.dash-tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 16px;
  font-family: var(--font);
  font-weight: 700;
  font-size: 14px;
  color: var(--text-muted);
  background: var(--surface-alt);
  border: 1.5px solid var(--border);
  border-bottom: 3px solid transparent;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  cursor: pointer;
  margin-bottom: -1px;
  white-space: nowrap;
  transition: background .12s ease, color .12s ease;
}
.dash-tab:hover { color: var(--text); background: var(--surface); }
.dash-tab.active { color: var(--text); background: var(--surface); border-color: var(--border) var(--border) var(--ocean); border-bottom-color: var(--ocean); }

.dash-section {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 22px;
  margin-bottom: 24px;
  box-shadow: var(--shadow);
}
.dash-section h2 { display: flex; align-items: center; gap: 10px; font-size: 1.2rem; }

/* Bulleted help text under a dash-section's h2 (Flagged Cards, All Cards,
   Activity Leaders, Invite an Activity Leader), same font-size/color as
   .field-hint so it reads as the same "quiet explanatory text" register,
   just with a real list instead of one run-on sentence. */
.dash-help { font-size: 12.5px; color: var(--text-faint); margin: -8px 0 16px; }
.dash-help p { margin: 0 0 6px; }
.dash-help ul { margin: 0; padding-left: 18px; }
.dash-help li { margin-bottom: 3px; }
.dash-help li:last-child { margin-bottom: 0; }
.dash-section .count-badge {
  font-size: 12px;
  background: var(--lehua-light);
  color: var(--lehua-dark);
  padding: 2px 9px;
  border-radius: 999px;
  font-weight: 700;
}
.dash-row {
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}
.dash-row:last-child { border-bottom: none; }
.dash-row-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
}
.dash-row-main { flex: 1; min-width: 200px; }
.dash-row-main[data-toggle-detail] { cursor: pointer; }
.dash-row-main .name { font-weight: 700; }
.dash-row-main .meta { font-size: 12.5px; color: var(--text-faint); }
.dash-row-chevron { display: inline-block; font-size: 12px; color: var(--text-faint); }
.dash-actions { display: flex; gap: 8px; }
.btn-sm { padding: 7px 14px; font-size: 13px; }
.dash-empty { color: var(--text-faint); font-size: 14px; padding: 10px 0; }
.dash-row-detail { margin: 10px 0 4px; }
/* .fact-list's own display:grid is an author rule and beats the browser
   default [hidden]{display:none} regardless of source order (author rules
   always outrank user-agent ones), so the accordion detail stayed visibly
   expanded no matter what the hidden attribute said until this rule made
   the collapsed state explicit again. */
.dash-row-detail[hidden] { display: none; }

/* Contact pills: used in the admin dashboard's card detail accordion so
   multiple social links (Instagram, email, phone...) each read as their
   own small clickable chip instead of one run-on, comma-joined line that
   wraps awkwardly. Mirrors the public card-detail page's connect-item
   pattern, simplified (no icon set) since this is an internal,
   utilitarian view rather than the polished public one. */
.contact-pills { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 6px; }
.contact-pill {
  display: inline-flex;
  align-items: center;
  font-size: 12.5px;
  font-weight: 600;
  padding: 4px 11px;
  border-radius: 999px;
  border: 1.5px solid var(--border);
  color: var(--text);
  background: var(--surface);
  white-space: nowrap;
}
a.contact-pill:hover { border-color: var(--ocean); background: var(--ocean-light); text-decoration: none; color: var(--text); }

/* Admin dashboard: "Message" compose overlay (site/js/dashboard.js). One
   shared panel reused for every card row's Message button rather than
   one per row. */
.compose-overlay {
  position: fixed;
  inset: 0;
  background: rgba(20, 30, 26, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  z-index: 100;
}
.compose-overlay[hidden] { display: none; }
.compose-box {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 24px;
  max-width: 460px;
  width: 100%;
  /* Cap the panel to the viewport (the overlay adds 20px padding top and
     bottom) and let a long form scroll inside itself instead of bleeding
     off the top and bottom of the screen. */
  max-height: calc(100vh - 40px);
  overflow-y: auto;
}
/* The sponsor form carries many more fields than the Message/Insta Post
   overlays, so give it more room to breathe. */
#sponsor-overlay .compose-box { max-width: 560px; }

/* Live sponsor preview (site/js/dashboard.js's updateSponsorPreview):
   pinned to the top of the scrolling compose-box so it stays visible while
   an admin scrolls down through the rest of the form. Background uses --bg
   rather than --surface to match the page the placement will actually sit
   on, since the two are close but not identical and the sponsor-slot's
   ochre/dashed border is tuned against --bg's specific shade.
   Deliberately NOT using negative margins to bleed this edge-to-edge in the
   box: a negative top margin pulls the element up regardless of scroll
   position, which overlapped the hint paragraph above it by the same
   amount at rest, clipping its second line. Sitting in normal flow with an
   ordinary top margin avoids that; it just means the preview is inset like
   every other field rather than flush with the box's border. */
.sponsor-preview {
  position: sticky;
  top: 0;
  margin: 4px 0 16px;
  padding: 14px 0 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  z-index: 2;
}
.sponsor-preview-label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--text-muted);
  margin-bottom: 8px;
}
.sponsor-preview-stage {
  background: var(--bg);
  border-radius: var(--radius-sm);
  padding: 14px;
}
#sponsor-preview-caption { margin-top: 6px; }
.compose-box h3 { margin: 0 0 4px; }
.compose-box .form-field { margin-bottom: 14px; }
.compose-box textarea { min-height: 130px; }
/* AI rewrite row (compose-ai-instruction/compose-ai-rewrite): sits
   between the message textarea and the Send/Cancel row, styled to match
   the rest of the box's inputs since it lives outside a .form-field. */
.compose-box #compose-ai-instruction {
  font-family: var(--font);
  font-size: 13px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  background: var(--surface);
  color: var(--text);
}

.scaffold-note {
  background: var(--ochre-light);
  border: 1px solid #e3cf9c;
  color: var(--ochre-dark);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
  font-size: 13.5px;
  margin-bottom: 22px;
}
.scaffold-note strong { color: var(--ochre-dark); }

/* Sponsor placement proof (site/sponsor-proof.html). Deliberately NOT ochre
   like .scaffold-note: that color is the sponsor slot's own "this is paid"
   signal (see "Sponsor placements" above), and this banner sits directly
   above a real one on this page, so sharing its palette would blur the line
   between "information about the proof" and "the placement itself" right
   where that distinction matters most. Neutral gray-green instead. */
.proof-banner {
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  padding: 14px 16px;
  font-size: 13.5px;
  line-height: 1.5;
  margin: 20px 0 18px;
}
.proof-banner strong { color: var(--text); }

/* Printed or saved-as-PDF, the proof should be the placement in its board
   context, not this page's own UI -- the print/copy-link buttons have
   nothing to do once on paper. The banner stays: it self-identifies the
   document (who it's for, prepared when), which is exactly the part worth
   keeping on a page that might get forwarded or printed standalone. */
@media print {
  #proof-actions { display: none; }
}

/* ---------- Footer ---------- */

.site-footer {
  border-top: 1px solid var(--border);
  background: var(--surface-alt);
  padding: 32px 0;
  margin-top: auto;
}
.footer-inner {
  max-width: 1080px;
  margin: 0 auto;
  padding: 0 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 12px;
  font-size: 13px;
  color: var(--text-faint);
}
.footer-inner a { color: var(--text-muted); font-weight: 600; }
.footer-links { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; }
.footer-sep { color: var(--text-faint); }

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

@media (max-width: 640px) {
  .hero { padding: 44px 16px 40px; }
  .hero h1 { font-size: 1.9rem; }
  .topbar { padding: 12px 16px; flex-wrap: wrap; }
  /* Eased back from the desktop 44px/1.45rem. The topbar wraps at this width,
     so the logo and wordmark sit on their own line above the nav; at full
     desktop size they dominate that line on a narrow screen. */
  .logo-mark img { width: 36px; height: 36px; }
  .topbar .home-link { gap: 10px; font-size: 1.2rem; }
  .topbar-nav { gap: 2px; }
  .topbar-nav a { padding: 7px 10px; font-size: 13px; }
  .detail-grid { gap: 20px; }
  .connect-panel { position: static; }
  .fact-list { grid-template-columns: 1fr 1fr; padding: 16px; }
  .form-panel { padding: 20px; }

  /* Scroll instead of wrap: six tabs wrapping into two or three rows of
     plain-looking chips was the actual mobile complaint, not the chip
     style itself. A single scrollable row keeps every tab immediately
     readable as "one of six options" instead of a stacked list. */
  .dash-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    margin-left: -20px;
    margin-right: -20px;
    padding: 0 20px 2px;
  }
  .dash-tab { flex: none; }

  /* One card placement per line on a phone. The three-across basis would put
     two 240px-min tiles side by side on a 390px screen otherwise, which
     overflows; and a placement squeezed to half a phone width isn't worth
     selling anyway. */
  .sponsor-slot--card { flex: 1 0 100%; max-width: none; }

  /* Five footer links plus four pipes wrap into ragged lines that can open
     with a stranded "|" at phone width. Spacing alone separates them here. */
  .footer-sep { display: none; }
  .footer-links { gap: 6px 16px; }

  /* Category tiles on mobile portrait: compact single-column rows instead
     of the desktop card stack. The desktop tile is a vertical column (icon,
     then heading, then count, with 22px padding and 12px gaps), which at
     phone width becomes a full-bleed block roughly 130px tall holding one
     short line of text, so seven of them meant several screens of scrolling
     through mostly empty card. Turned on its side, the same content is one
     ~50px row: icon left, label filling the middle, count pinned right, and
     all seven fit in about one screen. The category's color moves from the
     top border to the left border, which reads better on a wide short row
     and keeps the color coding intact. Desktop (>=860px, the four-column
     grid above) and the tablet range in between are untouched. */
  .section { padding: 32px 0; }
  .section-heading { margin-bottom: 18px; }

  .category-grid { grid-template-columns: 1fr; gap: 8px; }

  .category-tile {
    flex-direction: row;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
    border-top: 1px solid var(--border);
    border-left: 4px solid var(--tile-color, var(--ocean));
  }
  /* Mobile browsers fire :hover on tap and leave it stuck there, so the
     desktop lift would jog the row the user just pressed. */
  .category-tile:hover { transform: none; }
  .category-tile .cat-icon { display: flex; flex: none; }
  /* 30px rather than the old 20px. There is no hover on touch, so the icon
     animations are triggered by the scroll observer in index.html instead --
     and the same sizing problem applies here as on desktop: the motion is
     written in viewBox units, so at 20px a 3-unit move is under 3 real pixels
     and the animation may as well not run. 30px still fits the compact row. */
  .category-tile .cat-icon svg { width: 30px; height: 30px; }
  /* min-width:0 lets a long label ("Parent & Me / Early Childhood") wrap
     inside the row rather than forcing the row wider than the screen. */
  .category-tile h3 { flex: 1; min-width: 0; font-size: 15px; line-height: 1.25; }
  .category-tile .count { flex: none; margin-left: auto; font-size: 12px; white-space: nowrap; }

  .category-tile--open {
    justify-content: flex-start;
    border: 2px solid #c2c2c2;
    border-left: 6px solid #8f8f8f;
    background: linear-gradient(90deg, #f3f3f3, var(--surface) 30%, var(--surface) 70%, #f3f3f3);
  }
}

/* ---------- Photo upload (coach edit) + galleries ---------- */

.photo-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 10px;
}
.photo-thumb {
  position: relative;
  width: 90px;
  height: 90px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1.5px solid var(--border);
}
.photo-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.photo-remove {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.photo-remove:hover {
  background: var(--brick);
}
/* Bottom corners of a .photo-thumb, mirroring .photo-remove's top-right
   circle. index 0 never has a move-earlier button (see photoThumbHtml in
   coach-edit.js), so .photo-cover-badge and .photo-move-earlier never
   both try to occupy the bottom-left corner at once. */
.photo-move {
  position: absolute;
  bottom: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.photo-move:hover {
  background: var(--fern-dark);
}
.photo-move-earlier { left: 3px; }
.photo-move-later { right: 3px; }
.photo-cover-badge {
  position: absolute;
  bottom: 3px;
  left: 3px;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  line-height: 1;
  padding: 4px 6px;
  border-radius: 4px;
  pointer-events: none;
}
.photo-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 10px;
  margin-bottom: 16px;
}
.photo-gallery img {
  width: 100%;
  aspect-ratio: 4 / 3;
  /* contain, not cover: an Activity Leader's uploaded graphic (often
     text-heavy flyer art, not a plain photo, see the "Fielding Pitching
     Lessons" example that prompted this) was getting its edges sliced off
     by cover's crop-to-fill behavior, sometimes losing the exact text
     that mattered (a "Reserve Your Spot" call to action, in one real
     case). contain always shows the whole image; the surface-alt
     background fills whatever gap is left on the short axis instead of
     leaving it transparent (which would just show the white card behind
     it, indistinguishable from actual whitespace in the image itself).
     No way to set a custom crop/focal point yet: that'd need its own
     upload-time cropping UI, a bigger addition than this fix. */
  object-fit: contain;
  background: var(--surface-alt);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  display: block;
}
/* Wraps each .photo-gallery img (card.js's photoGalleryHtml) so it's
   click/keyboard-activatable to open the lightbox below; plain button
   reset since all the actual visual styling lives on the img itself via
   the descendant selector above. */
.photo-gallery-item {
  border: none;
  background: none;
  padding: 0;
  margin: 0;
  display: block;
  width: 100%;
  cursor: zoom-in;
}
/* Same gallery inside the admin dashboard's card detail accordion, at a
   size that suits a scan-and-verify glance rather than the public card
   page's showcase: smaller tiles, no bottom margin (the surrounding
   .fact-item already provides that spacing), and a hard cap on tile width
   so a card with a single photo doesn't stretch it across the whole
   panel. Everything else, including the contain/lightbox behavior, is
   inherited from the rules above. */
.dash-row-detail .photo-gallery {
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  margin-bottom: 0;
  margin-top: 6px;
  max-width: 460px;
}
.photo-gallery-item img { transition: opacity 0.15s; }
.photo-gallery-item:hover img { opacity: 0.85; }

/* Full-size photo lightbox (card.js's wireLightbox): one shared overlay,
   same structural pattern as the admin dashboard's .compose-overlay. */
.lightbox-overlay {
  position: fixed;
  inset: 0;
  background: rgba(20, 30, 26, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px;
  z-index: 100;
}
.lightbox-overlay[hidden] { display: none; }
.lightbox-overlay img {
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
}
.lightbox-close {
  position: absolute;
  top: 18px;
  right: 22px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.lightbox-close:hover { background: rgba(0, 0, 0, 0.8); }
.card-thumb {
  width: 100%;
  aspect-ratio: 16 / 10;
  /* Same reasoning as .photo-gallery img above: board-grid thumbnails
     were cropping off the top of text-heavy flyer images (a card's own
     title text, in some real cases), which is worse here than on the
     detail page since this is often a family's only look at the card
     before deciding whether to click in. */
  object-fit: contain;
  background: var(--surface-alt);
  display: block;
  border-radius: var(--radius-sm);
}

/* ---------- Card detail: name-led badge row, email copy feedback ---------- */

.card-detail-header h1 {
  margin: 4px 0 0;
}
.detail-badges-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}
.connect-item .copy-hint {
  margin-left: auto;
  font-size: 11px;
  font-weight: 500;
  color: var(--text-faint);
}
.connect-item.is-copied {
  border-color: var(--ocean);
  background: var(--ocean-light);
}
