/* ============================================================
   ECM Light Theme — mobile-first redesign
   Palette 3 (teal + amber on paper #FAFAFD — no purple):
     ink #0F172A · slate body #475569 · muted #64748B
     paper #FAFAFD · card #FFFFFF · line #E5E7F0
     teal #0D9488 · teal-deep #0F766E · teal-soft #F0FDFA
     amber #D97706 · amber-soft #FFFBEB
   Loads AFTER custom.css/globals.css; wins on the dark remaps.
   Light wins are pinned with `body` / `body.light` prefixes.

   NEVER write a comment-closing sequence inside a comment in this file -- not
   even to talk about it. A CSS comment ends at the first closing marker
   wherever it appears, so a stray one silently ends the comment early and the
   rest of those lines becomes the SELECTOR PRELUDE of the next rule, which the
   parser then throws away whole. That is not a specificity fight and no amount
   of prefixing recovers it: the rule is simply not in the CSSOM, so
   getComputedStyle reports the other stylesheet's value and
   `document.styleSheets` never lists the selector. This file has already been
   bitten by exactly that (see the note above `body .bg-white`), twice, once by
   a stray marker in prose and once by an explanatory comment ABOUT the stray
   marker that repeated it. Write `hover:bg-stone-X` and `hover:text-stone-Y`,
   or put a space around the slash.
   ============================================================ */

html {
  background-color: #FAFAFD;
}

body {
  --paper: #FAFAFD;
  --card: #FFFFFF;
  --line: #E5E7F0;
  --ink: #0F172A;
  --slate: #475569;
  /* Was #64748B. Measured on the lightest surface any muted token actually
     lands on -- .seat-open-badge's own #F1F5F9 fill -- that is 4.34:1, and
     4.45:1 on the homepage canvas (#F6F7FB). Both are below the 4.5:1 that
     11-12px metadata needs, so every --muted user failed, not just one.
     #5B6779 measures 5.24:1 on #F1F5F9, 5.36:1 on #F6F7FB and 5.74:1 on
     white. It also absorbs the old #94A3B8 (slate-400) step: on these
     backgrounds there is no room for two distinct muted greys that both
     clear AA, so both are now --muted. */
  --muted: #5B6779;
  --brand: #0D9488;
  --brand-deep: #0F766E;
  --brand-soft: #F0FDFA;
  --teal: #0D9488;
  --teal-soft: #E6F7F4;
  background-color: var(--paper);
  color: var(--ink);
  background-image:
    radial-gradient(ellipse 90% 30% at 50% -8%, rgba(13, 148, 136, 0.10), transparent 70%),
    radial-gradient(circle, rgba(15, 23, 42, 0.045) 1px, transparent 1px);
  background-size: auto, 26px 26px;
  background-attachment: fixed;
}

/* Headings / links back to light values */
body h1, body h2, body h3, body h4 { color: var(--ink); }
body a { color: var(--brand-deep); }
body a:hover { color: var(--brand); }

/* ---------- Surfaces: cards, shells, panels ---------- */
body .card,
body .aside-card,
body .info-card,
body .form-card,
body .table-card,
body .club-card,
body .event-card,
body .respond-card,
body .q-card,
body .d-card {
  background: var(--card);
  border-color: var(--line);
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05), 0 8px 24px -12px rgba(15, 23, 42, 0.18);
}
body .card:hover,
body .club-card:hover,
body .aside-card:hover { box-shadow: 0 2px 4px rgba(15, 23, 42, 0.06), 0 16px 32px -16px rgba(15, 23, 42, 0.25); }

/* Dark remaps in custom.css section 14 flipped back to light.
   .bg-stone-200 / .border-stone-100 / .text-stone-800 and the three
   hover:bg-stone-50 / hover:bg-stone-100 / hover:text-stone-900 pairs were
   dropped from BOTH layers: no template references any of them, so each
   existed twice to style nothing.

   This comment used to quote a class-name pair containing a comment-closing
   marker, and that marker ended the comment on the second line of it. The
   parser then read the rest of the sentence as a selector attached to the
   block below and dropped the pair, so `body .bg-white` never reached the
   CSSOM at all: 149 top-level rules parsed instead of 150,
   `body .bg-white` absent from [...styleSheets].cssRules, and every .bg-white
   element on /events/create/ computed to custom.css's dark #292524 -- 1.18:1
   against its own #0F172A text. Same shape, no specificity: (0,1,1) had
   already been out-specifying custom.css's (0,1,0) the whole time. */
