:root {
  --paper: #F0EFE6;
  --paper-alt: #E5E3D4;
  --paper-line: #D8D6C4;
  --ink: #17211C;
  --ink-soft: #3A423C;
  /* Darkened from #6B7268 - the original only hit 3.84:1 against
     --paper-alt (WCAG AA needs 4.5:1 for normal text), and this color is
     used everywhere for meta text, hints, and timestamps. #60675E clears
     4.5:1 against every background it actually appears on. */
  --muted: #60675E;
  --accent: #2451FF;
  --accent-ink: #F4F6FF;
  --chip: #FFFFFF;
  --chip-ink: #3A423C;
  --card-bg: #FBFAF4;
  --shadow: rgba(23, 33, 28, 0.14);
  /* Darkened from #2E7D46 - the original was 4.40:1 against --paper,
     just under WCAG AA's 4.5:1 floor. */
  --success: #2D7B45;
  --danger: #B3261E;
  --font-display: "Inter", -apple-system, "SF Pro Display", "Segoe UI Semibold", "Segoe UI", ui-sans-serif, system-ui, sans-serif;
  --font-body: "Inter", -apple-system, "SF Pro Text", "Segoe UI", ui-sans-serif, system-ui, sans-serif;
  --font-mono: var(--font-body);
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #12150F;
    --paper-alt: #1B1E15;
    --paper-line: #2B2E22;
    --ink: #ECEAE0;
    --ink-soft: #C6C7BB;
    --muted: #949A8C;
    --accent: #7C9AFF;
    --accent-ink: #0E1330;
    --chip: #262A1E;
    --chip-ink: #C6C7BB;
    --card-bg: #191C13;
    --shadow: rgba(0, 0, 0, 0.45);
    --success: #6FCF8F;
    --danger: #E5867D;
  }
}
:root[data-theme="dark"] {
  --paper: #12150F;
  --paper-alt: #1B1E15;
  --paper-line: #2B2E22;
  --ink: #ECEAE0;
  --ink-soft: #C6C7BB;
  --muted: #949A8C;
  --accent: #7C9AFF;
  --accent-ink: #0E1330;
  --chip: #262A1E;
  --chip-ink: #C6C7BB;
  --card-bg: #191C13;
  --shadow: rgba(0, 0, 0, 0.45);
  --success: #6FCF8F;
  --danger: #E5867D;
}
:root[data-theme="light"] {
  --paper: #F0EFE6;
  --paper-alt: #E5E3D4;
  --paper-line: #D8D6C4;
  --ink: #17211C;
  --ink-soft: #3A423C;
  --muted: #60675E;
  --accent: #2451FF;
  --accent-ink: #F4F6FF;
  --chip: #FFFFFF;
  --chip-ink: #3A423C;
  --card-bg: #FBFAF4;
  --shadow: rgba(23, 33, 28, 0.14);
  --success: #2D7B45;
  --danger: #B3261E;
}

