/* ── Tokens — monochrome/grayscale system ── */
:root {
  --white:      #FFFFFF;
  --off:        #F9FAFB;
  --surface:    #F3F4F6;
  --border:     #E5E7EB;
  --muted:      #6B7280;
  /* --muted measures 4.83:1 on white, which passes, but only 4.39:1 on
     --surface — under the 4.5:1 AA needs for body text. Anything that puts
     muted-weight text on a --surface chip or panel takes this instead
     (5.24:1 there, 5.76:1 on white). */
  --muted-on-surface: #5F6675;
  /* Text tier. Was #9CA3AF, which measures 2.54:1 on white — below the 4.5:1
     WCAG AA needs for body text, and it was carrying the AGE/WHEN/WHERE labels
     on every card. The #767D89 that replaced it was recorded here as "~4.6:1";
     measured with axe-core against the live site it is 4.14:1, so it never
     actually cleared AA. #6D7583 measures 4.64:1 and still reads lighter than
     --muted. Non-text uses (scrollbar, borders) take --subtle-ui instead so
     they keep the old lightness. */
  --subtle:     #6D7583;
  --subtle-ui:  #9CA3AF;
  --body:       #111827;
  --accent:     #111827;
  /* Status traffic light, shared by the Data review header dot and the
     Supabase pill on review.html so the same fact is never two colours. */
  --dot-green:  #15803D;
  --dot-yellow: #CA8A04;
  --dot-red:    #DC2626;
  --radius:     6px;
  --radius-lg:  12px;
  --max:        1080px;
  --gap:        2rem;
  --shadow-sm:  0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow-md:  0 4px 12px rgba(0, 0, 0, 0.05);
  --shadow-lg:  0 12px 32px rgba(0, 0, 0, 0.08);
}

/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; scroll-behavior: smooth; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  background: var(--white);
  color: var(--body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; }
img { display: block; max-width: 100%; }

/* ── Layout ── */
.wrap {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 1.5rem;
}
@media (min-width: 1024px) {
  .wrap { padding: 0 2rem; }
}

/* ── Header ── */
header {
  position: sticky;
  top: 0;
  /* above page content (incl. any stray third-party z-index), below modals */
  z-index: 100;
  border-bottom: 1px solid var(--border);
  padding: 1.5rem 0 1.1rem;
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.header-inner {
  display: flex;
  align-items: center;
  /* The split is done by margin-right:auto on .site-name, not by
     space-between: only some pages carry the submit link, and with
     space-between those pages spread every item evenly across the bar.
     That rule used to live on .beta-badge; when the badge was removed at
     launch it had to move here, or every nav link collapses against the
     wordmark on the left. */
  justify-content: flex-start;
  gap: 1rem;
}
.site-name {
  position: relative;
  z-index: 0;
  font-family: 'Baloo 2', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 1.625rem;
  font-weight: 700;
  letter-spacing: 0;
  text-decoration: none;
  color: #2b2420;
  line-height: 1;
  /* the torch "l" is an inline-block, so without this the wordmark can break
     across lines ("el" / "pys") when the header gets tight */
  white-space: nowrap;
}
/* Sparkles from the full lockup. The lockup's field is 3.5em tall, which
   would spill past the header's padding and shrink each star to ~4px, so the
   header uses a shorter field with fewer, proportionally larger stars.
   z-index -1 inside .site-name's own stacking context keeps them behind the
   letters but still above the header background. */
.site-name-sparkles {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 3.1em;
  height: 2.0em;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: -1;
}
.site-name-l {
  position: relative;
  display: inline-block;
}
/* The "l" doubles as the torch handle: the bowl sits across its top and the
   flame rises above it. Sizes are in em so the mark tracks the wordmark's
   font-size, preserving the proportions of the full logo lockup.
   The offsets are the lockup's (-0.333em / 0.233em) shifted up by 0.3em:
   .site-name sets line-height 1 against Baloo 2's natural 1.6 content area,
   and that negative half-leading lifts the glyph by half the difference. */
.site-name-flame {
  position: absolute;
  width: 0.5em;
  height: auto;
  top: -0.633em;
  left: 0.133em;
  transform: translateX(-50%);
}
.site-name-torch-base {
  position: absolute;
  width: 0.333em;
  height: auto;
  top: -0.067em;
  left: 0.133em;
  transform: translateX(-50%);
}
.header-submit-link,
.header-auth-link,
.header-signup-btn,
.header-admin-link,
.header-logout-btn {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--body);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.3rem 0.75rem;
  text-decoration: none;
  white-space: nowrap;
  font-family: inherit;
  cursor: pointer;
}
.header-submit-link:hover,
.header-auth-link:hover,
.header-signup-btn:hover,
.header-admin-link:hover,
.header-logout-btn:hover { background: var(--off); }
/* Admin mode, row 1 (Approve / Submit / Edit / Log out). Wraps rather than
   running off the right edge on a phone — it used to be a fixed one-line row
   about 530px wide. */
.header-admin-main {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
  min-width: 0;
}
.header-admin-sub {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
  padding-top: 0.75rem;
  margin-top: 0.5rem;
}
/* Traffic light on the Data review link.
   red    — the database looks about to pause; act now
   yellow — flags waiting on a human, or the project is already paused
   green  — nothing waiting, nothing wrong
   grey   — the status couldn't be fetched; deliberately not green.
   Each colour also carries a title and aria-label, since colour alone is not
   a signal everyone can read. */
.header-admin-link .status-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  margin-left: 0.4rem;
  vertical-align: 0.05em;
  flex-shrink: 0;
}
.status-dot.is-green   { background: var(--dot-green); }
.status-dot.is-yellow  { background: var(--dot-yellow); }
.status-dot.is-red     { background: var(--dot-red); }
.status-dot.is-unknown { background: transparent; border: 1px solid var(--subtle-ui); }