body .bg-white { background-color: #FFFFFF; }
body .bg-stone-50 { background-color: #F8FAFC; }
body .bg-stone-100 { background-color: #F1F5F9; }
body .text-stone-100, body .text-stone-300,
body .text-stone-700, body .text-stone-900 { color: var(--ink); }
body .text-stone-400, body .text-stone-500 { color: var(--muted); }
body .text-stone-600 { color: var(--slate); }
body .border-stone-200, body .border-stone-300 { border-color: var(--line); }

/* Accent utilities for the club-head pages. These were written as Tailwind
   arbitrary values (text-[#0D9488], border-t-[#D97706], bg-[#0D9488]/10 ...),
   none of which exist in the compiled tailwind.css, so they silently fell
   back to currentColor and drew dark-ink accents on a light page. */
.accent-teal { color: #0F766E; }
.accent-amber { color: #B45309; }
.accent-teal-wash { background-color: rgba(13, 148, 136, 0.10); }
.accent-amber-wash { background-color: rgba(217, 119, 6, 0.10); }
.accent-top-teal { border-top-color: #0D9488; }
.accent-top-amber { border-top-color: #D97706; }

/* Template-local dark backgrounds → light cards/panels */
body .page-dark { background: var(--paper); color-scheme: light; }
body .err-card,
body .banned-card,
body .link-card { background: var(--card); border-color: var(--line); color: var(--ink); }
/* ---------- Text helpers (custom.css §17.3) ---------- */
body .text-strong { color: var(--ink); }
body .text-muted, body .d-sub { color: var(--muted); }
/* Was #94A3B8, which measures 2.56:1 on white -- and these are the 11-12px
   metadata labels on every ECM and club-head table, i.e. the smallest text on
   the page at the worst contrast. --muted is now 5.24:1 on the darkest fill
   these land on. */
body .text-faint, body .d-muted { color: var(--muted); }

/* ---------- The audience chip: ONE look, every page ---------- */
/* An event's audience (Girls only / Boys only / Everyone) is shown in four
   places: the event card, the details dialog, the event detail page's "Side"
   row, and the club page. They disagreed. Measured before this rule, the card
   rendered blue-100/blue-700 for Boys, pink-100/pink-700 for Girls and
   purple-100/purple-700 for Everyone, while the dialog rendered amber for all
   three -- so the same event was pink on a card and amber in the popup opened
   from that card.

   One colour for all three, because the DISTINCTION does not come from colour:
   each chip carries its own icon and its own word ("Girls only", not a bare
   hue), so a colour-blind reader gets the same information. Colour here is
   decoration, and decoration that disagrees between two views of one event is
   worse than decoration that is simply consistent.

   #8A5200 on #FEF3E2 measures 5.82:1, comfortably over the 4.5:1 that an 11px
   bold label needs. Unlayered, so it wins over the palette utilities it
   replaces. */
.audience-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  flex-shrink: 0;
  padding: 0.25rem 0.625rem;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1.25;
  border-radius: 9999px;
  border: 1px solid #FCD9A8;
  background: #FEF3E2;
  color: #8A5200;
}

/* Per-audience colours. Girls and Boys each get their own hue so a reader can
   tell them apart at a glance across a board of cards; the "Everyone" chip is
   gone entirely, because a co-ed event is now shown as TWO chips -- Girls and
   Boys -- or, when it is only in one room, a single "Co-ed" chip. Either
   states the same thing the old purple chip did without inventing a third
   colour that meant "both".

   Contrast, measured on the rendered chip: #9D174D on #FDF2F8 is 7.9:1 and
   #1E40AF on #EFF6FF is 8.6:1. Both clear AA with room to spare, which a
   lighter step (pink-600, blue-600) would not at 12px bold. */
.audience-chip--girls {
  border-color: #F9A8D4;
  background: #FDF2F8;
  color: #9D174D;
}
.audience-chip--boys {
  border-color: #93C5FD;
  background: #EFF6FF;
  color: #1E40AF;
}
.audience-chip--coed {
  border-color: #F5C26B;
  background: #FEF3E2;
  color: #8A5200;
}

/* Tailwind's text-slate-400 is 2.56:1 on white and 2.63:1 on the modal
   panel's own #FFFFFF, which fails AA at every size it is used for -- it is
   the "Date"/"Time"/"Location" label row in the event dialog, the "Event
   contact" heading, and the ISO-A4 caption. --muted is the same role one step
   darker and measures 5.24:1 on #F1F5F9 / 5.74:1 on white. Unlayered, so it
   beats the utility rather than losing to build order. */
body .text-slate-400 { color: var(--muted); }
body .page-title,
body .d-title,
body .d-h2,
body .form-title,
body .builder-title,
body .respond-title,
body .resp-title,
body .questions-title,
body .card-title,
body .contact-title,
body .tt-title,
body .profile-name,
body .err-title,
body .empty-title { color: var(--ink); }
body .page-sub,
body .form-sub,
body .builder-sub,
body .respond-sub,
body .resp-sub,
body .contact-sub,
body .empty-desc,
body .empty-sub,
body .q-label { color: var(--slate); }
body .d-label,
body .info-value,
body .detail-value,
body .cell-strong,
body .contact-name,
body .profile-username,
body .q-input { color: var(--ink); }
body .info-label,
body .detail-label,
body .cell-muted,
body .lbl,
body .progress-text,
body .legend,
body .d-fact-label,
body .aside-label,
body .questions-meta { color: var(--muted); }
body .detail-row,
body .info-row { border-color: var(--line); }
body .hub-count { color: var(--muted); }
body .hub-count strong { color: var(--brand-deep); }

/* ---------- Buttons: flat ink/brand, no gradients (anti-slop rule 6) ---------- */
body .btn {
  background-color: #FFFFFF;
  border-color: #CBD5E1;
  color: var(--ink);
  border-radius: 0.375rem;
}
body .btn:hover:not(:disabled) { background-color: #F1F5F9; }
body .btn-primary,
body .submit-btn {
  background: var(--brand-deep);
  border: 1px solid var(--brand-deep);
  color: #fff;
  border-radius: 0.375rem;
}
body .btn-primary:hover:not(:disabled),
body .submit-btn:hover { background: #0F766E; border-color: #0F766E; box-shadow: none; color: #fff; }
/* The event dialog's Join CTA is an <a>, so it inherits `body a { color:
   var(--brand-deep) }` -- which is UNLAYERED, and therefore wins over the
   utility `text-white` on the element itself. Measured: the label rendered
   rgb(15,118,110) on the button's rgb(79,57,246) = 1.18:1, i.e. teal on
   indigo, at 16px. This restores white without touching any other link. */
body #modal-rsvp-btn,
body #modal-rsvp-btn:hover {
  color: #FFFFFF;
}

body .btn:active:not(:disabled),
body .btn-primary:active:not(:disabled),
body .submit-btn:active:not(:disabled) { transform: translateY(1px); }
/* Secondary is a rank below primary, not a second teal. It used to be
   var(--teal) with a hover border byte-identical to the primary fill, so the
   two read as one undifferentiated button family. */
body .btn-secondary { background: #FFFFFF; border-color: #CBD5E1; color: var(--brand-deep); }
body .btn-secondary:hover:not(:disabled) { background: #F1F5F9; border-color: var(--brand); color: var(--brand-deep); }

/* .btn-danger was MISSING from this theme, and that omission silently disabled
   it: the base `body .btn` above is (0,1,1) while every .btn-* variant in
   custom.css is (0,1,0), so custom.css's .btn-danger was out-specified and
   Reject rendered as a neutral ghost, identical to every other button. */
body .btn-danger { background: #DC2626; border: 1px solid #DC2626; color: #FFFFFF; }
body .btn-danger:hover:not(:disabled) { background: #B91C1C; border-color: #B91C1C; color: #FFFFFF; }

/* Split: .d-btn is the primary-weight action on the ECM club pages (its amber
   intent is declared in ecm_club_*.html) and .d-btn-ghost is the neutral
   variant. Declared together, the primary had no visual weight and the two
   were pixel-identical. */
body .d-btn { background: var(--brand-deep); border: 1px solid var(--brand-deep); color: #FFFFFF; border-radius: 0.5rem; }
body .d-btn:hover:not(:disabled) { background: #115E59; border-color: #115E59; color: #FFFFFF; }
body .d-btn-ghost { background: #FFFFFF; border-color: #CBD5E1; color: var(--ink); border-radius: 0.5rem; }
body .d-btn-ghost:hover:not(:disabled) { background: #F1F5F9; color: var(--ink); }

/* The dark theme sets .action-link to amber-400 (#FBBF24), which is 1.67:1 on
   white. These are destructive and acknowledgement links, so they need to be
   legible, not merely colourful. */
body .action-link { color: var(--brand-deep); }
body .action-link:hover { color: var(--brand); }

/* ---------- Chips / badges / pills ----------
   custom.css §5 is the single base; these are its light-theme values. The
   former dark white/5 + white/10 pills are unreadable on paper, so every alias
   needs a light fill, not just .chip. The dark fills are 15%-alpha tints of a
   saturated colour; the light ones are the same idea over a light base, with a
   saturated 700/900 text step that clears 4.5:1 -- the dark tints did not
   (.badge-success was #4ADE80 on a 15%-green wash, 1.6:1).
   Only fill/colour are set here: .d-chip and .badge are borderless by design,
   so a border-color would be inert. */
body .chip,
body .status-pill,
body .seat-open-badge { background: #F1F5F9; border-color: #E2E8F0; color: var(--slate); }
body .d-chip { background: #F1F5F9; color: var(--slate); }
body .chip-teal, body .chip-secondary { background: var(--teal-soft); border-color: #99F6E4; color: #0F766E; }
body .chip-amber, body .chip-primary { background: #FEF3C7; border-color: #FDE68A; color: #92400E; }
body .chip-red, body .chip-danger { background: #FEE2E2; border-color: #FECACA; color: #B91C1C; }
body .chip-stone, body .chip-dark { background: #F1F5F9; border-color: #E2E8F0; color: var(--slate); }
body .chip-orange { background: #FFEDD5; border-color: #FDBA74; color: #9A3412; }
body .chip-success,
body .chip-green { background: #DCFCE7; border-color: #BBF7D0; color: #15803D; }
body .d-chip-orange { background: #FFEDD5; color: #9A3412; }
body .d-chip-amber { background: #FEF3C7; color: #92400E; }
body .d-chip-stone { background: #F1F5F9; color: var(--slate); }
body .d-chip-teal { background: var(--teal-soft); color: #0F766E; }
body .d-chip-red { background: #FEE2E2; color: #B91C1C; }
body .d-chip-green { background: #DCFCE7; color: #15803D; }
body .badge-success { background: #DCFCE7; color: #15803D; }
body .badge-danger { background: #FEE2E2; color: #B91C1C; }
body .badge-primary { background: #FFF7ED; color: #9A3412; }
body .badge-secondary { background: var(--teal-soft); color: #0F766E; }
/* .seat-open-badge is the muted/disabled read: the template's copy used
   #94A3B8, which is 2.56:1 on white. */
body .seat-open-badge { color: var(--muted); }
body .status-pill:hover { color: var(--ink); border-color: #CBD5E1; }
body .status-pill.active { background: var(--brand-deep); border-color: var(--brand-deep); color: #FFFFFF; }

/* ---------- Tables ---------- */
body .table-rich thead, body thead th { background: #F8FAFC; color: var(--slate); }
body .table-rich tbody tr:nth-child(even) { background-color: #F8FAFC; }
body .table-rich tbody tr:hover, body tbody tr:hover { background-color: #F0FDFA; }
body .table-rich th, body .table-rich td, body tbody td { border-color: var(--line); color: var(--ink); }
body .table-scroll { border: 1px solid var(--line); border-radius: 0.75rem; background: #fff; }

/* ---------- Forms ---------- */
body .field-label { color: var(--ink); }
body .field-input, body .q-input, body .auto-input,
body input[type="text"], body input[type="email"], body input[type="password"],
body input[type="number"], body input[type="url"], body input[type="date"],
body input[type="search"], body textarea, body select {
  background-color: #FFFFFF;
  border-color: #CBD5E1;
  color: var(--ink);
}
body .field-input:focus, body .q-input:focus,
body input:focus, body textarea:focus, body select:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.18);
}
body input::placeholder, body textarea::placeholder,
body .field-input::placeholder, body .q-input::placeholder { color: #94A3B8; }
body .check-label { color: var(--slate); }
body .check-label:hover { background: #F0FDFA; }

/* KYC / KPI / stat tiles */
body .kpi-card, body .stat-tile, body .d-kpi {
  background: #FFFFFF;
  border-color: var(--line);
  border-top-color: var(--brand);
}
body .kpi-label, body .stat-label { color: var(--ink); }
body .kpi-sub, body .stat-sub { color: var(--muted); }
body .kpi-num, body .d-kpi-num { color: var(--ink); }

/* Misc panels carried from dark templates */
body .cover-banner,
body .cover {
  background: linear-gradient(135deg, #F0FDFA 0%, #FFFFFF 60%, #FFFBEB 100%);
  border-color: #99F6E4;
  color: var(--ink);
  box-shadow: 0 12px 32px -16px rgba(13, 148, 136, 0.35);
}
body .skeleton { background-color: #E2E8F0; }
body .skeleton::after { background: linear-gradient(90deg, transparent, rgba(255,255,255,.7), transparent); }
body .empty-state { color: var(--muted); }
body .empty { color: var(--muted); }
body .msg { background: #FFFFFF; border-color: var(--line); color: var(--slate); }
body .avatar { background: var(--brand); color: #fff; }
body .avatar-tile { background: var(--brand-soft); color: var(--brand-deep); }
body .live-dot { background: #22C55E; }
body ::selection { background: rgba(13, 148, 136, 0.18); color: var(--ink); }
body :focus-visible { outline-color: var(--brand); }

/* ---------- Editorial type pairing (anti-slop rule 1) ----------
   Fraunces for display, Inter for body, JetBrains Mono for
   dates/counts/metadata. Display weight 600 max. */
body {
  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
}
body h1, body .display { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.01em; }
body h2, body h3 { font-weight: 600; }
body .toggle-count,
body .kpi-num,
body .d-kpi-num { font-family: var(--font-mono); }
/* ---------- Radius discipline: 6px buttons/chips, 8px cards.
   Kills the uniform rounded-2xl-everywhere tell.
   The chip half moved into custom.css §5: now that the pill family has one
   base, pinning the radius in two layers meant editing one silently did
   nothing. Cards stay here because their radius is theme-specific. ---------- */
body .card,
body .club-card,
body .event-card,
body .form-card,
body .table-card,
body .aside-card,
body .info-card,
body .respond-card,
body .q-card,
body .d-card { border-radius: 0.5rem; }
/* ---------- Top app bar: floating glass pill so the ambient backdrop
   flows around it instead of being sliced by a full-width white bar ---------- */
/* The desktop nav row must not depend on Tailwind's responsive utilities.
   The compiled tailwind.css (v4) puts utilities inside @layer, and an
   unlayered rule outranks a layered one regardless of specificity or
   source order - so "hidden md:flex" collapsed to display:none on pages
   without the Tailwind Play CDN (the media pages), producing a header
   with no page links at all. These unlayered rules are CDN-proof. */
body .topbar .topnav { display: none; }
@media (min-width: 768px) {
  body .topbar .topnav { display: flex; }
}
body .topbar {
  position: sticky; top: 0.75rem; z-index: 50;
  width: calc(100% - 1.5rem); max-width: 76rem; margin-inline: auto;
  margin-top: 0.75rem;
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(16px) saturate(1.5);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 1rem;
  box-shadow: 0 8px 24px -16px rgba(15, 23, 42, 0.25);
}
body .nav-scope { position: sticky; top: 0; z-index: 50; }
body .nav-scope .topbar { position: relative; top: auto; }
@media (max-width: 1023.5px) {
  body .nav-scope {
    transition: transform 0.25s ease-out, opacity 0.25s ease-out;
  }
  body .nav-scope.is-hidden {
    transform: translateY(-120%);
    opacity: 0;
    pointer-events: none;
  }
  body .nav-scope:not(.is-hidden) {
    transform: translateY(0);
    opacity: 1;
  }
  /* WCAG 2.2 2.4.11 Focus Not Obscured (Minimum). The scroll-away header is
     fine to animate out from under nobody, but hideHeader() in base.html
     cannot know whether the thing the user just tabbed to is inside it, and
     it keys purely off scroll position. Measured at 360 / 390 / 820 / 1023
     with scrollY=700: .nav-scope came back transform
     matrix(1,0,0,1,0,-69.6) opacity 0 and every header link sat entirely off
     screen (A "E ECM" at y=-43, bottom=-15) while still holding focus. So
     :focus-within pins the header back in place for as long as focus is
     anywhere inside it -- which is the same behaviour as not hiding it at
     all for that user, and is undone the moment focus leaves.
     Deliberately after :not(.is-hidden) above and equal specificity, so it
     wins on source order; it does not need to out-specify anything. */
  body .nav-scope:focus-within {
    transform: translateY(0);
    opacity: 1;
    pointer-events: auto;
  }
}
@media (prefers-reduced-motion: reduce) {
  body .nav-scope { transition: none; }
}
body .topbar .brandmark {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-weight: 800; letter-spacing: -0.02em; color: var(--ink); text-decoration: none;
}
body .topbar .brandmark .mark {
  width: 1.75rem; height: 1.75rem; border-radius: 0.375rem;
  display: inline-flex; align-items: center; justify-content: center;
  background: #0F172A;
  color: #fff; font-size: 0.8rem; font-weight: 700;
}
body .topbar .toplink {
  padding: 0.5rem 0.8rem; border-radius: 0.6rem;
  font-size: 0.9375rem; font-weight: 600; color: var(--slate); text-decoration: none;
}
body .topbar button.toplink { background: none; border: none; cursor: pointer; font: inherit; }
body .topbar .toplink:hover { background: #F0FDFA; color: var(--ink); }
body .topbar .toplink.is-active { background: var(--brand-soft); color: var(--brand-deep); }

/* ---------- Floating chrome needs clearance: sticky offset + footer/tabbar gaps ---------- */
body .nav-scope + .page-wrap { padding-top: 0.75rem; }
/* ---------- Bottom tab bar: floating glass pill matching the header ---------- */
body .tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
  background: rgba(255, 255, 255, 0.72);
  backdrop-filter: blur(16px) saturate(1.5);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
  border: none;
  border-top: 1px solid rgba(15, 23, 42, 0.08);
  box-shadow: 0 -8px 24px -18px rgba(15, 23, 42, 0.3);
}
body .tabbar-inner {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  max-width: 34rem; margin-inline: auto;
  padding: 0.375rem 0.5rem calc(0.375rem + env(safe-area-inset-bottom, 0));
}
body .tab {
  display: flex; flex-direction: column; align-items: center; gap: 0.15rem;
  padding: 0.5rem 0.25rem; border-radius: 0.375rem;
  font-size: 0.6875rem; font-weight: 600; color: var(--muted);
  text-decoration: none; min-height: 3.5rem; justify-content: center;
  transition: color 0.1s ease, background-color 0.1s ease;
}
body .tab svg { width: 1.4rem; height: 1.4rem; }
body .tab:hover { color: var(--ink); background: #F1F5F9; }
body .tab:active { background: #E2E8F0; }
body .tab.is-active { color: var(--brand-deep); }
body .tab.is-active .tab-pill { background: var(--brand-soft); }
body .tab .tab-pill {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.2rem 0.9rem; border-radius: 9999px;
}
body .tab .tab-dot {
  width: 0.3rem; height: 0.3rem; border-radius: 9999px;
  background: var(--brand); margin-top: 0.1rem; opacity: 0;
}
body .tab.is-active .tab-dot { opacity: 1; }
/* Content clearance so the edge-docked tabbar never covers CTAs/toasts */
body.has-tabbar { padding-bottom: 6.5rem; }
/* WCAG 2.2 2.4.11 Focus Not Obscured (Minimum), the bottom half.
   The padding-bottom above reserves room at the END of the document, which is
   what stops the last control being unreachable. It does nothing for a
   control in the middle of the page: the bar is position:fixed, so when the
   browser scrolls a newly focused element into view it parks that element at
   the very bottom of the viewport -- exactly under an 85px bar. Measured at
   390x844: the bar's top edge is at y=759 and the scroller had no
   scroll-padding at all (getComputedStyle(document.documentElement)
   .scrollPaddingBottom === "auto").

   scroll-padding on the scrolling element is the browser-native way to say
   "when you bring something into view, leave this much room". Chrome honours
   it for sequential focus navigation, not just scrollIntoView, which is the
   case that matters here. Below 768px only: at 768px and up the bar is
   display:none, so reserving 6.5rem would just make every scroll stop short. */
@media (max-width: 767.5px) {
  html { scroll-padding-bottom: 6.5rem; }
}

/* ---------- A modal overlay must own the whole viewport ----------
   The tab bar and the event modals were both z-index 50, and the tab bar came
   later in the document, so it painted ON TOP of the dialog. Measured at
   390x844: 85px of the poster lightbox and 53px of the details modal sat
   behind the bar, and the "Join event" CTA lost 19px of its 44px height.
   Worse, the bar stayed interactive -- tapping "Events" with a dialog open
   navigated away and stranded it.

   Raising the overlay's own z-index is NOT enough, and this records why so
   nobody tries it again: the modal is nested inside .page-wrap, which is
   position:relative with z-index:1, so it forms a stacking context. A
   z-index of 60 on the modal is resolved *within* that context and still
   loses to the tab bar's root-level 50. Verified by hit-testing the tab bar's
   own centre with the dialog open -- it still returned the tab, not the
   overlay.

   So the bar is hidden instead. That removes the overlap, removes the stray
   taps, and drops a backdrop-filter compositor layer while a dialog is open.
   :has() is safe to rely on: Chrome 105+, Safari 15.4+, Firefox 121+.
   Unlayered, so it beats the layered md:hidden that hides the bar at 768px+. */
body:has(#details-modal:not(.hidden)) .tabbar,
body:has(#poster-modal:not(.hidden)) .tabbar,
body:has(#club-poster-lightbox:not(.hidden)) .tabbar { display: none; }

/* The overlays must reach BOTH viewport edges. They are children of a
   .space-y-8 container, and computed margin-top came back as 32px -- the
   measured result was a dialog covering y=32..812 in an 844px viewport, so the
   top and bottom 32px strips of "backdrop" were actually live page. A tap
   meant to dismiss the dialog hit a nav link instead. No rule in any
   same-origin sheet matched margin-top on these elements, so rather than keep
   chasing which one wins, this is an explicit reset: light.css is the last
   stylesheet base.html loads, and it is unlayered. */
body #details-modal,
body #poster-modal,
body #club-poster-lightbox { margin: 0; }
@media (min-width: 768px) {
  body .tabbar { display: none; }
  body.has-tabbar { padding-bottom: 0; }
}

/* ---------- Responsive tiers: tablet + desktop ---------- */
body .page-shell { padding: 1.25rem 1rem 6rem; }
@media (min-width: 768px) {
  body .page-shell { padding: 2.5rem 2rem; }
  /* Tablet: two-up grids, roomier cards */
  body .club-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  body .stat-band { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  /* Desktop: three-up content, centered rail, sidebar inline */
  body .club-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  body .stat-band { grid-template-columns: repeat(4, 1fr); }
  body .page-shell { max-width: 76rem; }
}

/* ---------- Masthead band: paper card, ink rule, teal accent.
   Light scheme: the board sits ON the ambient backdrop, not in a dark box. */
body .hero-band {
  position: relative; overflow: hidden; isolation: isolate;
  background: rgba(255, 255, 255, 0.9);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  /* Padding is deliberately NOT set here. This selector is `body .hero-band`
     (specificity 0,1,1) and would beat the page's own `.hero-band` rules
     (0,1,0) at every breakpoint, which silently pinned the band to zero
     horizontal padding and made its responsive padding a no-op. The
     homepage owns the padding. */
}
body .hero-band::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(circle at 88% 12%, rgba(13, 148, 136, 0.10), transparent 46%);
}
body .hero-band h1, body .hero-band h2 { color: var(--ink); }
body .hero-band p { color: var(--slate); }
body .hero-band .btn { background: var(--brand-deep); border-color: var(--brand-deep); color: #fff; }
body .hero-band .btn:hover { background: #0F766E; border-color: #0F766E; }
/* ---------- Toasts above the tab bar ---------- */
body [aria-live="polite"] { bottom: 6.5rem !important; }
@media (min-width: 768px) {
  body [aria-live="polite"] { bottom: 1rem !important; }
}

/* ---------- Header dropdowns: My Clubs menu + shared enter/leave motion ---------- */
body .club-menu {
  position: absolute; left: 0; top: calc(100% + 0.5rem); z-index: 70;
  min-width: 16rem; max-width: 20rem; max-height: 22rem; overflow-y: auto;
  background: rgba(255, 255, 255, 0.96);
  backdrop-filter: blur(16px) saturate(1.5);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 0.75rem;
  box-shadow: 0 24px 48px -20px rgba(15, 23, 42, 0.35);
  padding: 0.375rem;
}
body .club-menu-link {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.5rem 0.625rem; border-radius: 0.5rem;
  font-size: 0.875rem; font-weight: 500; color: var(--ink); text-decoration: none;
}
body .club-menu-link:hover { background: #F0FDFA; text-decoration: none; }
body .club-menu-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
body .club-menu-role {
  flex-shrink: 0; font-size: 0.6875rem; font-weight: 600;
  background: var(--teal-soft); border: 1px solid #99F6E4; color: #0F766E;
  border-radius: 0.375rem; padding: 0.125rem 0.5rem;
}
body .club-menu-empty { font-size: 0.8125rem; color: var(--muted); padding: 0.5rem 0.625rem; margin: 0; }
body .admin-link { display: inline-flex; align-items: center; gap: 0.4rem; }
body .menu-enter { transition: opacity 0.15s ease-out, transform 0.15s ease-out; }
body .menu-enter-start { opacity: 0; transform: translateY(-0.5rem); }
body .menu-enter-end { opacity: 1; transform: translateY(0); }
body .menu-leave { transition: opacity 0.1s ease-in, transform 0.1s ease-in; }
body .menu-leave-start { opacity: 1; transform: translateY(0); }
body .menu-leave-end { opacity: 0; transform: translateY(-0.5rem); }

@media print {
  body .tabbar, body .topbar { display: none !important; }
}

/* ============================================================
   WCAG AA pass: the four palette steps that failed on small text
   ============================================================
   Every one of these was measured with getComputedStyle + a canvas
   resolve of the composited background, not read off a stylesheet.
   Four colours, all of them 11-14px text that needs 4.5:1:

     #D97706 amber-600   2.81-3.19:1   -> #B45309 amber-700 (4.52-5.02)
     #0D9488 teal-600    3.45-3.74:1   -> #0F766E teal-700  (4.82-5.47)
     #94A3B8 slate-400   2.17-2.86:1   -> var(--muted)       (4.95-5.74)
     #64748B slate-500   4.34-4.45:1   -> var(--muted)       (5.24-5.74)

   Two steps go further than amber-700, because these two sit on a
   wash of their own hue and the wash eats the margin:
     .kpi-delta.amber  #92400E -> 6.25:1 on its rgba(217,119,6,.12) fill
     td.cell.busy      #92400E -> 6.16:1 on its rgba(245,158,11,.13) fill
   (amber-700 measures 4.42 and 4.37 there -- under 4.5, which is why
   they are not simply "the amber-700 rule" like the rest.)

   Specificity, which is the whole reason this block exists here
   instead of in each page: every template carries its own <style>
   block, base.html renders {% block extra_head %} AFTER the
   stylesheet <link>s, so a page rule of equal specificity always
   wins on source order and a stylesheet edit cannot reach it. The
   page rules are all (0,1,0) -- `.eyebrow`, `.clubs-kicker`,
   `.tt-eyebrow`, `.field-optional`, ... -- so prefixing with `body`
   here is (0,1,1) and beats them regardless of order. The three
   compound selectors below need one extra class or element for the
   same reason: the page writes `.cover-banner .eyebrow` (0,2,0),
   `.table-rich thead th.break-col` (0,2,3) and
   `.d-table thead th` (0,1,3).

   `.d-green` and `.d-red` are the audit log's status-code figures.
   `.d-green` was #4ADE80 on white = 1.74:1, i.e. invisible; #15803D
   is this theme's existing success step (5.02:1 on white, 4.58:1 on
   a #F1F5F9 panel) and .d-red moves off #DC2626, which was 4.41:1 on
   that same panel, to the #B91C1C this theme already uses for
   .btn-danger and .chip-red.
   ------------------------------------------------------------ */

/* amber-600 -> amber-700 */
body .eyebrow { color: #B45309; }
body .cover-banner .eyebrow { color: #B45309; }
body .section-title { color: #B45309; }
body .tt-eyebrow { color: #B45309; }
body .table-rich thead th.break-col { color: #B45309; }
body .kpi-delta.amber { color: #92400E; }

/* teal-600 -> teal-700 */
body .clubs-kicker,
body .media-kicker,
body .login-kicker,
body .reg-eyebrow,
body .aside-title,
body .block-palette-title,
body .err-eyebrow,
body .d-teal { color: #0F766E; }

/* slate-400 and slate-500 -> --muted. .q-num is the one exception:
   on the form-builder it was slate-400, on the respond form it is a
   teal badge on a teal wash, and teal-700 clears both (5.00:1 on the
   #F1F5F9 panel, 4.63:1 on the rgba(20,184,166,.12) fill). */
/* .reg-sub a is (0,1,1) in the page and out-specifies `body a`, which is
   only (0,0,2) -- so the teal-600 it declares is what actually renders,
   3.74:1. Needs the same extra class. */
body .reg-sub a { color: #0F766E; }

body .pres-label,
body .seat-open,
body .resp-foot,
body .field-optional,
body .tt-foot,
body .team-class,
body .finder-count { color: var(--muted); }
body .q-num { color: #0F766E; }

/* kpi-delta's teal sibling. The amber one is above because it needs
   amber-800 on its own wash; the teal one clears at teal-700. */
body .kpi-delta.teal { color: #0F766E; }

/* Page-local accent pills, avatars and headings. Every rule here belongs to
   a template this stylesheet does not own, and every one of them is the same
   defect: teal-600 or amber-600 sitting on a 10-14% wash of its own hue, or
   white type on a teal-600 button. Measured before, at 1280px:
     .role-chip.staff       #0D9488 on #E5FAF7   3.45:1
     .role-chip.president   #D97706 on #F2E8D8   2.63:1
     .team-avatar           #0D9488 on #E5FAF7   3.45:1
     .edit-avatar           #0D9488 on #E6F7F6   3.37:1
     .apply-btn             #FFFFFF on #0D9488   3.74:1
     .club-poster-eyebrow   #818CF8 on #FAFAFD   2.86:1
     .club-poster-zoom      #94A3B8 on #FAFAFD   2.46:1
     .q-remove              #DC2626 on #F1F5F9   4.41:1
     .notice-info .accent   #D97706 on #F1F5F9   2.91:1
     .panel h2              #D97706 on #FFFFFF   3.19:1
   The two `.panel-*-sex h2` rules are restated at the same weight only so
   the boys/girls blue and pink survive the `.panel h2` rule above them --
   without that they would both flatten to amber. */
body .role-chip.staff { color: #0F766E; }
/* amber-700 is only 4.15:1 on this pill's own amber-14% fill, so it takes
   the same deeper step .kpi-delta.amber does. */
body .role-chip.president { color: #92400E; }
body .team-avatar,
body .edit-avatar,
body .d-avatar-sm { color: #0F766E; }
body .apply-btn { background: #0F766E; }
body .apply-btn:hover { background: #115E59; }
body .club-poster-eyebrow { color: #4F46E5; }
body .club-poster-zoom { color: var(--muted); }
body .q-remove { color: #B91C1C; }
body .notice-info .accent { color: #B45309; }
body .panel h2 { color: #B45309; }
body .panel-boys h2 { color: #2563EB; }
body .panel-girls h2 { color: #BE185D; }
/* The two side-coloured initial tiles, blue-600 and pink-600 on a 12% wash
   of themselves: 4.32:1 and 3.97:1. One step down each. */
body .avatar-boys, body .pres-boys { color: #1D4ED8; }
body .avatar-girls, body .pres-girls { color: #BE185D; }
/* The "← All Clubs" / "← Back to ..." links. club_detail.html, club_edit.html
   and club_list.html each ship their own `.text-teal-300 { #0D9488 }`, which
   is (0,1,0) and so out-specifies light.css's `body a` (0,0,2) -- 3.59:1. */
body .text-teal-300 { color: #0F766E; }
body .text-teal-400 { color: #0F766E; }
body .d-link { color: var(--brand-deep); }

/* audit log status-code figures */
body .d-green { color: #15803D; }
body .d-red { color: #B91C1C; }

/* The dark theme's orange steps. custom.css §17.2/§17.3 declare .d-eyebrow
   and .d-orange as #FB923C and .d-amber as #FBBF24 -- 2.17:1 and 1.67:1 on
   paper. .d-eyebrow is a live 12px eyebrow on /ecm/dashboard/ and
   /ecm/audit/; the other two are currently only reached as
   .d-kpi-num.<x>, where light.css's `body .d-kpi-num` already forces
   --ink, but they are remapped so they cannot fail the moment they are used
   on their own. */
body .d-eyebrow { color: #B45309; }
body .d-orange { color: #B45309; }
body .d-amber { color: #B45309; }

/* Table headers. Six templates -- incident_list, head_incidents, contacts,
   common_room_list, dashboard, audit_log -- each declare their own
   `thead th` colour at (0,1,3), and three of them put #64748B on their own
   #F1F5F9 header fill, which measures 4.34:1. One (0,1,4) rule here covers
   every one of them. */
body .table-rich thead th,
body .d-table thead th { color: var(--muted); }

/* Timetable cells. The FREE/BUSY labels are 13px on a 14%/13% wash
   of their own hue, which is the worst surface on the site: 2.76:1
   for Busy and 3.16:1 for FREE as shipped. The break column is a
   third amber tint and needs its own step for the same reason. */
body .table-rich tbody td.cell.free .cell-label,
body .table-rich tbody td.bg-green-100 .cell-label { color: #0F766E; }
body .table-rich tbody td.cell.busy .cell-label,
body .table-rich tbody td.bg-stone-100 .cell-label { color: #92400E; }
body .table-rich tbody td.break-cell { color: #92400E; }
/* 11px day heading, #64748B on #F1F5F9 = 4.34:1 */
body .week-day-name { color: var(--muted); }

/* The "Required" pill in the form builder paints a 12%-teal fill and
   a teal-600 label: 3.07:1. Teal-700 is 4.49:1 on that exact fill --
   still a hair under. #115E59, already this theme's deep step from
   the .d-btn hover and the .topbar link, is 6.22:1. */
body .required-toggle:has(input:checked) { color: #115E59; }

/* ---------- WCAG 2.2 SC 2.5.8 Target Size (Minimum) ----------
   Both dialogs in _event_modal.html are closable only by their own
   close button (plus Escape), so neither can be an exception. The
   two close buttons measured 36x36 and 40x40; the lightbox footer's
   two buttons measured 98.6x21 and 52.6x21 -- the 21px height is a
   straight failure of 2.5.8. All four are raised here because
   _event_modal.html is not this file; min-width/min-height only, so
   nothing else in those dialogs moves. */
#details-modal [aria-label="Close event details"],
#poster-modal [aria-label="Close poster"] {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
#details-modal [aria-label="Share link"] {
  min-width: 44px;
  min-height: 44px;
}
#poster-modal button[onclick="downloadPoster()"],
#poster-modal button[onclick="window.print()"] {
  min-height: 24px;
  min-width: 24px;
}

/* ---------- The ECM club-event <summary> crushed its own name ----
   .d-ev summary is a single flex line holding three nowrap items --
   the name, a status pill and the date. At 320px the line is 196px
   and .d-ev-name (flex:1 1 auto, min-width:0) absorbed all of the
   shortfall: 40.1px shown against a 196px scrollWidth, and the
   "Approved" pill next to it lost 45px of its own. Wrapping gives the
   name the full line and drops pill + date onto a second one.
   Verified inert at 820px and above, where all three already fit. */
body .d-ev summary { flex-wrap: wrap; }

/* ---------- Themed scrollbar: soft-teal thumb on paper ---------- */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: #FAFAFD !important; }
::-webkit-scrollbar-thumb {
  background: #5EEAD4 !important;
  border-radius: 9999px;
  border: 2px solid #FAFAFD !important;
}
::-webkit-scrollbar-thumb:hover { background: #0D9488 !important; }
* {
  scrollbar-width: thin !important;
  scrollbar-color: #0D9488 #FAFAFD !important;
}