* { box-sizing: border-box; }
html { scrollbar-gutter: stable; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
/* Decorative blooms + dot grid live on a fixed, full-viewport pseudo-
   element behind everything (z-index -1), NOT directly on body's own
   background like an earlier version of this rule did. Every gradient
   below is positioned with percentages, which CSS resolves against the
   size of whatever box they're painted on - when they lived on body
   itself, that box's height is the page's total scrollable height, so
   anything that changed how tall the page was (filtering a list down to
   fewer results, opening/closing an accordion, a modal appearing) shifted
   every single shape, visibly, on a totally unrelated click. A
   position:fixed layer's box is always exactly the viewport size no
   matter how tall the actual content is, so the same percentages now
   resolve to the same pixels every time regardless of page content.
   Soft dark-grey blooms (blurred radial washes) rather than a harder
   shape - kept at a low enough alpha (5-9%) that they don't meaningfully
   cut into text contrast against --ink/--ink-soft, so headings sitting
   directly on this background (not inside a card) don't need their own
   backing box either. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-color: var(--paper);
  background-image:
    radial-gradient(circle 38vmax at 8% 0%, rgba(0, 0, 0, 0.14) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 62% 8%, rgba(0, 0, 0, 0.10) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 30% 22%, rgba(0, 0, 0, 0.12) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 88% 34%, rgba(0, 0, 0, 0.10) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 15% 48%, rgba(0, 0, 0, 0.12) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 68% 60%, rgba(0, 0, 0, 0.10) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 40% 74%, rgba(0, 0, 0, 0.12) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 85% 86%, rgba(0, 0, 0, 0.10) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 20% 100%, rgba(0, 0, 0, 0.12) 0%, transparent 100%),
    radial-gradient(circle, rgba(23, 33, 28, 0.22) 1px, transparent 1px);
  background-repeat: no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, repeat;
  background-size: 100% 100%, 100% 100%, 100% 100%, 100% 100%, 100% 100%, 100% 100%, 100% 100%, 100% 100%, 100% 100%, 24px 24px;
}
/* Dark-theme version: a true black canvas (not the near-black olive paper
   tone --paper itself resolves to in dark mode - overridden explicitly
   here rather than by changing --paper, since that variable is also
   reused for input/textarea backgrounds elsewhere that shouldn't go pure
   black) with soft white-tinted grey blooms - same shapes/positions as
   light mode above, just inverted so they read as light grey against
   black instead of dark grey against paper. Every color here is a literal
   rgba(r, g, b, a), deliberately NOT composed through a custom property
   or color-mix(), after a real report of a color-mix()'d version of this
   rendering as solid black in a browser where DevTools showed the rule
   correctly applied but the channels had apparently resolved to 0. */
@media (prefers-color-scheme: dark) {
  body::before {
    background-color: #000000;
    background-image:
      radial-gradient(circle 38vmax at 8% 0%, rgba(255, 255, 255, 0.16) 0%, transparent 100%),
      radial-gradient(circle 36vmax at 62% 8%, rgba(255, 255, 255, 0.11) 0%, transparent 100%),
      radial-gradient(circle 38vmax at 30% 22%, rgba(255, 255, 255, 0.13) 0%, transparent 100%),
      radial-gradient(circle 36vmax at 88% 34%, rgba(255, 255, 255, 0.11) 0%, transparent 100%),
      radial-gradient(circle 38vmax at 15% 48%, rgba(255, 255, 255, 0.13) 0%, transparent 100%),
      radial-gradient(circle 36vmax at 68% 60%, rgba(255, 255, 255, 0.11) 0%, transparent 100%),
      radial-gradient(circle 38vmax at 40% 74%, rgba(255, 255, 255, 0.13) 0%, transparent 100%),
      radial-gradient(circle 36vmax at 85% 86%, rgba(255, 255, 255, 0.11) 0%, transparent 100%),
      radial-gradient(circle 38vmax at 20% 100%, rgba(255, 255, 255, 0.13) 0%, transparent 100%),
      radial-gradient(circle, rgba(255, 255, 255, 0.11) 1px, transparent 1px);
  }
}
:root[data-theme="dark"] body::before {
  background-color: #000000;
  background-image:
    radial-gradient(circle 38vmax at 8% 0%, rgba(255, 255, 255, 0.16) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 62% 8%, rgba(255, 255, 255, 0.11) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 30% 22%, rgba(255, 255, 255, 0.13) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 88% 34%, rgba(255, 255, 255, 0.11) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 15% 48%, rgba(255, 255, 255, 0.13) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 68% 60%, rgba(255, 255, 255, 0.11) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 40% 74%, rgba(255, 255, 255, 0.13) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 85% 86%, rgba(255, 255, 255, 0.11) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 20% 100%, rgba(255, 255, 255, 0.13) 0%, transparent 100%),
    radial-gradient(circle, rgba(255, 255, 255, 0.11) 1px, transparent 1px);
}
/* Explicit light override, not just relying on the base rule above: if the
   OS is set to dark but the manual toggle picks light, the plain
   `body::before` selector inside the dark media query above has equal
   specificity to the base rule and comes later in the file, so it would
   otherwise win even though data-theme="light" is set - higher-
   specificity attribute selector here is what actually makes the manual
   toggle override the OS preference for this property, the same way it
   already does for the --paper/--ink color tokens up top. */
:root[data-theme="light"] body::before {
  background-color: var(--paper);
  background-image:
    radial-gradient(circle 38vmax at 8% 0%, rgba(0, 0, 0, 0.14) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 62% 8%, rgba(0, 0, 0, 0.10) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 30% 22%, rgba(0, 0, 0, 0.12) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 88% 34%, rgba(0, 0, 0, 0.10) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 15% 48%, rgba(0, 0, 0, 0.12) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 68% 60%, rgba(0, 0, 0, 0.10) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 40% 74%, rgba(0, 0, 0, 0.12) 0%, transparent 100%),
    radial-gradient(circle 36vmax at 85% 86%, rgba(0, 0, 0, 0.10) 0%, transparent 100%),
    radial-gradient(circle 38vmax at 20% 100%, rgba(0, 0, 0, 0.12) 0%, transparent 100%),
    radial-gradient(circle, rgba(23, 33, 28, 0.22) 1px, transparent 1px);
}
/* Every page's <main> grows to fill whatever space isn't taken by the
   header/footer, so the footer actually sits at the bottom of the
   viewport on short pages instead of floating right under the content. */
main { flex: 1 0 auto; }
a { color: inherit; }

/* Keyboard/screen-reader users otherwise have to tab through the entire
   header nav on every single page before reaching actual content - this
   sits off-screen until it receives focus (the first Tab stop on the
   page), then pops into a normal visible position so sighted keyboard
   users can see what they just focused. */
.skip-link {
  position: absolute;
  top: -100%;
  left: 1rem;
  z-index: 100;
  padding: 0.65rem 1.1rem;
  background: var(--accent);
  color: var(--accent-ink);
  border-radius: 8px;
  font-weight: 600;
  text-decoration: none;
}
.skip-link:focus {
  top: 1rem;
}

.wrap {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 clamp(1.25rem, 4vw, 3rem);
}

/* ---------- nav (shared) ---------- */
/* Normal in-flow header (not fixed, not sticky) - it only occupies the very
   top of the page, so scrolling down moves it off-screen like any other
   content and it's only visible again once the user scrolls back up to the
   top. nav.js plays a one-shot drop-down animation (.nav-drop-in, defined
   below) via IntersectionObserver whenever the header re-enters the
   viewport - on initial page load and every time scrolling back up brings
   it back into view. Background/text/border use the same theme tokens as
   the rest of the page (--card-bg/--ink/--paper-line) rather than a fixed
   always-dark color - a real report that the header stayed solid black in
   light mode ("super ugly") is what this fixed; the translucent overlay
   tints on the buttons below use color-mix() against --ink specifically so
   they invert correctly too. */
header.nav {
  background: var(--card-bg);
  border-bottom: 1px solid var(--paper-line);
  color: var(--ink);
}
@keyframes nav-drop-in {
  from { transform: translateY(-100%); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
header.nav.nav-drop-in {
  /* Same slow, unmistakably-a-drop 1.4s easing tuned earlier for this
     header, kept here so the animation still reads as a deliberate
     drop-down rather than a snap. */
  animation: nav-drop-in 1.4s cubic-bezier(0.16, 1, 0.3, 1);
}
/* No header-specific link-color overrides here anymore - nav.links only
   ever appears inside header.nav, so the plain nav.links a:not(.btn) /
   .active rules further down (already theme-aware, --ink-soft/--ink) are
   all that's needed now that the header itself follows the theme too. */
header.nav .btn-ghost {
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  color: var(--ink);
  border-color: color-mix(in srgb, var(--ink) 22%, transparent);
}
header.nav .btn-ghost:hover { background: color-mix(in srgb, var(--ink) 14%, transparent); }
header.nav .nav-toggle { background: color-mix(in srgb, var(--ink) 8%, transparent); border-color: color-mix(in srgb, var(--ink) 20%, transparent); }
header.nav .nav-toggle-bar { background: var(--ink); }
.nav-row {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem;
  /* padding-top/bottom only (not the padding shorthand) - .nav-row and
     .wrap are the same element, and the shorthand would silently reset
     .wrap's horizontal padding to 0 instead of layering with it, since
     shorthand properties replace rather than merge. That's what was
     pinning header content (most visibly the avatar button - see
     .nav-profile) flush against the viewport edge on mobile with no
     breathing room at all. */
  padding-top: 1.1rem;
  padding-bottom: 1.1rem;
}
/* Hidden on desktop (.nav-center/.nav-auth show inline as normal); the
   mobile media query below flips this to visible and collapses those two
   into a stacked panel that opens underneath the header instead. */
.nav-toggle {
  display: none;
  flex: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 4px;
  /* 2.75rem = 44px, the widely-recommended minimum touch target size
     (WCAG 2.5.5, Apple/Google HIG) - this is the hamburger button, tapped
     on every mobile page load, so worth the extra few px over the 36px it
     measured at before. The hamburger bars themselves (.nav-toggle-bar)
     have their own fixed width, so this only adds tap-area padding around
     them, not a bigger-looking icon. */
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid var(--paper-line);
  border-radius: 8px;
  background: var(--chip);
  cursor: pointer;
}
.nav-toggle-bar {
  display: block;
  width: 1.1rem;
  height: 2px;
  background: var(--ink);
  border-radius: 2px;
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.nav-toggle.active .nav-toggle-bar:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle.active .nav-toggle-bar:nth-child(2) { opacity: 0; }
.nav-toggle.active .nav-toggle-bar:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
.logo {
  justify-self: start;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.65rem;
  letter-spacing: -0.02em;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.logo .mark {
  height: 2rem;
  width: auto;
  flex: none;
}
/* Two logo files, one shown at a time - light-mode art on light
   backgrounds, the lighter dark-mode art once it'd otherwise go muddy on
   the near-black background. Same media-query + data-theme override
   pattern as the color tokens above, so the manual theme toggle wins in
   both directions. */
.mark-dark { display: none; }
@media (prefers-color-scheme: dark) {
  .mark-light { display: none; }
  .mark-dark { display: block; }
}
:root[data-theme="dark"] .mark-light { display: none; }
:root[data-theme="dark"] .mark-dark { display: block; }
:root[data-theme="light"] .mark-light { display: block; }
:root[data-theme="light"] .mark-dark { display: none; }

nav.links {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 2rem);
  font-size: 0.95rem;
}
/* The header is a 3-column grid: logo | nav-center | nav-auth. Everything
   except Sign in/Sign up (or Log out) lives in the centered middle column;
   account actions stay pinned to the right, on every page. */
nav.links.nav-center { justify-self: center; }

/* Replaces the old nav-auth Sign in/Sign up/Log out row - a single avatar
   button, justify-self: end same as nav-auth was, that pops open a
   dropdown with those same actions instead of showing them inline. See
   nav.js for what fills the dropdown in. */
.nav-profile { position: relative; justify-self: end; display: flex; align-items: center; gap: 0.6rem; }
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Same 44px touch-target minimum as .nav-toggle above, same reasoning -
     this one measured 36px too, and it's reachable from every page. */
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  flex: none;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--ink) 24%, transparent);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  color: var(--ink);
  cursor: pointer;
}
.theme-toggle:hover { background: color-mix(in srgb, var(--ink) 14%, transparent); }
/* The 3 bare auth pages (signin/signup/confirm-age) have no header.nav to
   sit inside - floats in the corner instead, independent of the centered
   .auth-page column so it doesn't disturb that layout. */
.theme-toggle--floating {
  position: fixed;
  top: 1.25rem;
  right: 1.25rem;
  z-index: 40;
}
/* Same default/media-query/data-theme override pattern as .mark-light/
   .mark-dark above - the icon shown reflects whichever theme is actually
   active (OS preference by default, the manual override once set), not a
   fixed sun-always icon. */
.theme-icon-moon { display: none; }
@media (prefers-color-scheme: dark) {
  .theme-icon-sun { display: none; }
  .theme-icon-moon { display: block; }
}
:root[data-theme="dark"] .theme-icon-sun { display: none; }
:root[data-theme="dark"] .theme-icon-moon { display: block; }
:root[data-theme="light"] .theme-icon-sun { display: block; }
:root[data-theme="light"] .theme-icon-moon { display: none; }
.nav-avatar-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  padding: 0;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--ink) 24%, transparent);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  cursor: pointer;
  overflow: hidden;
  flex: none;
}
/* Mobile-only label injected next to the avatar button by nav.js - hidden
   by default (desktop already has the avatar button on its own, no label
   needed) and only ever shown by the @media (max-width: 760px) block once
   the hamburger is open, alongside the avatar button itself. */