/* Result line for "Delete unused photos". It gets a line of its own under the
   buttons (full-width flex item, right-aligned) so a long message can't push
   the row's buttons out of line with the row above; empty, it takes no space. */
.header-admin-sub { flex-wrap: wrap; row-gap: 0.35rem; }
.header-cleanup-msg { font-size: 0.8125rem; flex-basis: 100%; text-align: right; }
.header-cleanup-msg:empty { display: none; }

/* ── Mobile header ──
   .header-inner is a single non-wrapping flex row. On a 375px phone the
   wordmark, badge and three nav items add up to roughly 494px, so the row
   either clipped or forced the whole page to scroll sideways. Below 640px the
   nav wraps onto its own line under the wordmark. "Submit an opportunity"
   stays visible at the same smaller size as the other links: hiding it left
   phone visitors with no route to /submit at all. */
@media (max-width: 640px) {
  header { padding: 1rem 0 0.85rem; }
  .header-inner {
    flex-wrap: wrap;
    row-gap: 0.6rem;
    column-gap: 0.6rem;
  }
  .site-name { font-size: 1.375rem; }
  .header-submit-link,
  .header-auth-link,
  .header-signup-btn,
  .header-admin-link,
  .header-logout-btn {
    font-size: 0.75rem;
    padding: 0.28rem 0.6rem;
  }
  .header-admin-sub { flex-wrap: wrap; row-gap: 0.5rem; }
}
/* Tightened so the wordmark and all three links still share one row down to
   a 375px phone. */
@media (max-width: 389px) {
  .header-submit-link,
  .header-auth-link,
  .header-signup-btn { padding: 0.28rem 0.5rem; }
  .header-inner { column-gap: 0.4rem; }
}
/* Narrower than that, the wordmark takes the first row alone and the links
   share the second, rather than "Sign up" wrapping onto a row of its own.
   The 100% right margin forces the break without widening the wordmark's
   link area. Not in admin mode, where the Admin badge sits beside the
   wordmark. */
@media (max-width: 374px) {
  .header-inner > .site-name:not(:has(+ .admin-badge)) { margin-right: 100%; }
}

/* ── Alerts ──
   One canonical error/success treatment. login/signup/account/admin-login each
   carried their own copy of these colours; submit.html and map.html had none,
   and rendered failure and success in the same near-black. */
.alert-error,
.form-status.error {
  background: #FEF2F2;
  border: 1px solid #FCA5A5;
  color: #991B1B;
}
.alert-success,
.form-status.success {
  background: #ECFDF5;
  border: 1px solid #6EE7B7;
  color: #065F46;
}
/* Shared base for the status banners the auth pages share. Pages that need a
   different size or spacing (submit.html, account.html) override it locally. */
.form-status {
  font-size: 0.9375rem;
  padding: 1rem;
  border-radius: var(--radius);
  display: none;
}

/* ── Load failure / empty state ──
   Shown when the database can't be reached, in place of a grid that would
   otherwise sit on "Loading…" forever. */
.load-error {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: 1.25rem;
  font-size: 0.875rem;
  color: var(--muted);
  line-height: 1.6;
  max-width: 60ch;
}
.load-error-title {
  font-weight: 700;
  color: var(--body);
  margin-bottom: 0.35rem;
}
.load-error a { color: var(--body); }