.nav-profile-label {
  display: none;
  align-items: center;
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
}
.nav-avatar-btn:hover { background: color-mix(in srgb, var(--ink) 14%, transparent); }
.nav-avatar-img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.nav-avatar-fallback {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--ink);
}
.nav-avatar-fallback svg { width: 20px; height: 20px; }
.nav-dropdown {
  position: absolute;
  top: calc(100% + 0.6rem);
  right: 0;
  min-width: 13rem;
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
  padding: 0.5rem;
  z-index: 30;
  flex-direction: column;
  gap: 0.15rem;
}
/* display set only here, on the :not([hidden]) variant - never as an
   unconditional rule on .nav-dropdown itself. An authored display rule on
   the bare class would beat the [hidden] attribute's own default
   display:none (equal specificity, later in the cascade), which is exactly
   the bug that's bitten this codebase before (see .preview-banner,
   .role-option, etc.) - keeping "shown" and "hidden" on two different
   selectors avoids that class of bug entirely instead of re-fixing it. */
.nav-dropdown:not([hidden]) { display: flex; }
.nav-dropdown-meta {
  padding: 0.5rem 0.7rem 0.6rem;
  color: var(--muted);
  font-size: 0.78rem;
  border-bottom: 1px solid var(--paper-line);
  margin-bottom: 0.3rem;
  overflow-wrap: break-word;
}
.nav-dropdown a, .nav-dropdown button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.55rem 0.7rem;
  border-radius: 8px;
  border: none;
  background: none;
  color: var(--ink);
  text-decoration: none;
  font-size: 0.92rem;
  font-family: var(--font-body);
  cursor: pointer;
}
.nav-dropdown a:hover, .nav-dropdown button:hover { background: var(--paper-alt); }
.nav-dropdown-danger { color: var(--danger); }
.nav-dropdown-error {
  padding: 0.55rem 0.7rem;
  margin-bottom: 0.3rem;
  border-radius: 8px;
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger);
  font-size: 0.82rem;
}
/* nav.js fills these in at runtime - #nav-links and #nav-auth need their
   own flex+gap, since the "gap" above only spaces nav.links' direct
   children, not links injected inside a nested span. Without this they'd
   render with zero spacing. */
#nav-links,
#nav-auth {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 2rem);
}
nav.links a:not(.btn) {
  text-decoration: none;
  color: var(--ink-soft);
  position: relative;
  padding-bottom: 3px;
}
nav.links.nav-center a:not(.btn) {
  font-size: 1.1rem;
  font-weight: 500;
}
nav.links a:not(.btn)::after {
  content: "";
  position: absolute;
  left: 0; right: 100%;
  bottom: 0;
  height: 2px;
  background: var(--accent);
  transition: right 0.2s ease;
}
nav.links a:not(.btn):hover::after,
nav.links a:not(.btn).active::after {
  right: 0;
}
nav.links a:not(.btn).active { color: var(--ink); }

/* Sign in / Sign up match Find jobs / Volunteer's text size (1.1rem) -
   they used to be smaller (0.95rem plain text and 1.02rem button) and
   looked out of place next to the nav-center links at a glance. */
nav.links.nav-auth .nav-signin {
  font-size: 1.1rem;
  font-weight: 500;
}
nav.links.nav-auth .btn {
  font-size: 1.1rem;
  padding: 0.72rem 1.3rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.95rem;
  text-decoration: none;
  border-radius: 10px;
  padding: 0.65rem 1.15rem;
  border: 1px solid transparent;
  cursor: pointer;
  white-space: nowrap;
  transition: transform 0.15s ease, background 0.15s ease, opacity 0.15s ease;
  /* Positioning context + clip for .btn-ripple (see /js/btnRipple.js) -
     without overflow: hidden the ripple would spill past the button's
     own rounded corners instead of being masked by them. */
  position: relative;
  overflow: hidden;
}
.btn:active { transform: translateY(1px); }
/* Click-triggered ripple, appended/removed by /js/btnRipple.js. currentColor
   (not a hardcoded color) so it automatically matches whichever --accent/
   --danger/--ink token that specific button variant already uses as its
   text color, instead of needing a separate ripple color per .btn-* class. */
.btn-ripple {
  position: absolute;
  border-radius: 50%;
  transform: scale(0);
  background: currentColor;
  opacity: 0.32;
  pointer-events: none;
  animation: btn-ripple-anim 0.5s ease-out;
}
@keyframes btn-ripple-anim {
  to { transform: scale(2.6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .btn-ripple { display: none; }
}
.btn:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-accent { background: var(--accent); color: var(--accent-ink); }
.btn-accent:hover { opacity: 0.88; }
/* Solid --chip background rather than transparent - these sit directly on
   the page in a few places (not just inside a card), and a transparent
   button over the big background shapes could disappear into a dark one
   entirely. A solid surface keeps them visible no matter what's behind. */
.btn-ghost { background: var(--chip); border-color: var(--paper-line); color: var(--ink); }
.btn-ghost:hover { background: var(--paper-alt); }
/* Toggled state for a .btn-ghost that also acts as an on/off control (e.g.
   "Add to talent pool" on the candidates page) - same accent-tint language
   as .filter-toggle.active/.day-btn.active elsewhere, just for a button
   that isn't part of a pill-toggle group. */
.btn-ghost.active { background: color-mix(in srgb, var(--accent) 16%, var(--chip)); border-color: var(--accent); color: var(--accent); }
.btn-danger { background: var(--chip); border-color: var(--paper-line); color: var(--danger); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 10%, var(--chip)); border-color: var(--danger); }
.btn-block { width: 100%; }
/* flex-wrap so a row that doesn't fit (long labels, narrow card) wraps
   onto a second line instead of squishing button text or overflowing the
   card - each button keeps its natural (nowrap) width via flex-basis auto,
   only stretching to fill leftover space when there's room to spare. */
.btn-row { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.btn-row .btn { flex: 1 1 auto; }

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- auth pages ---------- */
.auth-shell {
  /* Height comes from the shared main{flex:1 0 auto} rule now, so it
     grows to fill whatever space is left between header/footer (or just
     fills the page on signin/signup, which have neither) - no more
     guessing a fixed header height to subtract. */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(2rem, 6vw, 4rem) 1.25rem;
}
.auth-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5rem;
  width: 100%;
}
.auth-card {
  width: 100%;
  max-width: 26rem;
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  border-radius: 18px;
  box-shadow: 0 14px 34px var(--shadow);
  padding: clamp(1.75rem, 4vw, 2.5rem);
}
.auth-card--wide { max-width: 30rem; }
.auth-card h1 {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.7rem;
  letter-spacing: -0.02em;
  margin: 0 0 0.4rem;
}
.auth-card .sub {
  color: var(--ink-soft);
  margin: 0 0 1.75rem;
  font-size: 0.97rem;
}

.oauth-list {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
  margin-bottom: 1.5rem;
}
.oauth-btn {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  width: 100%;
  padding: 0.65rem 1rem;
  border-radius: 10px;
  border: 1px solid var(--paper-line);
  background: var(--chip);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.92rem;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.15s ease, opacity 0.15s ease;
}
.oauth-btn:hover { background: var(--paper-alt); }
.oauth-btn.is-unconfigured { opacity: 0.55; }
/* Houses Google's actual multi-color "G" mark (inline SVG in the HTML) -
   no border/background/letter styling here anymore, since the real logo
   is what makes it recognizable rather than a plain boxed initial. */
.oauth-mark {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.oauth-mark svg { width: 100%; height: 100%; }

.divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  color: var(--muted);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  margin: 0 0 1.5rem;
}
.divider::before,
.divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--paper-line);
}