/* ── Modal ── */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}
.modal-box {
  background: var(--white);
  border-radius: 10px;
  padding: 1.75rem 2rem 1.5rem;
  max-width: 380px;
  width: calc(100% - 2.5rem);
  position: relative;
  box-shadow: 0 8px 32px rgba(0,0,0,0.18);
}
.modal-close {
  position: absolute;
  top: 0.85rem;
  right: 0.9rem;
  background: none;
  border: none;
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
  color: var(--muted);
  padding: 0.2rem 0.4rem;
  border-radius: 4px;
}
.modal-close:hover { color: var(--body); background: var(--off); }
.modal-title { font-size: 1.0625rem; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 0.5rem; padding-right: 1.5rem; }
.modal-body  { font-size: 0.9375rem; color: #444; line-height: 1.55; margin-bottom: 1.5rem; }
.modal-actions { display: flex; gap: 0.65rem; justify-content: flex-end; }
/* Optional text field in a dialog (the Reject reason on admin-review.html). */
.modal-input { margin: -0.75rem 0 1.25rem; }
.modal-input-label { display: block; font-size: 0.8125rem; font-weight: 600; margin-bottom: 0.35rem; }
.modal-input textarea {
  width: 100%; box-sizing: border-box; min-height: 4.5rem; resize: vertical;
  font: inherit; font-size: 0.9375rem; line-height: 1.45;
  padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: var(--radius);
}
.modal-input textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.modal-input-hint { font-size: 0.8125rem; color: var(--muted); margin-top: 0.35rem; }
.modal-btn-action:disabled { opacity: 0.4; cursor: not-allowed; }
.modal-btn-cancel {
  font-family: inherit; font-size: 0.9375rem;
  color: var(--muted); background: none;
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.55rem 1.1rem; cursor: pointer;
}
.modal-btn-cancel:hover { color: var(--body); border-color: #aaa; }
.modal-btn-action {
  font-family: inherit; font-size: 0.9375rem; font-weight: 600;
  color: var(--white); background: var(--body);
  border: none; border-radius: var(--radius);
  padding: 0.55rem 1.1rem; cursor: pointer;
}
.modal-btn-action:hover { opacity: 0.85; }
.modal-btn-action.danger { background: #991B1B; }
.modal-btn-action.danger:hover { background: #7F1D1D; opacity: 1; }

/* The beta badge that used to sit beside the wordmark was removed at launch.
   It carried the margin-right:auto that splits the header into a left and a
   right group, so that has to live on whatever is now last in the left group:
   the wordmark normally, or the admin-mode label when one is present.
   Scoped to .header-inner because admin.html, review.html and admin-review.html
   have a .site-name inside their own header markup, which this must not touch. */
.header-inner > .site-name { margin-right: auto; }
/* supabase-auth.js inserts .admin-badge directly after .site-name, so when it
   exists it becomes the last element of the left group and takes the spacer
   over. Without this the admin label would be pushed across to the nav. */
.header-inner > .admin-badge { margin-right: auto; }
.header-inner > .site-name:has(+ .admin-badge) { margin-right: 0; }

/* Signals that the header is in admin mode. Plain text, not a badge — it is a
   statement of what you are logged in as, not a label on the product. Takes the
   wordmark's typeface, size, weight and colour so "elpys Admin" reads as one
   line rather than two mismatched pieces. */
.admin-badge {
  font-family: 'Baloo 2', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 1.625rem;
  font-weight: 700;
  letter-spacing: 0;
  line-height: 1;
  color: #2b2420;
  /* the 1rem header gap already clears the sparkle field's overhang */
  margin-left: 0;
  flex-shrink: 0;
}

/* ── Beta banner — remove once site launches ── */
#beta-banner {
  position: relative;
  background: var(--off);
  border-bottom: 1px solid var(--border);
  padding: 0.45rem 2.75rem;
  text-align: center;
  font-size: 0.8125rem;
  color: #555;
}
#beta-banner a { color: var(--body); }
#beta-banner-close {
  position: absolute;
  right: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  font-size: 1.125rem;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  padding: 0 0.25rem;
}
#beta-banner-close:hover { color: var(--body); }


/* ── Hero ── */
.hero {
  padding: 4.5rem 0 3.5rem;
  border-bottom: 1px solid var(--border);
}
.hero-label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 1rem;
}
.hero h1 {
  font-size: clamp(1.75rem, 5vw, 3rem);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.03em;
  max-width: 28ch;
  margin-bottom: 1.25rem;
}
.hero p {
  font-size: 1.0625rem;
  color: #444;
  max-width: 70ch;
  line-height: 1.65;
}

/* ── How it works ── */
.how {
  padding: 2.5rem 0;
  border-bottom: 1px solid var(--border);
}
.how-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1.5rem;
  margin-top: 1.5rem;
}
.how-step {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.how-step .step-num {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.how-step .step-title {
  font-size: 0.9375rem;
  font-weight: 600;
}
.how-step .step-desc {
  font-size: 0.875rem;
  color: #555;
  line-height: 1.5;
}

/* ── Section headers ── */
.section {
  padding: 3rem 0 1rem;
}
.section-header {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin-bottom: 1.5rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--border);
}
.section-header h2 {
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.section-header .section-desc {
  font-size: 0.875rem;
  color: var(--muted);
}

/* ── Cards ── */
.cards {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.25rem;
  margin-bottom: 3rem;
}
.card {
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: #D1D5DB;
}
.card-body { padding: 1.5rem; }

/* ── Card photo (Option C: photo-forward card) ──
   aspect-ratio keeps every card the same height across a row regardless of
   the source image's own dimensions; object-fit: cover crops rather than
   distorting. .card-photo-fallback fills the same box with the category
   icon (categoryFallbackIconHtml, supabase-client.js) when there's no
   uploaded cover photo — never a blank box, never a stock photo. */
.card-photo {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--surface);
}
.card-photo-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Attribution some photo licenses require. Sits over the photo's bottom-right
   corner so it costs the card no height; the full credit is also under the
   photo on the detail page. */
.card-photo-credit {
  position: absolute;
  right: 0;
  bottom: 0;
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 0.625rem;
  line-height: 1.3;
  padding: 0.15rem 0.4rem;
  color: #fff;
  background: rgba(0, 0, 0, 0.5);
  border-top-left-radius: 4px;
}
.card-photo-fallback {
  display: block;
  width: 100%;
  height: 100%;
}
.card-pill {
  position: absolute;
  top: 0.6rem;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.3rem 0.65rem;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.92);
  color: var(--body);
  box-shadow: var(--shadow-sm);
  max-width: calc(100% - 1.2rem);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.card-pill-category { left: 0.6rem; }
.card-pill-date {
  right: 0.6rem;
  background: var(--body);
  color: var(--white);
}
.filter-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 0;
}
/* Pairs the "Category" label with its buttons, mirroring .time-filter-group,
   so the label sits outside #filters - the category buttons are rebuilt from
   live data with .filter-btn:not(:first-child), which would eat a label
   placed inside. */
.filter-group {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
}
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
}
.total-count {
  font-size: 0.8125rem;
  color: var(--muted);
  white-space: nowrap;
  line-height: 1;
}
.section-count {
  font-size: 0.8125rem;
  color: var(--muted);
  margin-left: auto;
}
@media (max-width: 640px) {
  .cards { grid-template-columns: 1fr; }
}
@media (min-width: 1024px) {
  .cards { grid-template-columns: repeat(3, 1fr); }
}