.field {
  margin-bottom: 1.1rem;
}
/* One visually-fused control ($ | number | unit) instead of three floating
   pieces - a shared border/background on the group, with the inner input
   and select stripped of their own borders so the seams disappear. */
.input-with-select {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--paper-line);
  border-radius: 10px;
  background: var(--paper);
  overflow: hidden;
  transition: border-color 0.15s ease;
}
.input-with-select:focus-within { border-color: var(--accent); }
.input-with-select .input-prefix {
  display: flex;
  align-items: center;
  padding: 0 0.65rem;
  color: var(--muted);
  font-weight: 600;
  font-size: 0.95rem;
}
.input-with-select .input-prefix:first-child { padding-left: 0.85rem; }
.input-with-select input[type="number"],
.input-with-select input[type="time"] {
  flex: 1;
  min-width: 0;
  border: none;
  border-radius: 0;
  background: transparent;
}
.input-with-select input[type="number"]:focus,
.input-with-select input[type="time"]:focus { outline: none; }
.input-with-select select {
  flex: none;
  border: none;
  border-left: 1px solid var(--paper-line);
  border-radius: 0;
  background: var(--paper-alt);
}

.day-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}
.day-btn, .filter-toggle {
  font-family: var(--font-body);
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--ink-soft);
  background: var(--paper);
  border: 1px solid var(--paper-line);
  border-radius: 999px;
  padding: 0.5rem 1rem;
  cursor: pointer;
  /* Harmless for the existing <button>-only usages (never underlined
     anyway) - needed now that the hiring guide's jump-to-section links
     reuse this same pill styling on real <a> tags, which browsers
     underline by default. */
  text-decoration: none;
  display: inline-block;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.day-btn:hover, .filter-toggle:hover { background: var(--paper-alt); }
.day-btn.active, .filter-toggle.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  /* Only plays when the class is actually (re)applied by a click - toggling
     .active off and back on replays it, same as a real press each time. */
  animation: toggle-pop 0.28s ease;
}
@keyframes toggle-pop {
  0% { transform: scale(1); }
  45% { transform: scale(1.08); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .day-btn.active, .filter-toggle.active { animation: none; }
}
/* Floating "back to top" button (see /js/backToTop.js) - appended to
   <body> by that script rather than living in each page's own HTML, so
   every page gets it for free without a per-page markup change. */
.back-to-top {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 40;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  border: none;
  background: var(--accent);
  color: var(--accent-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 6px 18px var(--shadow);
  opacity: 0;
  transform: translateY(0.5rem) scale(0.9);
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease, background 0.15s ease;
}
.back-to-top:hover { opacity: 0.9; }
.back-to-top.is-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}
.back-to-top.is-pressed { animation: back-to-top-pulse 0.4s ease; }
@keyframes back-to-top-pulse {
  0% { transform: translateY(0) scale(1); }
  40% { transform: translateY(0) scale(0.85); }
  100% { transform: translateY(0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .back-to-top { transition: opacity 0.2s ease; }
  .back-to-top.is-visible { transform: none; }
  .back-to-top.is-pressed { animation: none; }
}
@media (max-width: 600px) {
  .back-to-top { right: 1rem; bottom: 1rem; width: 2.5rem; height: 2.5rem; }
}
.day-time-rows { display: flex; flex-direction: column; gap: 0.5rem; }
.day-time-row { max-width: 26rem; }
.day-time-label {
  display: flex;
  align-items: center;
  padding: 0 0.65rem 0 0.85rem;
  font-weight: 700;
  font-size: 0.85rem;
  color: var(--ink);
  min-width: 3.25rem;
}
.availability-hint { color: var(--muted); font-size: 0.88rem; margin: 0.25rem 0 0; }
.field-hint { color: var(--muted); font-size: 0.85rem; margin: 0.4rem 0 0; }
.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}
label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--ink-soft);
  margin-bottom: 0.4rem;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="time"],
input[type="tel"],
input[type="date"] {
  width: 100%;
  font-family: var(--font-body);
  font-size: 0.98rem;
  padding: 0.7rem 0.85rem;
  border-radius: 10px;
  border: 1px solid var(--paper-line);
  background: var(--paper);
  color: var(--ink);
  /* The native calendar icon (and popup) renders dark-on-dark and is
     nearly invisible on the dark theme otherwise - this tells the browser
     which color scheme its own form-control chrome should render in,
     independent of the page's own CSS colors around it. */
  color-scheme: light dark;
}
input::placeholder { color: var(--muted); }
/* Native number-input up/down steppers don't match anything else on the
   page - hide them and just leave a plain numeric field. */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }

.location-autocomplete { position: relative; width: 100%; }
/* Matches .nav-dropdown's format (the account menu) rather than the old
   divided-list look: rounded floating panel, no borders between rows,
   each row individually rounded on hover/active instead of a shared
   border-bottom rule - this is the dropdown "shape" used everywhere else
   on the site. */
.location-suggestions {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0;
  right: 0;
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
  z-index: 30;
  max-height: 14rem;
  overflow-y: auto;
  padding: 0.5rem;
}
.location-suggestion {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: 8px;
  padding: 0.55rem 0.7rem;
  font-family: var(--font-body);
  font-size: 0.92rem;
  color: var(--ink);
  cursor: pointer;
}
.location-suggestion:hover,
.location-suggestion.active { background: var(--paper-alt); }
input.invalid, textarea.invalid { border-color: var(--danger); box-shadow: 0 0 0 1px var(--danger); }
.form-alert--clickable { cursor: pointer; }
.form-alert--clickable:hover { text-decoration: underline; }

.password-input-wrap { position: relative; }
.password-input-wrap input { padding-right: 2.75rem; width: 100%; }
.password-toggle {
  position: absolute;
  top: 50%;
  right: 0.6rem;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.password-toggle:hover { color: var(--ink-soft); }
/* The [hidden] attribute doesn't reliably map to display:none on <svg>
   elements in every browser (a real, documented SVG-vs-HTML quirk, not the
   usual "authored display rule beats [hidden]" bug this codebase has hit
   before) - confirmed both icons rendering at once with computed
   display:block despite hidden being true. Toggling a class on the button
   itself instead sidesteps that quirk entirely rather than fighting it. */
.password-toggle .eye-closed { display: none; }
.password-toggle.showing .eye-open { display: none; }
.password-toggle.showing .eye-closed { display: block; }

.password-rules {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.3rem;
  font-size: 0.82rem;
  color: var(--muted);
}
.password-rules li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.password-rules li::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--muted);
  flex: none;
}
.password-rules li.met {
  color: var(--success);
}
.password-rules li.met::before {
  background: var(--success);
}

.form-alert {
  display: none;
  background: color-mix(in srgb, var(--danger) 12%, var(--card-bg));
  border: 1px solid var(--danger);
  color: var(--danger);
  border-radius: 10px;
  padding: 0.75rem 1rem;
  font-size: 0.88rem;
  margin-bottom: 1.1rem;
}
.form-alert.visible { display: block; }
.form-alert ul { margin: 0; padding-left: 1.1rem; }

.auth-foot {
  margin-top: 1.5rem;
  text-align: center;
  font-size: 0.9rem;
  color: var(--ink-soft);
}
.auth-foot a { color: var(--accent); text-decoration: none; font-weight: 600; }
.auth-foot a:hover { text-decoration: underline; }

/* ---------- dashboard ---------- */
.dash-shell {
  max-width: 40rem;
  margin: 0 auto;
  padding: clamp(3rem, 6vw, 5rem) 1.25rem;
}
.dash-card {
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  border-radius: 18px;
  padding: clamp(1.75rem, 4vw, 2.5rem);
}
.dash-card h1 {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.8rem;
  letter-spacing: -0.02em;
  margin: 0 0 0.5rem;
}
.dash-card p { color: var(--ink-soft); margin: 0 0 1.75rem; }

/* ---------- billing / plan comparison ---------- */
.plan-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.1rem; }
.plan-card {
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  border-radius: 18px;
  padding: 1.5rem 1.65rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
/* The three cards are equal height (grid row stretch), but each one's
   badge/button sat wherever its own feature list happened to end - since
   the lists are different lengths (2 vs 3 bullets, plus Pro's price line
   is taller than "Free"), that left the CTAs at three different heights
   instead of lining up in a row like the rest of the card. margin-top:
   auto on whichever element is last (badge, button, or the bullet list
   itself on the one plan/state combination with no action at all) pushes
   it to the bottom uniformly regardless of how much is above it. */
.plan-card > :last-child { margin-top: auto; }
.plan-card--current { border-color: var(--accent); }
.plan-card-name { font-family: var(--font-display); font-weight: 800; font-size: 1.2rem; margin: 0; }
.plan-card-price { font-size: 1.6rem; font-weight: 800; margin: 0; }
.plan-card-price span { font-size: 0.9rem; font-weight: 600; color: var(--muted); }
.plan-card ul { margin: 0; padding: 0 0 0 1.1rem; color: var(--ink-soft); font-size: 0.92rem; }
.plan-card li { margin-bottom: 0.35rem; }
.pending-payment-note { color: var(--accent); font-size: 0.82rem; font-weight: 700; margin: 0 0 0.5rem; }
.expiry-note { color: var(--muted); font-size: 0.82rem; margin: 0 0 0.5rem; }
.plan-card-badge {
  align-self: flex-start;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
}
@media (max-width: 640px) {
  .plan-grid { grid-template-columns: 1fr; }
}

.dash-meta {
  display: grid;
  gap: 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.88rem;
  background: var(--paper-alt);
  border-radius: 12px;
  padding: 1rem 1.15rem;
  margin-bottom: 1.75rem;
}
.dash-meta span.label { color: var(--muted); }

.resume-meta {
  color: var(--muted);
  font-size: 0.82rem;
  margin: 1.25rem 0 0.5rem;
}
/* Native file inputs render with raw OS chrome ("Choose File" button +
   filesystem-font label) that clashes with everything else on the page -
   ::file-selector-button lets us restyle the button half while the
   filename label just inherits normal body text/color. */
input[type="file"] {
  font-family: var(--font-body);
  font-size: 0.9rem;
  color: var(--ink-soft);
}
input[type="file"]::file-selector-button {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.88rem;
  color: var(--ink);
  background: var(--chip);
  border: 1px solid var(--paper-line);
  border-radius: 8px;
  padding: 0.55rem 0.95rem;
  margin-right: 0.85rem;
  cursor: pointer;
  transition: background 0.15s ease;
}
input[type="file"]::file-selector-button:hover { background: var(--paper-alt); }

.upload-row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; }
/* min-width: 14rem here used to be an unconditional floor - harmless on
   desktop where there's room, but a native <input type="file"> has its own
   browser-drawn "Choose File" control whose real minimum rendering width
   isn't fully bounded by CSS the way a text input's is, and on a narrow
   mobile viewport (.upload-row's available width is under 352px once
   .dash-card's padding is subtracted) the combination pushed the whole
   page to ~450px wide - the same min-content-floor-forces-page-overflow
   class of bug as .job-card-flat/.application-info above, just on a
   native form control instead of a text node. flex: 1 alone already
   makes it grow to fill available space on wide viewports without
   needing a hard floor. */
.upload-row input[type="file"] { flex: 1; min-width: 0; }
/* flex: 1 + min-width: 0 above stops the native file input from forcing
   page overflow (see the comment above), but on a narrow mobile row it
   still only leaves the input ~160px next to the Upload button - not
   page-breaking, but too little room for the browser's own "No file
   chosen" label, which Chromium then truncates to "N...n" mid-word.
   Stacking the row instead of sharing it gives the input the full width
   it needs to render that label legibly. */
@media (max-width: 600px) {
  .upload-row { flex-direction: column; align-items: stretch; }
  /* align-items: stretch alone isn't enough to keep a native file input
     from rendering at its own min-content width (button + full "No file
     chosen" label) instead of the container's width - explicit width:
     100% plus the min-width: 0 reset (needed here again since a plain
     flex: none, unlike the row layout's flex: 1, doesn't already imply
     it) is what actually caps it, same overflow class as the row layout
     this replaces on narrow screens. */
  .upload-row input[type="file"] { flex: none; width: 100%; min-width: 0; }
}
/* .btn's site-wide white-space: nowrap (fine for the vast majority of
   short, fixed button labels) means a button's own min-content is its
   full unwrapped text width - the quiz category buttons here render
   "Take quiz: <category label>" (server-driven, not a short static
   string) and the longest current one alone is ~344px, wider than
   .dash-card's ~352px content width leaves much room for on mobile once
   its own padding is subtracted. Even though buttons wrap onto separate
   lines from each other just fine (normal block/inline flow), that single
   button's un-wrappable text was still enough on its own to inflate this
   page's main.page-shell auto-min-size well past the viewport - scoped
   here rather than changing .btn everywhere, since nowrap is still correct
   for e.g. "Save name"/"Upload". */
#skill-categories-list button { white-space: normal; text-align: left; }
.resume-output-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-top: 1.25rem; }
.resume-output-head .resume-meta { margin: 0; }
.resume-text {
  white-space: pre-wrap;
  font-family: var(--font-body);
  font-size: 0.92rem;
  line-height: 1.6;
  background: var(--paper-alt);
  border-radius: 12px;
  padding: 1.1rem 1.25rem;
  margin: 0;
}

textarea {
  width: 100%;
  font-family: var(--font-body);
  font-size: 0.98rem;
  padding: 0.7rem 0.85rem;
  border-radius: 10px;
  border: 1px solid var(--paper-line);
  background: var(--paper);
  color: var(--ink);
  resize: vertical;
  min-height: 6rem;
}

/* ---------- page shells (jobs, employer) ---------- */
/* .page-shell is a flex item of body (display: flex; flex-direction:
   column - see body above). margin: 0 auto is what centers it once
   max-width caps its growth on wide viewports - but on a flex item, an
   auto margin on the cross axis (left/right, for a column-direction
   container) takes priority over stretch alignment per spec, REGARDLESS
   of align-items/align-self, and disables it entirely. Without stretch,
   width: auto instead falls back to fit-content sizing, which - unlike
   stretch - is driven by descendant content, not by the container's
   available width. Multiple unrelated descendants across different pages
   have each independently pushed that fit-content size past the mobile
   viewport this way (long job titles, a nowrap button with a server-driven
   label, a native <input type="file">'s own unsuppressable intrinsic
   width) - each is a real, narrow bug on its own, but they kept recurring
   in new places because the actual defect was here the whole time: this
   element should never have been sized by its content at all. width: 100%
   restores the intended behavior (fill available width, then let
   max-width + margin: auto cap and center it on wide viewports) without
   depending on stretch. */
.page-shell {
  width: 100%;
  max-width: 60rem;
  margin: 0 auto;
  padding: clamp(2.5rem, 6vw, 4rem) 1.25rem clamp(3rem, 7vw, 5rem);
}
/* Narrower column for form/settings-style pages (profile) - the site's
   default 60rem is sized for job-listing grids, but a page that's really
   just a single-column stack of short inputs and paragraphs reads as
   sparse and overly wide at that size on a normal desktop screen. Same
   idea as .auth-card--wide above, just for the page shell itself. */
.page-shell--narrow { max-width: 44rem; }
.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  margin-bottom: 2rem;
}
.page-head h1 {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.8rem, 3.4vw, 2.3rem);
  letter-spacing: -0.02em;
  margin: 0 0 0.35rem;
}
.page-head p { color: var(--ink-soft); margin: 0; }