.age-filter {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.75rem;
}
.age-filter input {
  width: 5rem;
  font-size: 0.8125rem;
  padding: 0.3rem 0.6rem;
  border-radius: 20px;
  border: 1px solid var(--border);
  color: var(--body);
}
.age-filter input:focus {
  outline: none;
  border-color: #aaa;
}
.age-filter-hint {
  font-size: 0.75rem;
  color: var(--muted);
}
/* Day picker: one dropdown instead of eleven buttons, styled like the age box. */
.day-select {
  font-family: inherit;
  font-size: 0.8125rem;
  padding: 0.3rem 1.8rem 0.3rem 0.75rem;
  border-radius: 20px;
  border: 1px solid var(--border);
  color: var(--body);
  background-color: var(--white);
  cursor: pointer;
}
.day-select:focus { outline: none; border-color: #aaa; }

.age-cond-phrase.highlighted {
  background: var(--surface);
  font-weight: 700;
  border-bottom: 2px solid var(--body);
  border-radius: 2px;
  padding: 0 0.2em;
}

.card-name {
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
}
.card-name:hover {
  text-decoration: underline;
}

/* Single condensed meta line (e.g. "Ages 13+ · Saturday mornings"),
   replacing the old three-row Age/When/Where grid — "Where" now lives only
   on the detail page. */
.card-meta-line {
  font-size: 0.8125rem;
  color: var(--subtle);
  margin: 0.35rem 0 0.75rem;
  line-height: 1.4;
}
.card-meta-sep { color: var(--border); }

.card-desc {
  font-size: 0.9rem;
  color: #444;
  line-height: 1.6;
  margin-bottom: 1.125rem;
}
/* Clamped to two lines on the card face — the full description is one click
   away on the detail page. */
.card-desc-clamp {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card-more-info { width: 100%; text-align: center; }

.steps-label {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.5rem;
}
.steps {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-bottom: 1.125rem;
}
.steps li {
  font-size: 0.875rem;
  color: #333;
  display: grid;
  grid-template-columns: 1.25rem 1fr;
  gap: 0.5rem;
  align-items: baseline;
  line-height: 1.5;
}
.steps li span:last-child {
  /* A step's text is data-driven and can contain a long unbroken URL (e.g. a
     Google Form link). Without this, that URL's min-content width forces the
     whole .detail-right column past its 300px flex-basis (flex-shrink: 0),
     squeezing .detail-main down to almost nothing. `anywhere`, not
     `break-word`, because only `anywhere` actually shrinks the min-content
     contribution — `break-word` looks the same once wrapped but still
     reports the unbroken width to the flex layout. */
  overflow-wrap: anywhere;
}
.steps li .n {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--muted);
  text-align: right;
}

/* Note */
.card-note {
  font-size: 0.8125rem;
  color: var(--muted);
  line-height: 1.5;
  margin-bottom: 1rem;
  padding-left: 0.75rem;
  border-left: 2px solid var(--border);
}

/* ── Skip link ──
   Off-screen until focused, then pinned to the top-left. Positioned rather
   than display:none so it stays in the tab order — a hidden skip link is the
   same as no skip link. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 1000;
  background: var(--body);
  color: var(--white);
  font-size: 0.875rem;
  font-weight: 600;
  padding: 0.65rem 1.1rem;
  border-radius: 0 0 var(--radius) 0;
  text-decoration: none;
}
.skip-link:focus {
  left: 0;
}

/* CTA button */
.btn {
  display: inline-block;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--white);
  background: var(--body);
  padding: 0.55rem 1.1rem;
  border-radius: var(--radius);
  text-decoration: none;
  transition: opacity 0.12s;
}
.btn:hover { opacity: 0.75; }
.btn-ghost {
  background: transparent;
  color: var(--body);
  border: 1px solid var(--border);
}
.btn-ghost:hover { background: var(--off); opacity: 1; }

.card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

/* ── Filter bar ── */
.filter-btn {
  font-size: 0.8125rem;
  font-weight: 500;
  padding: 0.4rem 0.9rem;
  border-radius: 20px;
  border: 1px solid transparent;
  background: var(--surface);
  /* Not --muted: on --surface that pair measures 4.39:1, just under AA. */
  color: var(--muted-on-surface);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.filter-btn:hover {
  background: var(--border);
  color: var(--body);
}
.filter-btn.active {
  background: var(--body);
  color: var(--white);
  border-color: var(--body);
}
.filter-btn.active:hover {
  background: var(--body);
  opacity: 0.9;
}

/* ── Footer ── */
footer {
  border-top: 1px solid var(--border);
  padding: 2rem 0;
  margin-top: 2rem;
}
/* The note and the links are two different things — one is advice about the
   listings, the other is site navigation — but they sat as one block of grey
   text. Stacking them with a rule between separates them.
   The wrap gap stays for narrow screens, where the row breaks anyway. */
.footer-inner {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--border);
}
footer p {
  font-size: 0.8125rem;
  color: var(--muted);
}
footer a {
  font-size: 0.8125rem;
  color: var(--muted);
  text-decoration: underline;
}

/* ── Responsive ── */
@media (max-width: 640px) {
  .hero { padding: 2.5rem 0 1.75rem; }
  .hero-break { display: none; }
  .hero p { font-size: 1rem; }
  .how { padding: 1.75rem 0; }
  .how-grid { margin-top: 1rem; gap: 1rem; }
  .section { padding: 2rem 0 0.5rem; }
  .section-header { flex-direction: column; align-items: flex-start; gap: 0.2rem; margin-bottom: 1rem; }
  .section-header .section-desc { display: none; }
  .section-count { margin-left: 0; }
  .card-body { padding: 1.25rem; }
  .header-sub { display: none; }
  footer { padding: 1.5rem 0; margin-top: 1.25rem; }
}

@media (max-width: 480px) {
  .wrap { padding: 0 1rem; }
  .hero { padding: 2rem 0 1.25rem; }
  .hero h1 { font-size: clamp(1.625rem, 7vw, 2rem); margin-bottom: 0.875rem; }
  .section { padding: 1.75rem 0 0.5rem; }
  .age-filter-hint { display: none; }
  .time-filter-label { min-width: 0; width: 100%; }
  footer { padding: 1.25rem 0; }
}