.info-note {
  display: none;
  background: color-mix(in srgb, var(--accent) 10%, var(--card-bg));
  border: 1px solid var(--paper-line);
  color: var(--ink-soft);
  border-radius: 10px;
  padding: 0.75rem 1rem;
  font-size: 0.88rem;
  margin-bottom: 1.5rem;
}
.info-note.visible { display: block; }
.info-note a { color: var(--accent); font-weight: 600; text-decoration: none; }
.info-note a:hover { text-decoration: underline; }

/* ---------- post-signup onboarding banner (see onboardingHomeForRole in
   src/profiles.js) - same display:none + .visible pattern as .info-note
   above, for the same reason: an authored display rule always beats the
   browser's [hidden] default, so toggling [hidden] alone would leave this
   showing unconditionally. */
.onboarding-banner {
  display: none;
  background: color-mix(in srgb, var(--accent) 12%, var(--card-bg));
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--paper-line));
  border-radius: 14px;
  padding: 1.25rem 1.5rem;
  margin-bottom: 1.5rem;
}
.onboarding-banner.visible { display: block; }
.onboarding-banner h2 { margin: 0 0 0.35rem; font-size: 1.15rem; }
.onboarding-banner p { margin: 0 0 1rem; color: var(--ink-soft); font-size: 0.92rem; }
.onboarding-steps { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1.1rem; }
.onboarding-step { display: flex; align-items: center; gap: 0.6rem; font-size: 0.9rem; color: var(--ink-soft); }
.onboarding-step-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--chip);
  color: var(--ink-soft);
  font-size: 0.78rem;
  font-weight: 700;
  flex: none;
}
.onboarding-step.current { color: var(--ink); font-weight: 600; }
.onboarding-step.current .onboarding-step-num { background: var(--accent); color: var(--accent-ink); }
.onboarding-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }

/* display:none by default, NOT the [hidden] attribute - an explicit
   display on the same element that's also toggled via [hidden] always
   wins over the browser's [hidden]{display:none} default (author styles
   beat user-agent styles regardless of specificity), so the banner was
   rendering every time regardless of its hidden state. .visible mirrors
   the .info-note pattern above for exactly this reason. */
.preview-banner {
  display: none;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  background: color-mix(in srgb, var(--accent) 10%, var(--card-bg));
  border: 1px solid var(--paper-line);
  border-radius: 12px;
  padding: 0.85rem 1.1rem;
  font-size: 0.92rem;
  font-weight: 600;
  margin-bottom: 1.5rem;
}
.preview-banner.visible { display: flex; }
.preview-banner .btn { flex: none; }

.search-row {
  margin-bottom: 1.75rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
.search-row input[type="search"] {
  /* Was 16rem - at .page-shell's max-width, that left the row about 8px
     too tight to fit all three filter pills alongside it, so Eco-friendly
     silently wrapped to its own line on both /jobs and /volunteer
     regardless of viewport width (the page-shell cap, not the browser
     window, was the actual constraint). 13rem still leaves the search box
     a reasonable minimum width. */
  flex: 1 1 13rem;
  font-family: var(--font-body);
  font-size: 1rem;
  padding: 0.8rem 1rem;
  border-radius: 12px;
  border: 1px solid var(--paper-line);
  background: var(--card-bg);
  color: var(--ink);
}
.filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin: -1rem 0 1.75rem;
}
.filter-field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  flex: 1 1 12rem;
}
.filter-field label { font-size: 0.78rem; font-weight: 600; color: var(--muted); }
.filter-field input, .filter-field select {
  font-family: var(--font-body);
  font-size: 0.92rem;
  padding: 0.6rem 0.8rem;
  border-radius: 10px;
  border: 1px solid var(--paper-line);
  background: var(--card-bg);
  color: var(--ink);
}

/* Filter pills reuse .day-btn's exact look (see the availability picker's
   Sun/Mon/Tue toggles further down this file) - a native checkbox would
   show up looking out of place next to the rest of the site's
   custom-styled controls. white-space: nowrap is the one addition these
   need beyond the base .day-btn rule, since a filter label is longer than
   a day abbreviation. */
.filter-toggle { white-space: nowrap; }

.recommended-heading {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.15rem;
  margin: 0 0 1rem;
}
#recommended-section { margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--paper-line); }

/* ---------- job listing cards (flat grid variant) ---------- */
.job-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: 1.1rem;
}
.job-card-flat {
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  border-radius: 14px;
  padding: 1.25rem 1.35rem;
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
  /* .job-grid's minmax(16rem, 1fr) tracks only bound the GRID TRACK's
     size - grid ITEMS (this card) still default to min-width: auto,
     which respects their content's min-content size regardless of that
     track minimum. Job titles have no length cap anywhere (client or
     server), and an unusually long, unbroken title set as the .role text
     below could otherwise force this card wider than its grid track. */
  min-width: 0;
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.job-card-flat:hover {
  transform: translateY(-3px);
  box-shadow: 0 10px 24px var(--shadow);
  border-color: var(--accent);
}
@media (prefers-reduced-motion: reduce) {
  .job-card-flat { transition: none; }
  .job-card-flat:hover { transform: none; }
}
.job-card-flat .role {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.08rem;
  margin: 0;
  letter-spacing: -0.01em;
  overflow-wrap: break-word;
}
.job-card-flat .biz { color: var(--muted); font-size: 0.88rem; margin: 0; display: flex; align-items: center; gap: 0.4rem; }
.verified-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  color: var(--success);
  font-size: 0.76rem;
  font-weight: 700;
}
.eco-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  color: var(--success);
  font-size: 0.76rem;
  font-weight: 700;
}
.job-card-flat .desc { color: var(--ink-soft); font-size: 0.9rem; margin: 0; }
/* This used to be flex: 1 on .desc, to stretch the description and
   equalize each card's height within an equal-height grid row - but a
   flex-grow item claims 100% of a flex container's leftover space during
   the "resolve flexible lengths" step, which runs BEFORE auto margins are
   resolved. That left the action buttons at a different height
   card-to-card, since different jobs have a different number of optional
   lines above them (expiry note, "no experience needed", positions-
   filled, etc. are all conditionally rendered), yet nothing was pushing
   them to a shared bottom edge - flex: 1 only inflated .desc itself,
   invisibly, by however much extra height that specific card had.
   Dropping flex: 1 here (so nothing else is competing for the leftover
   space) and giving margin-top: auto to whichever element actually holds
   the actions instead - .job-card-actions (jobs.js/volunteer.js: Apply +
   Practice interview) or .btn-row (employer-dashboard.js: Preview/Edit/
   Delete/etc.) - is what correctly pins the buttons to the same bottom
   edge on every card, regardless of how much (or how little) content is
   above them. */
.job-card-flat .job-card-actions { display: flex; flex-direction: column; gap: 0.65rem; margin-top: auto; }
.job-card-flat .btn-row { margin-top: auto; }
.job-card-flat .meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.job-card-flat .pay { color: var(--ink); font-weight: 600; }
.job-card-flat .tag {
  background: var(--chip);
  color: var(--chip-ink);
  border: 1px solid var(--paper-line);
  border-radius: 5px;
  padding: 0.15rem 0.45rem;
  font-size: 0.72rem;
  font-weight: 400;
}
.job-card-flat .location { color: var(--muted); font-size: 0.82rem; }
.est-yearly { color: var(--muted); font-size: 0.78rem; margin: 0; }
.reco-note { color: var(--success); font-size: 0.82rem; margin: 0; font-weight: 600; }
.beginner-note { color: var(--accent); font-size: 0.82rem; margin: 0; font-weight: 600; }
.age-note { color: var(--muted); font-size: 0.82rem; margin: 0; font-weight: 600; }
.modal-preview-note { color: var(--accent); font-size: 0.82rem; font-weight: 700; margin: -0.6rem 0 1.1rem; }

.empty-state {
  border: 1px dashed var(--paper-line);
  border-radius: 14px;
  padding: 2.5rem 1.5rem;
  text-align: center;
  color: var(--muted);
}