/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}

/* ── Detail pages (opportunity "More info") ── */
.detail-section {
  padding: 3rem 0 4rem;
}
.back-link {
  display: inline-block;
  font-size: 0.8125rem;
  color: var(--muted);
  text-decoration: none;
  margin-bottom: 1.5rem;
}
.back-link:hover {
  color: var(--body);
}
.detail-section h1 {
  font-size: clamp(1.75rem, 4vw, 2.5rem);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.02em;
  margin-bottom: 0.75rem;
  max-width: 24ch;
}
.detail-tag {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 1.5rem;
  display: block;
}
/* Loading, not-found and load-failure states on the detail page. */
.detail-placeholder {
  font-size: 0.9375rem;
  color: var(--muted);
  line-height: 1.6;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: 1.25rem;
  max-width: 60ch;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  align-items: flex-start;
}
.detail-placeholder-title {
  font-size: 1rem;
  font-weight: 700;
  color: var(--body);
}
.detail-placeholder-link {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--body);
}
.detail-desc {
  font-size: 0.9375rem;
  color: #444;
  line-height: 1.7;
  max-width: 60ch;
}
.detail-contact {
  margin-top: 1.75rem;
  max-width: 60ch;
}
.contact-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  font-size: 0.875rem;
  color: #333;
}
.contact-list a {
  text-decoration: underline;
}
/* ── Detail page cover photo + gallery ──
   Fallback rule matches the homepage card exactly: categoryFallbackIconHtml
   (supabase-client.js) fills the same box when there's no cover photo. */
.detail-cover {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  max-height: 420px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
  margin-bottom: 1.25rem;
}
.detail-cover-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.detail-cover-fallback { display: block; width: 100%; height: 100%; }
.detail-photo-credit {
  font-size: 0.75rem;
  color: var(--subtle);
  margin: -0.85rem 0 1.25rem;
}
.detail-cover-btn {
  all: unset; display: block; width: 100%; height: 100%; cursor: zoom-in;
}
.detail-cover-btn:focus-visible { outline: 2px solid var(--body); outline-offset: 2px; }

.detail-gallery { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-bottom: 1.75rem; }
.gallery-thumb {
  width: 84px; height: 84px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden; background: var(--surface); cursor: pointer;
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.gallery-thumb:hover { box-shadow: var(--shadow-sm); border-color: #D1D5DB; }
.gallery-thumb:focus-visible { outline: 2px solid var(--body); outline-offset: 2px; }
.gallery-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ── Lightbox ──
   Plain, dependency-free — no new front-end library. Keyboard: Escape
   closes, ArrowLeft/ArrowRight step. Focus is trapped inside while open (see
   opportunities-detail.html's onLightboxKeydown) and returned to whatever
   opened it on close. */
.lightbox {
  position: fixed; inset: 0; z-index: 2000;
  background: rgba(17, 24, 39, 0.92);
  display: flex; align-items: center; justify-content: center;
}
.lightbox[hidden] { display: none; }
.lightbox-figure {
  max-width: min(92vw, 1100px); max-height: 86vh;
  display: flex; flex-direction: column; align-items: center; gap: 0.75rem;
  margin: 0;
}
.lightbox-img { max-width: 100%; max-height: 76vh; object-fit: contain; border-radius: var(--radius); }
.lightbox-counter { color: var(--white); font-size: 0.8125rem; opacity: 0.85; }
.lightbox-btn {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; border-radius: 50%; border: none;
  background: rgba(255, 255, 255, 0.12); color: var(--white);
  font-size: 1.5rem; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.lightbox-btn:hover { background: rgba(255, 255, 255, 0.22); }
.lightbox-btn:focus-visible { outline: 2px solid var(--white); outline-offset: 2px; }
.lightbox-prev { left: 1rem; }
.lightbox-next { right: 1rem; }
.lightbox-close {
  position: absolute; top: 1rem; right: 1rem;
  width: 40px; height: 40px; border-radius: 50%; border: none;
  background: rgba(255, 255, 255, 0.12); color: var(--white);
  font-size: 1.25rem; line-height: 1; cursor: pointer;
}
.lightbox-close:hover { background: rgba(255, 255, 255, 0.22); }
.lightbox-close:focus-visible { outline: 2px solid var(--white); outline-offset: 2px; }
@media (max-width: 640px) {
  .lightbox-btn { width: 38px; height: 38px; }
  .gallery-thumb { width: 68px; height: 68px; }
}

.detail-layout {
  display: flex;
  gap: 2rem;
  align-items: flex-start;
}
.detail-main {
  flex: 1 1 320px;
}
.detail-right {
  flex: 0 0 300px;
  display: flex;
  flex-direction: column;
}
/* Before-your-first-visit block on a listing's detail page. Sits below the
   contact details, deliberately quiet — it is reference, not an alarm. */
.detail-safety {
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
  max-width: 62ch;
}
.detail-safety-title {
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--subtle);
  margin-bottom: 0.6rem;
}
.detail-safety-list {
  margin: 0;
  padding-left: 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.detail-safety-list li {
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--body);
}
.detail-safety-foot {
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--muted);
  margin-top: 0.9rem;
}
.detail-safety-foot a { color: var(--body); }

.detail-box {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.5rem;
  margin-top: 1.25rem;
}
@media (max-width: 640px) {
  .detail-layout { flex-direction: column; }
  .detail-right { flex: 1 1 auto; width: 100%; }
}

/* ── Map page ── */
.map-section {
  padding: 3rem 0 4rem;
}
.map-intro {
  font-size: 0.9375rem;
  color: #444;
  line-height: 1.6;
  max-width: 65ch;
  margin-bottom: 1.5rem;
}
.route-search {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--white);
  box-shadow: var(--shadow-sm);
  padding: 1rem;
  margin-bottom: 1.25rem;
}
.route-search label {
  display: block;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin-bottom: 0.5rem;
}
.route-search-row {
  display: flex;
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
  background: var(--white);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.route-search-row:focus-within {
  border-color: var(--body);
  box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);
}
.route-search input {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  font-size: 0.875rem;
  border: none;
  background: transparent;
  padding: 0.7rem 1.1rem;
}
.route-search input:focus {
  outline: none;
}
.route-search button {
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--white);
  background: var(--body);
  border: none;
  border-radius: 0;
  padding: 0.7rem 1.3rem;
  cursor: pointer;
  transition: opacity 0.12s ease;
  flex-shrink: 0;
}
.route-search button:hover {
  opacity: 0.82;
}
.route-status {
  font-size: 0.8125rem;
  color: var(--muted);
  line-height: 1.5;
  margin-top: 0.7rem;
  padding-left: 0.7rem;
  border-left: 2px solid transparent;
}
/* Success and failure used to differ only by a border-left colour against the
   same near-black text, which was easy to miss. These take the shared alert
   palette; the border-left accent and padding stay so the layout is unchanged. */
.route-status.success {
  color: #065F46;
  font-weight: 600;
  border-left-color: #6EE7B7;
}
.route-status.error {
  color: #991B1B;
  font-weight: 600;
  border-left-color: #FCA5A5;
}
.map-layout {
  display: flex;
  gap: 1.25rem;
  align-items: flex-start;
  margin-bottom: 1rem;
}
.map-sidebar {
  flex: 0 0 240px;
  max-height: 65vh;
  min-height: 360px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  scrollbar-width: thin;
  scrollbar-color: var(--subtle-ui) transparent;
}
.map-sidebar::-webkit-scrollbar {
  width: 8px;
}
.map-sidebar::-webkit-scrollbar-track {
  background: transparent;
}
.map-sidebar::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 999px;
}
.map-sidebar::-webkit-scrollbar-thumb:hover {
  background: var(--subtle-ui);
}
.map-list-item-wrap {
  border-bottom: 1px solid var(--border);
  border-left: 3px solid transparent;
  transition: background-color 0.12s ease, border-left-color 0.12s ease;
}
.map-sidebar .map-list-item-wrap:last-child {
  border-bottom: none;
}
.map-list-item {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  width: 100%;
  text-align: left;
  padding: 0.65rem 0.9rem 0.65rem 0.75rem;
  cursor: pointer;
}
.map-list-item:hover {
  background: var(--off);
}
.map-list-item-wrap.active .map-list-item:hover {
  background: transparent;
}
.map-list-item-wrap.active {
  background: var(--off);
  border-left-color: var(--body);
}
.map-list-name {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--body);
  text-decoration: none;
}
.map-list-name:hover {
  text-decoration: underline;
}
.map-list-date {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--body);
}
.map-list-desc {
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--muted);
}
/* The row stopped being a button (see map.html), so selecting an opportunity
   needs a control of its own. Deliberately quiet — it sits under the
   description and must not compete with the organisation name, which is the
   thing people are scanning for. --muted-on-surface, not --muted: on --surface
   that pair measures 5.24:1, where --muted would be 4.39:1 and fail AA.
   min-height clears the 24px WCAG 2.5.8 target size, which 11px text and
   0.2rem of padding would not on their own. */
.map-list-show {
  align-self: flex-start;
  margin-top: 0.3rem;
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  font-family: inherit;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--muted-on-surface);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.map-list-show:hover {
  background: var(--border);
  color: var(--body);
}
.map-list-live-note {
  font-size: 0.75rem;
  color: var(--muted);
  line-height: 1.4;
  padding: 0 0.9rem 0.65rem;
}
.map-list-live-note a {
  text-decoration: underline;
}
.map-main {
  flex: 1 1 auto;
  min-width: 0;
}
@media (max-width: 640px) {
  /* align-items: flex-start on .map-layout sizes children to their content once
     the row stacks, which collapsed .map-main to its 1px border - the map was
     a hairline on every phone. The sidebar escaped it only because it was given
     an explicit width. Stretch fixes both; the width below is belt and braces.
     min-height: 0 lets the 200px max-height above actually win over the base
     rule's min-height: 360px. */
  .map-layout { flex-direction: column; align-items: stretch; }
  .map-sidebar { flex: 1 1 auto; width: 100%; min-height: 0; max-height: 200px; }
  .map-main { width: 100%; }
  .route-search-row { flex-direction: column; }
  .route-search button { width: 100%; }
}
/* Touch screens: shown briefly when one finger tries to move the map. */
.map-touch-hint {
  position: absolute;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  text-align: center;
  font-size: 1rem;
  font-weight: 600;
  color: #fff;
  background: rgba(0, 0, 0, 0.45);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.map-touch-hint.show { opacity: 1; }
#map {
  width: 100%;
  height: 65vh;
  min-height: 360px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  margin-bottom: 1rem;
}
.map-disclaimer {
  font-size: 0.8125rem;
  color: var(--muted);
  line-height: 1.5;
}
.leaflet-popup-content-wrapper {
  border-radius: var(--radius);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  box-shadow: var(--shadow-lg);
}
.leaflet-popup-tip {
  box-shadow: none;
}
.map-popup-title,
.leaflet-popup-content .map-popup-title {
  display: block;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--body);
  text-decoration: none;
  margin-bottom: 0.2rem;
}
.map-popup-title:hover,
.leaflet-popup-content .map-popup-title:hover {
  text-decoration: underline;
}
.map-popup .map-popup-tag {
  display: block;
  font-size: 0.75rem;
  color: var(--muted);
  margin-bottom: 0.4rem;
}
.map-popup a:not(.map-popup-title) {
  display: block;
  font-size: 0.8125rem;
  color: var(--body);
  text-decoration: underline;
  margin-top: 0.2rem;
}
/* ── Mini map previews (detail pages + main page launcher) ── */
/* Leaflet gives its panes and controls z-index 400-1000. A map container that
   makes no stacking context of its own lets those escape into the root one,
   where they paint over the sticky header. isolation: isolate contains them
   without touching layout. */