/* ---------- account-type picker ---------- */
.role-picker {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.85rem;
  margin-bottom: 1.5rem;
}
.role-option {
  display: flex;
  position: relative;
  cursor: pointer;
}
/* [hidden] attribute selector beats the bare class rule above on
   specificity (0,2,0 vs 0,1,0), so this actually wins - unlike a plain
   `.role-option { display: flex }` + `hidden` combination, where the
   author's own unconditional display would otherwise always override the
   browser's built-in [hidden]{display:none} default regardless of the
   attribute being set. choose-account-type.js relies on exactly this to
   hide whichever role you already have. */
.role-option[hidden] { display: none; }
.role-option input { position: absolute; opacity: 0; pointer-events: none; }
.role-option .role-card {
  display: flex;
  flex-direction: column;
  flex: 1;
  border: 1px solid var(--paper-line);
  border-radius: 14px;
  padding: 1.1rem 1rem;
  background: var(--chip);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.role-option .role-card strong {
  display: block;
  font-family: var(--font-display);
  font-size: 1rem;
  margin-bottom: 0.25rem;
}
.role-option .role-card span { color: var(--muted); font-size: 0.83rem; }
.role-option input:checked + .role-card {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--chip));
}
.role-option input:focus-visible + .role-card {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- checkbox ---------- */
/* No native checkbox existed anywhere in the app before this - every prior
   boolean field (experienceLevel, willingToWriteRecommendation, jobType)
   used the .role-option radio-pair pattern above instead. This is a real
   custom-styled checkbox (appearance: none + a CSS-drawn checkmark, no
   image asset) for the one case that's genuinely a single yes/no toggle
   rather than a choice between two labeled options. */
.checkbox-field {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  cursor: pointer;
}
.checkbox-field input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  flex: none;
  margin: 0.1rem 0 0;
  border: 1px solid var(--paper-line);
  border-radius: 6px;
  background: var(--chip);
  display: grid;
  place-content: center;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.checkbox-field input[type="checkbox"]::before {
  content: "";
  width: 11px;
  height: 11px;
  transform: scale(0);
  transition: transform 0.12s ease;
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
  background: var(--accent-ink);
}
.checkbox-field input[type="checkbox"]:checked {
  border-color: var(--accent);
  background: var(--accent);
}
.checkbox-field input[type="checkbox"]:checked::before { transform: scale(1); }
.checkbox-field input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.checkbox-field .checkbox-label { display: block; font-weight: 600; font-size: 0.9rem; }
.checkbox-field .checkbox-hint { display: block; color: var(--muted); font-size: 0.83rem; font-weight: 400; margin-top: 0.15rem; }

select {
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 600;
  padding: 0.5rem 0.75rem;
  border-radius: 8px;
  border: 1px solid var(--paper-line);
  background: var(--paper);
  color: var(--ink);
}

/* ---------- status badges ---------- */
.status-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: 0.25rem 0.55rem;
  border-radius: 6px;
  border: 1px solid transparent;
  white-space: nowrap;
}
.status-badge::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.status-badge.tone-neutral { color: var(--ink-soft); background: var(--chip); border-color: var(--paper-line); }
.status-badge.tone-active { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--chip)); border-color: color-mix(in srgb, var(--accent) 35%, var(--paper-line)); }
.status-badge.tone-positive { color: var(--success); background: color-mix(in srgb, var(--success) 14%, var(--chip)); border-color: color-mix(in srgb, var(--success) 35%, var(--paper-line)); }
.status-badge.tone-muted { color: var(--muted); background: var(--paper-alt); border-color: var(--paper-line); }

/* ---------- tabs ---------- */
.tab-row {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-bottom: 1.5rem;
  border-bottom: 1px solid var(--paper-line);
}
.tab-row button {
  font-family: var(--font-body);
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--muted);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 0.6rem 0.2rem;
  margin-bottom: -1px;
  cursor: pointer;
}
.tab-row button:hover { color: var(--ink-soft); }
.tab-row button.active { color: var(--ink); border-bottom-color: var(--accent); }
.tab-row button .count { color: var(--muted); font-weight: 400; }

/* ---------- candidate list (employer view of one job's applicants) ---------- */
.candidate-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  border-radius: 12px;
  padding: 1rem 1.15rem;
}
.candidate-row + .candidate-row { margin-top: 0.75rem; }
/* Same min-content blowout as .job-card-flat/.application-info above -
   .candidate-info is a flex item in .candidate-row, and its .email span
   appends "applied to " + the job's title (uncapped length). min-width: 0
   on .candidate-info isn't enough on its own here: per spec, overflow-wrap:
   break-word creates break opportunities for actual rendering but is
   explicitly excluded from min-content/intrinsic size calculations - so an
   unbroken title still forced this row's min-content size (and, through
   it, .page-shell's own flex-item auto-min-size several levels up in
   body's column flex layout) wide enough to blow out the whole page, even
   with min-width: 0 on every flex item in between. overflow-wrap: anywhere
   (unlike break-word) DOES count toward intrinsic sizing, which is what
   actually stops the upward min-content propagation at its source. */
.candidate-info { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.candidate-info .name { font-family: var(--font-display); font-weight: 700; font-size: 1rem; }
.candidate-info .email { color: var(--muted); font-size: 0.85rem; overflow-wrap: anywhere; min-width: 0; }
.candidate-actions { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
/* .candidate-detail is a flex item of .candidate-row (sibling of
   .candidate-info/.candidate-actions), toggled via [hidden] rather than
   removed from the DOM. Its own width: 100% doesn't stop the same
   min-content blowout seen elsewhere on this page (.candidate-info,
   .application-info): min-width: auto still applies as a floor on top of
   an explicit width, and .qa-answer holds a student's free-text answer to
   a custom application question, which has no length cap and could be one
   unbroken run of characters. min-width: 0 here plus overflow-wrap:
   anywhere on .qa-answer (not break-word - see .application-info .role
   above for why that distinction matters) closes it off the same way. */
.candidate-detail { width: 100%; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--paper-line); font-size: 0.88rem; color: var(--ink-soft); min-width: 0; }
.candidate-detail p { margin: 0 0 0.5rem; }
.candidate-detail .resume-text { font-size: 0.85rem; }
.qa-pair {
  background: var(--paper-alt);
  border-radius: 10px;
  padding: 0.65rem 0.85rem;
  margin-bottom: 0.5rem;
}
.qa-question { display: block; font-weight: 600; color: var(--ink); font-size: 0.85rem; margin-bottom: 0.25rem; overflow-wrap: anywhere; }
.qa-answer { display: block; color: var(--ink-soft); overflow-wrap: anywhere; }

/* ---------- application questions builder (employer posting a job) ---------- */
.questions-list { display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 0.75rem; }
.question-row { display: flex; gap: 0.5rem; align-items: center; }
.question-row .question-input { flex: 1; }
.btn-remove-question { flex: none; padding: 0.5rem 0.75rem; font-size: 1.1rem; line-height: 1; }

/* ---------- experience entries builder (student profile) ---------- */
.field-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.75rem; }
.experience-list { display: flex; flex-direction: column; gap: 1rem; margin-bottom: 0.75rem; }
.experience-entry { border: 1px solid var(--paper-line); border-radius: 12px; padding: 1rem 1.1rem 0.25rem; }
.experience-entry-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
.experience-entry-head span { font-weight: 700; font-size: 0.85rem; color: var(--ink-soft); }
.btn-remove-experience { flex: none; padding: 0.35rem 0.65rem; font-size: 1.1rem; line-height: 1; }

/* ---------- apply-questions modal (student applying to a job) ---------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.25rem;
  z-index: 50;
}
.modal-card {
  width: 100%;
  max-width: 44rem;
  max-height: 90vh;
  overflow-y: auto;
  background: var(--card-bg);
  border-radius: 18px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
  padding: clamp(1.75rem, 4vw, 2.75rem);
}
.modal-card--narrow {
  max-width: 26rem;
}
.modal-step-indicator {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--accent);
  margin: 0 0 0.5rem;
}
.modal-card h2 { margin: 0 0 0.4rem; font-family: var(--font-display); font-weight: 800; font-size: 1.3rem; }
.modal-sub { color: var(--ink-soft); margin: 0 0 1.25rem; font-size: 0.95rem; }
/* flex-wrap: wrap (missing before, same fix already applied to .btn-row) -
   without it, a modal with several buttons whose combined nowrap text
   doesn't fit the card's width on a narrow viewport doesn't grow the row
   or shrink the buttons, it just clips them against the card edge, cutting
   off the start/end of the text on both sides. Pre-existing on every modal
   sharing this class (applyModal, interviewCoach, the recommendation-
   letter modal, etc.), not specific to any one button's label length. */
.modal-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.6rem; margin-top: 1.25rem; }
.modal-contact { text-align: center; color: var(--muted); font-size: 0.82rem; margin: 1rem 0 0; }
.modal-contact a { color: var(--accent); font-weight: 600; text-decoration: none; }
.modal-contact a:hover { text-decoration: underline; }

/* ---------- application list (student's "My applications") ---------- */
.application-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  border-radius: 12px;
  padding: 1rem 1.15rem;
}
.application-row + .application-row { margin-top: 0.75rem; }
.application-actions { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
/* Same min-content blowout as .job-card-flat above - .application-row's
   own flex-wrap only lets its ITEMS move to new lines, it doesn't shrink
   a single item (.application-info) below its content's min-content
   width, which an unbroken long job title would otherwise force to be
   enormous. overflow-wrap: anywhere (not break-word) on .role matters here
   too - break-word's break opportunities are excluded from min-content/
   intrinsic size calculations per spec, so they don't stop this row's
   size from propagating up through body's column flex layout into
   .page-shell itself; anywhere is counted, so it actually stops it. */
.application-info { min-width: 0; }
.application-info .role { font-family: var(--font-display); font-weight: 700; font-size: 1rem; margin: 0 0 0.15rem; overflow-wrap: anywhere; }
.application-info .biz { color: var(--muted); font-size: 0.85rem; margin: 0; }

@media (max-width: 480px) {
  .field-row { grid-template-columns: 1fr; }
  .field-row-3 { grid-template-columns: 1fr; }
  .role-picker { grid-template-columns: 1fr; }
  .candidate-row, .application-row { flex-direction: column; align-items: flex-start; }
}

@media (max-width: 760px) {
  .nav-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-start;
  }
  .nav-toggle { display: flex; margin-left: auto; }
  /* .nav-profile (and #theme-toggle inside it) is meant to stay anchored
     in the top row at all times on mobile, next to the hamburger - but
     margin-left: auto on .nav-toggle only pushes items along the SAME flex
     line; it does nothing once nav.links.nav-center (right after
     .nav-toggle, right before .nav-profile, in DOM order) becomes
     `display: flex; width: 100%` when the menu opens. A 100%-width flex
     item forces everything after it in the (order-adjusted) sequence onto
     a new line - so without this, opening the hamburger visibly relocated
     the theme toggle from the top bar down into the expanded panel,
     exactly the "the toggle is in the hamburger menu" bug report this
     was written to fix. order lets nav-profile jump ahead of nav-center in
     layout sequence without touching DOM/HTML order (shared across 21
     pages) - it now always lands on the first line with the logo and
     hamburger, and nav-center is what wraps onto its own line instead. */
  .logo { order: 0; }
  .nav-toggle { order: 1; }
  .nav-profile { order: 2; }
  nav.links.nav-center, #nav-auth { order: 3; }
  /* Removed from mobile entirely (not just repositioned) - it was
     confusing users regardless of exactly where it sat next to the
     hamburger. Desktop and the floating variant on the 3 bare auth pages
     (.theme-toggle--floating, which isn't inside .nav-profile at all) are
     untouched; the site still respects the OS-level prefers-color-scheme
     automatically either way, this only removes the manual override
     control on small screens. */
  .nav-profile #theme-toggle { display: none; }
  .nav-avatar-btn,
  .nav-profile-label { display: none; }
  .nav-row.nav-open .nav-avatar-btn { display: flex; }
  .nav-row.nav-open .nav-profile-label { display: inline-flex; }
  /* nav.links { display: flex } (element + class) outweighs a plain
     .nav-center/.nav-auth class selector regardless of source order - match
     its specificity (nav.links.nav-center) so this actually wins. #nav-auth
     needs its own ID-specificity override too: the id="nav-auth" attribute
     sits on the <nav class="nav-auth"> element itself (unlike #nav-links,
     which is on an inner <span>), so the earlier #nav-auth { display: flex }
     rule (ID specificity) would otherwise beat a mere class selector here. */
  nav.links.nav-center,
  #nav-auth {
    display: none;
    width: 100%;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.6rem;
  }
  .nav-row.nav-open nav.links.nav-center {
    display: flex;
    margin-top: 0.85rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--paper-line);
  }
  .nav-row.nav-open #nav-auth {
    display: flex;
    margin-top: 0.6rem;
  }
  #nav-links {
    flex-direction: column;
    align-items: flex-start;
    width: 100%;
    gap: 0.6rem;
  }
  .nav-center .btn,
  .nav-auth .btn { width: 100%; justify-content: center; }
  /* "Sign in" is a plain text link on desktop (secondary, next to the
     boxed "Sign up" button) - fine inline, but stacked full-width in the
     mobile menu the mismatch (bare text row next to a button box) looks
     broken. Give it the same button shape/size here, styled like
     .btn-ghost, so the pair reads as two matching same-size buttons. */
  .nav-auth .nav-signin {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    font-weight: 600;
    font-size: 0.95rem;
    text-decoration: none;
    border-radius: 10px;
    padding: 0.65rem 1.15rem;
    border: 1px solid var(--paper-line);
    color: var(--ink);
  }
  .nav-auth .nav-signin::after { display: none; }
}

/* ---------- shared site footer (every page, including index.html).
   Same theme-aware glass treatment as the header now, so the two bars at
   the top and bottom of the page still match each other in either theme
   instead of the footer staying dark regardless. margin-top gives it
   visible breathing room from whatever content ends the page above it,
   instead of sitting flush against it. */
.site-footer {
  margin-top: clamp(2.5rem, 6vw, 4rem);
  border-top: 1px solid var(--paper-line);
  background: color-mix(in srgb, var(--card-bg) 70%, transparent);
  backdrop-filter: blur(14px);
}
.site-footer-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.25rem 2rem;
  justify-content: space-between;
  /* padding-top/bottom only (not the padding shorthand) - .site-footer-row
     and .wrap are the same element, and the shorthand would silently reset
     .wrap's horizontal padding to 0 instead of layering with it, since
     shorthand properties replace rather than merge. Same bug class as
     .nav-row above and .hero in landing.css - was pinning the footer text
     flush against the viewport edges on every page. Bumped from 1.5rem to
     give the footer real breathing room instead of feeling like a single
     cramped afterthought line. */
  padding-top: 2.25rem;
  padding-bottom: 2.25rem;
}
.site-footer-brand {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.site-footer-logo {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.02rem;
  color: var(--ink);
}
.site-footer-logo .mark { height: 1.2rem; width: auto; flex: none; }
.site-footer-brand p { margin: 0; font-size: 0.8rem; color: var(--ink-soft); }
.site-footer-contact { display: flex; align-items: center; gap: 1.1rem; font-size: 0.85rem; }
.site-footer-contact a {
  color: var(--ink-soft);
  text-decoration: none;
  font-weight: 600;
}
.site-footer-contact a:hover { color: var(--ink); text-decoration: underline; }
@media (max-width: 640px) {
  .site-footer-row { flex-direction: column; align-items: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
  /* The blanket transition:none above never touched this - CSS
     animations and transitions are separate mechanisms, and
     header.nav.nav-drop-in (see the nav section above) uses `animation`.
     Without this, someone who has explicitly told their OS/browser they
     want reduced motion would still get the header's 1.4s
     translateY(-100%) -> 0 drop-in on every scroll-triggered re-reveal,
     not just initial load - exactly the kind of motion this preference
     exists to suppress. animation: none collapses it to its end state
     immediately (final keyframe), so the header just appears normally
     instead of animating in. */
  * { animation: none !important; }
}