#map,
.mini-map-wrap {
  isolation: isolate;
}
.mini-map-wrap {
  cursor: pointer;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.mini-map-wrap:hover {
  transform: scale(1.03);
  box-shadow: var(--shadow-md);
}
.mini-map {
  width: 100%;
  height: 160px;
}
.mini-map-label {
  font-size: 0.8125rem;
  color: var(--muted);
  margin-top: 0.5rem;
}

/* ── Main page map launcher ── */
.hero-title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
}
.hero-text-col {
  flex: 1 1 300px;
}
.hero-map-launcher {
  flex: 0 0 240px;
}
.hero-map-launcher .mini-map {
  height: 230px;
}
.hero-map-launcher .mini-map-label {
  text-align: center;
  font-size: 0.7rem;
  color: var(--muted);
  font-weight: 500;
  margin-top: 0.9rem;
}
@media (max-width: 640px) {
  .hero-map-launcher { display: none; }
}
.map-modal-frame {
  width: 100%;
  height: 100%;
  border: none;
}

/* ── Timing filter ── */
.time-filter {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-top: 0.75rem;
}
.time-filter-group {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}
/* One shared label style so Category / Days / Time of day / Your age all line
   up in the same column down the left of the filter stack. */
.filter-label,
.time-filter-label,
.age-filter label {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--body);
  min-width: 5.5rem;
  flex-shrink: 0;
}

/* ============================================================
   LOADING STATES
   Skeletons and the inline button spinner. Authored as
   loading animations/elpys-skeleton.css and folded in here so
   every page picks them up from the stylesheet it already loads,
   rather than adding a second request to fourteen pages.
   Depends only on tokens defined above.

   Aug 2026 corrections pass: extensions 4 and 5 added. The card
   stack replaces the .skel-table shapes previously used on
   admin.html and review.html — both render stacked cards, not
   tables. .skel-table is kept for any genuinely tabular view.
   ============================================================ */

/* Elpys — skeleton loading state
   Drop-in. Depends only on existing tokens in styles.css:
   --off, --border, --surface, --muted, --subtle
   No JS, no libraries. Reduced motion holds a flat static frame. */

.skel {
  --skel-band: color-mix(in oklab, var(--border) 62%, var(--off));
  border-radius: 2px;
  background: linear-gradient(100deg, var(--off) 20%, var(--skel-band) 50%, var(--off) 80%);
  background-size: 240% 100%;
  animation: skel-sweep 2.1s linear infinite;
  /* browsers without color-mix fall back to the plain --off block below */
}
@supports not (background: color-mix(in oklab, red, blue)) {
  .skel { background: var(--off); animation-name: skel-fade; }
}

@keyframes skel-sweep { from { background-position: 180% 0 } to { background-position: -80% 0 } }
@keyframes skel-fade  { 0%,100% { opacity: .5 } 50% { opacity: .9 } }

/* Wave stagger — put on the container of a group of .skel blocks */
.skel-wave > *:nth-child(2) .skel, .skel-wave > .skel:nth-child(2) { animation-delay: .09s }
.skel-wave > *:nth-child(3) .skel, .skel-wave > .skel:nth-child(3) { animation-delay: .18s }
.skel-wave > *:nth-child(4) .skel, .skel-wave > .skel:nth-child(4) { animation-delay: .27s }
.skel-wave > *:nth-child(5) .skel, .skel-wave > .skel:nth-child(5) { animation-delay: .36s }
.skel-wave > *:nth-child(n+6) .skel, .skel-wave > .skel:nth-child(n+6) { animation-delay: .45s }

/* Sizes */
.skel-line   { height: 9px }
.skel-title  { height: 15px }
.skel-h1     { height: 24px }
.skel-meta   { height: 8px }
.skel-chip   { height: 19px; border-radius: 10px }
.skel-button { height: 36px; border-radius: 4px }
/* Matches .card-photo's 16:10 box, bled to the shell's edges the same way
   .card-photo does on the real card (see index.html's buildCardHtml). */
.skel-photo  { aspect-ratio: 16 / 10; height: auto; margin: -16px -16px 0; border-radius: 0 }

/* Shells */
.skel-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.skel-card hr, .skel-rule { border: 0; height: 1px; background: var(--border); margin: 4px 0 }
.skel-chips { display: flex; gap: 6px }
.skel-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 13px 0;
  border-bottom: 1px solid var(--border);
}
.skel-detail { display: flex; flex-direction: column; gap: 13px }

/* Screen-reader announcement — pair with aria-busy on the container */
.skel-sr {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .skel { animation: none !important; background: var(--off); opacity: .55 }
}


/* ─────────────────────────────────────────────────────────────
   EXTENSION 1 — Table / list rows
   For: admin.html (pending/approved queue), review.html (data_review_flags)
   Utilitarian sibling of .skel-card: no card shell, tighter rhythm,
   column widths driven by grid-template-columns so it lines up with
   the real <table>/list below it.
   ───────────────────────────────────────────────────────────── */

.skel-table {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  overflow: hidden;
}
.skel-thead,
.skel-trow {
  display: grid;
  gap: 16px;
  align-items: center;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
  /* override per view, e.g. style="--cols: 2.4fr 1fr 1fr 90px" */
  grid-template-columns: var(--cols, 2.4fr 1fr 1fr 90px);
}
.skel-trow:last-child { border-bottom: 0 }
.skel-thead { background: var(--off) }
.skel-thead .skel { animation-name: none; background: var(--border); opacity: .5 }
.skel-cell { height: 10px }
.skel-cell-strong { height: 12px }
.skel-pill { height: 18px; border-radius: 9px }

/* ─────────────────────────────────────────────────────────────
   EXTENSION 2 — Profile form
   For: account.html (interests / unsubscribed / availability)
   Fluid: every block is a % of the form column, no fixed px widths.
   ───────────────────────────────────────────────────────────── */

.skel-form { display: flex; flex-direction: column; gap: 26px; width: 100% }
.skel-field { display: flex; flex-direction: column; gap: 9px }
.skel-label { height: 9px; width: 28% }
.skel-input { height: 40px; border-radius: 4px; width: 100% }
.skel-checks { display: flex; flex-wrap: wrap; gap: 10px 14px }
.skel-check { display: flex; align-items: center; gap: 8px }
.skel-box { height: 15px; width: 15px; border-radius: 3px; flex: none }
.skel-checktext { height: 9px }

/* ─────────────────────────────────────────────────────────────
   EXTENSION 3 — Inline button spinner
   For: login.html, signup.html, admin-login.html, submit.html, feedback.html
   Inherits the button's text color; ems so it scales with the button.
   ───────────────────────────────────────────────────────────── */

.btn-spin {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  display: inline-block;
  vertical-align: -.16em;
  color: currentColor;
}
.btn-spin > svg { display: block; width: 100%; height: 100% }
.btn-spin circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 3.2;
  stroke-linecap: round;
  transform-origin: 20px 20px;
  animation: btn-spin 1.15s linear infinite;
}
.btn-spin .btn-spin-track { opacity: .28; animation: none; stroke-dasharray: none }
.btn-spin .btn-spin-arc { stroke-dasharray: 36 110 }
@keyframes btn-spin { to { transform: rotate(360deg) } }

/* Optional: button with spinner + label, keeps width stable */
.btn-loading { display: inline-flex; align-items: center; justify-content: center; gap: .5em }

@media (prefers-reduced-motion: reduce) {
  .btn-spin circle { animation: none !important }
  .btn-spin .btn-spin-arc { stroke-dasharray: none; opacity: .75 }
  .skel-thead .skel { opacity: .5 }
}


/* ═════════════════════════════════════════════════════════════
   EXTENSION 4 — Card-stack rows  (corrections pass, Aug 2026)
   Replaces the .skel-table usage on review.html and admin.html.
   Both pages render "name header → body → footer actions" cards,
   not tables. .skel-table stays in the file only for any genuine
   tabular view added later.
   For: review.html (.flag-card), admin.html (.submission-card)
   ───────────────────────────────────────────────────────────── */

.skel-stack { display: flex; flex-direction: column; gap: .85rem }

.skel-stackcard {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* top row: name (left) + field-name pill (right) */
.skel-cardhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
}
/* footer actions, separated by the card's top border */
.skel-cardfoot {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  margin-top: 2px;
}
.skel-btn-sm { height: 28px; border-radius: 4px }

/* suggested-value / admin-note callout box — a filled block, not a bar */
.skel-callout {
  height: 46px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: var(--off);
  animation: none;
}
/* embedded Leaflet map stand-in — admin.html pending cards only */
.skel-map {
  height: 200px;
  width: 100%;
  border-radius: var(--radius, 6px);
  border: 1px solid var(--border);
}
/* three short labeled inputs (slug / lat / lng) */
.skel-inputrow { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 10px }
.skel-input-sm { height: 32px; border-radius: 4px }

/* ═════════════════════════════════════════════════════════════
   EXTENSION 5 — Availability grid  (account.html)
   Mirrors .avail-grid's real 2.5rem 1fr 1fr 1fr template:
   a header row of 3 column labels, then 7 day rows of
   day-label + 3 unlabeled square toggles.
   ───────────────────────────────────────────────────────────── */

.skel-availgrid {
  display: grid;
  grid-template-columns: 2.5rem 1fr 1fr 1fr;
  gap: 6px;
  align-items: center;
  width: 100%;
}
.skel-availgrid > .skel-spacer { height: 1px }        /* empty top-left corner */
.skel-colhead { height: 8px; justify-self: center; width: 62% }
.skel-day     { height: 9px; width: 78% }
.skel-cell-sq { height: 30px; border-radius: 4px }

/* account.html block rhythm — matches .account-block spacing */
.skel-block { display: flex; flex-direction: column; gap: 12px }
.skel-hint  { height: 8px; width: 62% }
.skel-actions { display: flex; gap: 10px; flex-wrap: wrap }

@media (prefers-reduced-motion: reduce) {
  .skel-callout, .skel-map { opacity: .55 }
}

/* api-fetch.js: shown while the browser is sent through Vercel's security
   check and back, or when that check can't be passed. Fixed at the top so it
   is seen wherever the person was on a long form. */
.checkpoint-banner {
  position: fixed;
  top: 0.75rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 2000;
  width: min(92vw, 34rem);
  padding: 0.85rem 1.1rem;
  border-radius: var(--radius-lg);
  background: var(--accent);
  color: var(--white);
  font-size: 0.9375rem;
  line-height: 1.45;
  box-shadow: var(--shadow-lg);
}
.checkpoint-banner.error { background: #991B1B; }
.checkpoint-banner.info  { background: var(--white); color: var(--body); border: 1px solid var(--border); }
