:root {
  /* =====================================================================
     Nature semantic tokens (default pack) — see docs/superpowers/specs/
     2026-09-12-site-redesign-design.md, "PR 1" and "Compatibility aliases".
     Copied from backend/app/static/css/style.themes.draft.css (Nature
     block only; Dusk is not wired in yet). Component rules below still
     consume the old --forest-*/--paper-*/--ink-*/etc. primitive names —
     the compatibility-alias block at the end maps every one of them onto
     these semantic tokens so this is a visual no-op. Later PRs rewrite
     component rules to the semantic names directly and then delete the
     alias block.
     ===================================================================== */
  color-scheme: light;

  /* Surfaces */
  --surface-canvas: #faf7f0;            /* page background; was --paper-50 */
  --surface-canvas-subtle: #f3efe4;     /* inputs, sunken wells; was --paper-100 */
  --surface-raised: #ffffff;            /* cards, tables, modals (was hardcoded #fff) */
  --surface-sunken: #f3efe4;
  --surface-nav: rgba(250, 247, 240, 0.88);
  --surface-nav-solid: #f7f3e8;
  --surface-overlay: rgba(27, 58, 43, 0.45);
  --surface-inverse: #1b3a2b;           /* cookie banner, inverse chips */
  --surface-wash-sage: rgba(216, 243, 220, 0.55);
  --surface-wash-sun: #fff3d6;

  /* Text */
  --text-primary: #1f2921;              /* was --ink-900 */
  --text-secondary: #4b5a51;            /* was --ink-600 */
  --text-muted: #5c5648;                /* AA ≥4.5:1 on --surface-canvas and --surface-raised; was #a49c88 */
  --text-inverse: #faf7f0;
  --text-link: #2d6a4f;
  --text-on-accent: #ffffff;
  --text-on-sun: #1b3a2b;
  --text-danger: #c1440e;

  /* Brand / accent */
  --accent-strong: #1b3a2b;             /* was --forest-900 */
  --accent: #2d6a4f;                    /* was --forest-700 */
  --accent-soft: #40916c;               /* was --forest-500 */
  --accent-muted: #74c69d;              /* was --sage-400 */
  --accent-wash: #d8f3dc;               /* was --sage-200 */
  --sun: #ffb703;                       /* was --sun-400 */
  --sun-strong: #f4a533;                /* was --sun-500 */
  --sun-wash: #fff3d6;
  --bark: #3a2e26;                      /* was --bark-800; rarely used */

  /* Decorative leaf wash (body background radials) */
  --decor-leaf: var(--accent-muted);
  --decor-sun: var(--sun);
  --decor-leaf-opacity: 0.35;
  --decor-sun-opacity: 0.25;

  /* Borders */
  --border-subtle: rgba(45, 106, 79, 0.12);
  --border-default: #e2ddd0;            /* input borders, table rules */
  --border-strong: #dcd4c2;             /* dashed capture well */
  --border-focus: var(--accent-muted);
  --border-accent: rgba(45, 106, 79, 0.22);
  --accent-wash-hover: rgba(45, 106, 79, 0.08); /* nav-links a:hover background; no existing token matched this alpha */

  /* Status (pills, banners) */
  --status-success-bg: #d8f3dc;
  --status-success-fg: #2d6a4f;
  --status-warning-bg: #fff3d6;
  --status-warning-fg: #1f2921;
  --status-danger-bg: #fbe4dd;
  --status-danger-fg: #c1440e;
  --status-caution-bg: #fde2c8;
  --status-caution-fg: #b5651d;
  --status-info-bg: #d8f3dc;
  --status-info-fg: #1b3a2b;
  --danger: #c1440e;                    /* kept name; semantic alias of --status-danger-fg */

  /* Like / vote */
  --like-active-bg: #fbe4dd;
  --like-active-fg: #c1440e;

  /* Shadows (tinted with forest, not generic black) */
  --shadow-soft: 0 10px 30px rgba(27, 58, 43, 0.08);
  --shadow-lift: 0 16px 40px rgba(27, 58, 43, 0.16);
  --shadow-banner: 0 -6px 24px rgba(0, 0, 0, 0.15);

  /* Radii */
  --radius-lg: 20px;
  --radius-md: 14px;
  --radius-sm: 8px;
  --radius-pill: 999px;

  /* Spacing scale (4px base). Existing pages use ad-hoc 6/8/10/12/14/16/18/20/24/28/32;
     new rules should pick from this scale so density stays consistent. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;

  /* Typography */
  --font-display: "Fraunces", Georgia, serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-size-root: 16px;
  --line-body: 1.5;

  /* Focus ring — 3px --accent (~5.8:1 on paper) + 2px offset. Replaces outline:none. */
  --focus-ring: 0 0 0 3px var(--accent);
  --focus-ring-inverse: 0 0 0 3px var(--surface-canvas);

  /* Layout */
  --page-max: 880px;
  --feed-max: 640px;
  --auth-max: 460px;
  --nav-height-top: 56px;
  --nav-height-bottom: 64px;
  --cookie-banner-height: 7rem; /* extra main/footer pad while html.cookie-banner-visible */
  --z-decor: 0;
  --z-page: 1;
  --z-nav: 5;
  --z-banner: 10;
  --z-modal: 20;

  /* Motion */
  --ease: 0.15s ease;
  --ease-lift: 0.1s ease;

  /* Badge artwork defaults (SVG custom properties) */
  --badge-fill: #f7f3e8;
  --badge-rim: #2d6a4f;
  --badge-metal: #e8a317;

  /* Compatibility aliases — map the old primitive names onto the new
     semantic tokens so an unmodified component rule such as
     `color: var(--forest-700)` still resolves after this block. Drop this
     block once every rule in this file has been rewritten to semantic
     names. */
  --forest-900: var(--accent-strong);
  --forest-700: var(--accent);
  --forest-500: var(--accent-soft);
  --sage-400: var(--accent-muted);
  --sage-200: var(--accent-wash);
  --sun-500: var(--sun-strong);
  --sun-400: var(--sun);
  --bark-800: var(--bark);
  --paper-50: var(--surface-canvas);
  --paper-100: var(--surface-canvas-subtle);
  --ink-900: var(--text-primary);
  --ink-600: var(--text-secondary);

  font-size: 16px;
}

/* =========================================================================
   Dusk pack (PR 6 — docs/superpowers/specs/2026-09-12-site-redesign-design.md,
   "Palette — Dusk" / "Theme mechanism"). Values copied from
   style.themes.draft.css's worked example (plus --accent-wash-hover, added
   to production's Nature block after that draft was written — see PR 2/3).
   Night field-guide: deep forest canvas, moonlight sage, amber — a true
   inversion of Nature, not a generic grey dark mode. Redefines every
   semantic (non-layout) token name from the :root block above so
   `html[data-theme="dusk"]` is a complete pack, not a partial override —
   see the token-parity test (backend/tests/test_theme_tokens.py).
   ========================================================================= */
html[data-theme="dusk"] {
  color-scheme: dark;

  --surface-canvas: #121a16;
  --surface-canvas-subtle: #1a2420;
  --surface-raised: #1c2822;
  --surface-sunken: #0e1512;
  --surface-nav: rgba(18, 26, 22, 0.92);
  --surface-nav-solid: #15201b;
  --surface-overlay: rgba(0, 0, 0, 0.62);
  --surface-inverse: #d8f3dc;
  --surface-wash-sage: rgba(143, 203, 176, 0.12);
  --surface-wash-sun: rgba(224, 168, 74, 0.16);

  --text-primary: #e8ede8;
  --text-secondary: #b4c2ba;
  --text-muted: #8a9a90;             /* AA >=4.5:1 on --surface-canvas and --surface-raised */
  --text-inverse: #121a16;
  --text-link: #8fcbb0;
  --text-on-accent: #121a16;
  --text-on-sun: #121a16;
  --text-danger: #f0a58a;

  --accent-strong: #cde8d8;
  --accent: #8fcbb0;                 /* moonlight sage */
  --accent-soft: #5fa88a;
  --accent-muted: #4e8f74;
  --accent-wash: #244036;
  --sun: #e0a84a;                    /* amber */
  --sun-strong: #f0c56b;
  --sun-wash: rgba(224, 168, 74, 0.18);
  --bark: #cbb8a4;

  --decor-leaf: #4e8f74;
  --decor-sun: #e0a84a;
  --decor-leaf-opacity: 0.22;
  --decor-sun-opacity: 0.16;

  --border-subtle: rgba(143, 203, 176, 0.14);
  --border-default: #2a3a32;
  --border-strong: #3a4e44;
  --border-focus: #8fcbb0;
  --border-accent: rgba(143, 203, 176, 0.35);
  --accent-wash-hover: rgba(143, 203, 176, 0.10);

  --status-success-bg: #244036;
  --status-success-fg: #8fcbb0;
  --status-warning-bg: #3a3018;
  --status-warning-fg: #f0c56b;
  --status-danger-bg: #3a221c;
  --status-danger-fg: #f0a58a;
  --status-caution-bg: #3a2c18;
  --status-caution-fg: #e0a84a;
  --status-info-bg: #1e332c;
  --status-info-fg: #cde8d8;
  --danger: #f0a58a;

  --like-active-bg: #3a221c;
  --like-active-fg: #f0a58a;

  --shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.35);
  --shadow-lift: 0 16px 40px rgba(0, 0, 0, 0.48);
  --shadow-banner: 0 -6px 24px rgba(0, 0, 0, 0.4);

  --focus-ring: 0 0 0 3px var(--accent);
  --focus-ring-inverse: 0 0 0 3px var(--surface-canvas);

  --badge-fill: #1c2822;
  --badge-rim: #8fcbb0;
  --badge-metal: #e0a84a;
}

/* =========================================================================
   System pack — stamp data-theme="system" and let CSS follow the OS. Light
   OS uses the :root Nature values above (no extra block needed). Dark OS
   copies the Dusk pack in FULL — every non-layout token, including
   --shadow-banner and --focus-ring-inverse (a common mistake the spec
   explicitly calls out is a *partial* copy here). Never resolve
   system -> nature/dusk in JS; that would miss live OS theme changes.
   ========================================================================= */
@media (prefers-color-scheme: dark) {
  html[data-theme="system"] {
    color-scheme: dark;

    --surface-canvas: #121a16;
    --surface-canvas-subtle: #1a2420;
    --surface-raised: #1c2822;
    --surface-sunken: #0e1512;
    --surface-nav: rgba(18, 26, 22, 0.92);
    --surface-nav-solid: #15201b;
    --surface-overlay: rgba(0, 0, 0, 0.62);
    --surface-inverse: #d8f3dc;
    --surface-wash-sage: rgba(143, 203, 176, 0.12);
    --surface-wash-sun: rgba(224, 168, 74, 0.16);

    --text-primary: #e8ede8;
    --text-secondary: #b4c2ba;
    --text-muted: #8a9a90;
    --text-inverse: #121a16;
    --text-link: #8fcbb0;
    --text-on-accent: #121a16;
    --text-on-sun: #121a16;
    --text-danger: #f0a58a;

    --accent-strong: #cde8d8;
    --accent: #8fcbb0;
    --accent-soft: #5fa88a;
    --accent-muted: #4e8f74;
    --accent-wash: #244036;
    --sun: #e0a84a;
    --sun-strong: #f0c56b;
    --sun-wash: rgba(224, 168, 74, 0.18);
    --bark: #cbb8a4;

    --decor-leaf: #4e8f74;
    --decor-sun: #e0a84a;
    --decor-leaf-opacity: 0.22;
    --decor-sun-opacity: 0.16;

    --border-subtle: rgba(143, 203, 176, 0.14);
    --border-default: #2a3a32;
    --border-strong: #3a4e44;
    --border-focus: #8fcbb0;
    --border-accent: rgba(143, 203, 176, 0.35);
    --accent-wash-hover: rgba(143, 203, 176, 0.10);

    --status-success-bg: #244036;
    --status-success-fg: #8fcbb0;
    --status-warning-bg: #3a3018;
    --status-warning-fg: #f0c56b;
    --status-danger-bg: #3a221c;
    --status-danger-fg: #f0a58a;
    --status-caution-bg: #3a2c18;
    --status-caution-fg: #e0a84a;
    --status-info-bg: #1e332c;
    --status-info-fg: #cde8d8;
    --danger: #f0a58a;

    --like-active-bg: #3a221c;
    --like-active-fg: #f0a58a;

    --shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.35);
    --shadow-lift: 0 16px 40px rgba(0, 0, 0, 0.48);
    --shadow-banner: 0 -6px 24px rgba(0, 0, 0, 0.4);

    --focus-ring: 0 0 0 3px var(--accent);
    --focus-ring-inverse: 0 0 0 3px var(--surface-canvas);

    --badge-fill: #1c2822;
    --badge-rim: #8fcbb0;
    --badge-metal: #e0a84a;
  }
}

/* Dusk / system-dark: invert OSM tiles so the map is not a light island on
   an otherwise dark page. The dusk selector applies unconditionally (a user
   can pick Dusk manually even while their OS itself is in light mode); it
   is combined with the system selector inside the media query below rather
   than re-declaring a third independent copy of the same rule for system
   alone. */
html[data-theme="dusk"] .leaflet-tile-pane {
  filter: invert(1) hue-rotate(180deg) brightness(0.95) contrast(0.9);
}
html[data-theme="dusk"] .leaflet-control-attribution {
  background: rgba(28, 40, 34, 0.85);
  color: var(--text-secondary);
}
html[data-theme="dusk"] .leaflet-control-attribution a {
  color: var(--text-link);
}

@media (prefers-color-scheme: dark) {
  html[data-theme="dusk"] .leaflet-tile-pane,
  html[data-theme="system"] .leaflet-tile-pane {
    filter: invert(1) hue-rotate(180deg) brightness(0.95) contrast(0.9);
  }
  html[data-theme="dusk"] .leaflet-control-attribution,
  html[data-theme="system"] .leaflet-control-attribution {
    background: rgba(28, 40, 34, 0.85);
    color: var(--text-secondary);
  }
  html[data-theme="dusk"] .leaflet-control-attribution a,
  html[data-theme="system"] .leaflet-control-attribution a {
    color: var(--text-link);
  }
}

#theme-toggle-btn {
  cursor: pointer;
  padding: 0;
  font: inherit;
  appearance: none;
  -webkit-appearance: none;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  color: var(--ink-900);
  background:
    radial-gradient(circle at 100% 0%, var(--sage-200) 0%, transparent 45%),
    radial-gradient(circle at 0% 100%, var(--sun-wash) 0%, transparent 40%),
    var(--paper-50);
  min-height: 100vh;
  position: relative;
  overflow-x: hidden;
}

h1, h2, h3, .brand {
  font-family: 'Fraunces', Georgia, serif;
}

a { color: var(--forest-700); }

/* Decorative leaves */
.leaf-decor {
  position: fixed;
  width: 260px;
  height: 260px;
  color: var(--sage-400);
  pointer-events: none;
  z-index: 0;
}
.leaf-decor-tl { top: -60px; left: -70px; transform: rotate(-25deg); opacity: 0.35; }
.leaf-decor-br { bottom: -70px; right: -80px; transform: rotate(160deg); color: var(--sun-400); opacity: 0.25; }

/* Nav */
.topnav {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: calc(var(--space-4) + env(safe-area-inset-top)) var(--space-8) var(--space-3);
  background: var(--surface-nav);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border-subtle);
  /* Sticky already establishes a containing block; keep overflow visible
     so .toolbar-capture-btn can hang past this box into .page. hidden
     (or overflow-x:hidden, which computes overflow-y to auto) would
     clip the portion of the circle that sits below the seam. */
  overflow: visible;
}
.topnav-top {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  position: relative;
  width: 100%;
}
/* 2026-10-03 (explicit request), corrected 2026-10-04: the logo stays
   centered only while Capture mode's fullscreen overlay is actually
   active (body.capture-mode-active) - a genuinely "full screen" state
   with no visible header/logo at all, so this rule is close to moot
   there anyway. Scoping this to the whole /capture page (as the first
   version did, via body:has(> .page.capture-page)) was too broad: the
   page's own normal Discovery-mode view (before the shutter is pressed)
   has a fully visible header and should left-align/show the full logo
   exactly like every other page, which the original scoping incorrectly
   excluded. */
body.capture-mode-active .topnav-top { justify-content: center; }
/* Real bug found live (2026-10-11, direct owner report: the profile
   dropdown rendered BEHIND the raised Capture button): the previous
   position:absolute + transform:translateY(-50%) centering trick put a
   `transform` on .topnav-right itself, and ANY transform - regardless of
   position value - creates a new CSS stacking context. That silently
   trapped .profile-menu-list's z-index:50 inside .topnav-right's own
   local stacking order instead of letting it compete directly with
   .toolbar-capture-btn's z-index:2 inside .topnav's context, so
   .topnav-right (an implicit z-index:0 as a whole) painted BELOW the
   button despite the dropdown's own much higher declared z-index -
   confirmed via a real headless-browser stacking-context trace before
   writing this fix, and reproduced with literal getComputedStyle output
   showing the transform as the one differing property. Plain flex
   centering (margin-left:auto on this already-flex, already
   align-items:center .topnav-top row) achieves the identical visual
   result with no transform and therefore no stacking context of its
   own - .profile-menu-list now correctly outranks the Capture button
   wherever they overlap. Verified both ways with a real elementsFromPoint
   stack dump, not just visually. */
.topnav-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 12px;
}
.corn-balance {
  display: flex;
  align-items: center;
  gap: 4px;
  font-weight: 600;
  color: var(--forest-900);
  font-size: 0.95rem;
}
.profile-avatar-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  padding: 0;
  font: inherit;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  background: var(--paper-100);
  border: 1px solid var(--border-accent);
  color: inherit;
  text-decoration: none;
  transition: border-color .15s;
}
.profile-avatar-link:hover,
.profile-avatar-link[aria-expanded="true"] { border-color: var(--sun-500); }
.profile-avatar-link.active { border-color: var(--forest-900); border-width: 2px; }
.profile-avatar-img { width: 100%; height: 100%; object-fit: cover; }
.profile-avatar-fallback { font-size: 1.2rem; line-height: 1; }

/* Admin header control - a compact glyph next to the corn counter, not a
   full toolbar slot. Distinct from .profile-avatar-link so it does not
   read as a second avatar. */
.header-icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  background: none;
  border: none;
  text-decoration: none;
}
.header-icon-btn:hover,
.header-icon-btn.active { color: var(--accent-strong); background: var(--accent-wash-hover); }

/* More button - the last toolbar slot at every width (issue #237; was a
   header hamburger shown only on mobile). Button reset so it matches the
   <a> toolbar items. More-sheet entries for toolbar items that are
   visible at the current width are hidden (tier 3 above 640px, tier 2
   above 520px) - same breakpoints as .toolbar-item-tierN. */
.toolbar-item-more {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  font: inherit;
  cursor: pointer;
}
.toolbar-item-more[aria-expanded="true"] { background: var(--accent-wash-hover); }
@media (min-width: 641px) {
  .more-sheet-item.more-sheet-item-tier3 { display: none; }
}
@media (min-width: 521px) {
  .more-sheet-item.more-sheet-item-tier2 { display: none; }
}
@media (max-width: 520px) {
  .toolbar-item.toolbar-item-tier2 { display: none; }
}

/* Profile menu - same anchored-dropdown pattern as .lang-menu. */
.profile-menu { position: relative; }
.profile-menu-list {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 200px;
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lift);
  padding: 4px;
  z-index: 50;
  display: flex;
  flex-direction: column;
}
.profile-menu-list[hidden] { display: none; }
.profile-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  padding: 8px 10px;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font: inherit;
  color: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.profile-menu-item:hover,
.profile-menu-item:focus-visible { background: var(--surface-sunken); }
.profile-menu-item-danger { color: var(--danger); }
.profile-menu-item-danger:hover { background: var(--status-danger-bg); }
.profile-menu .logout-form { display: block; width: 100%; }

/* Language switcher (site-redesign PR 7) - a small menu anchored under the
   header's globe button, same header-utility slot as the theme switcher. */
.lang-switcher { position: relative; }
.lang-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 160px;
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lift);
  padding: 4px;
  z-index: 50;
  display: flex;
  flex-direction: column;
}
/* Author CSS always beats the UA stylesheet's [hidden]{display:none},
   regardless of specificity - without this override, .lang-menu's own
   `display: flex` above permanently defeats both the hidden attribute in
   base.html's markup and prefs.js's `menu.hidden = true/false` toggling,
   leaving the menu visible at all times. */
.lang-menu[hidden] { display: none; }
.lang-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.lang-menu-item:hover, .lang-menu-item:focus-visible { background: var(--surface-sunken); }
.lang-menu-item .lang-check { opacity: 0; width: 16px; height: 16px; flex-shrink: 0; }
.lang-menu-item.active .lang-check { opacity: 1; }
.brand {
  font-weight: 600;
  font-size: 1.3rem;
  color: var(--forest-900);
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 8px;
}
.brand-mark { font-size: 1.4rem; }
.brand-mark.large { font-size: 3rem; }
.brand-logo { height: 1.6rem; width: auto; display: inline-block; vertical-align: middle; }
.brand-logo.brand-logo-large { height: 3.4rem; }
/* Inlined sprite icons (backend/app/templates/svg/icons.svg via <use>) -
   stroke="currentColor" so they inherit the surrounding text color; size
   is em-based so an icon inside a larger/smaller-font-size ancestor
   (nav-icon, profile-avatar-fallback, etc.) scales with it automatically. */
.ui-icon { width: 1.25em; height: 1.25em; vertical-align: middle; }
.logout-form { display: inline; margin: 0; }
.link-btn {
  background: none;
  border: none;
  font: inherit;
  color: var(--ink-600);
  cursor: pointer;
  padding: 6px 4px;
}
.link-btn:hover { color: var(--accent); }
.nav-icon { font-size: 1.35rem; line-height: 1; }

/* Top toolbar (issue #126): one row for every viewport, icon+label items
   with the Capture shutter overlaying the seam between this bar and the
   page below (issue #132). Secondary items (Achievements, Leaderboard)
   stay in this row on desktop and hide under 640px, where the More sheet
   covers them. overflow stays visible on both .topnav and .toolbar -
   hidden would clip the hanging circle. */
.toolbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: start;
  gap: var(--space-5);
  width: 100%;
  padding-top: var(--space-3);
  padding-bottom: var(--space-2);
  overflow: visible;
}
.toolbar-cluster {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: var(--space-5);
  min-width: 0;
}
.toolbar-cluster:first-child { justify-content: flex-end; }
.toolbar-cluster:last-child { justify-content: flex-start; }
.toolbar-item {
  text-decoration: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  border-bottom: 2px solid transparent;
  transition: background var(--ease), border-color var(--ease);
  color: inherit;
}
.toolbar-item:hover, .toolbar-item.active {
  background: var(--accent-wash-hover);
  border-bottom-color: var(--sun-strong);
}
.toolbar-icon { font-size: 1.35rem; line-height: 1; }
.toolbar-label {
  font-size: 0.62rem;
  font-weight: 600;
  line-height: 1;
  color: var(--ink-600);
  letter-spacing: 0.01em;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.toolbar-item:hover .toolbar-label,
.toolbar-item.active .toolbar-label { color: var(--forest-900); }
/* Capture: the primary action, camera-shutter style. 5.5rem (88px) is
   ~1.4x the original 64px circle. The <a> stays in normal flow as the
   center toolbar slot (spacer so neighbors don't collide with the
   overlay). The inner .toolbar-capture-btn is position:absolute against
   .topnav (sticky already makes a containing block) — NOT against this
   item — so the circle is free of the row's box model and can straddle
   the .topnav / .page seam: 75% of its height on the bar, 25% hanging
   into the page. Do not position .toolbar-item-capture itself; a
   positioning context here would steal the button from .topnav and pin
   it back to the row. Icon-only (aria-label carries the name).
   toolbar_spec keeps 3 items on each side. z-index on the button puts
   the overlapping circle above sibling toolbar items for hit-testing
   and above .page (z-index 1) via parent .topnav's --z-nav (5).
   Token-only fill/ring/shadow so Nature and Dusk both restyle it. */
.toolbar-item-capture {
  justify-content: flex-start;
  border-bottom-color: transparent;
  padding-top: 0;
  min-width: 5.5rem;
  min-height: 44px;
  overflow: visible;
}
.toolbar-item-capture:hover,
.toolbar-item-capture.active {
  background: none;
  border-bottom-color: transparent;
}
.toolbar-item-capture:focus-visible {
  box-shadow: none;
}
.toolbar-capture-btn {
  --capture-btn-size: 5.5rem;
  /* Fraction of the diameter that hangs below .topnav's padding edge
     into .page. 0 (2026-10-11, direct owner request: "lift Capture
     button to same level as other toolbar") - the circle now sits
     fully flush with the toolbar row, bottom edge even with its
     neighbors, instead of dipping into .page below the seam. Previously
     0.25 (75% on the bar / 25% over the page), before that 0.36/0.44 -
     each a step further toward this same level. */
  --capture-btn-hang: 0;
  position: absolute;
  left: 50%;
  bottom: calc(var(--capture-btn-size) * var(--capture-btn-hang) * -1);
  transform: translateX(-50%);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--capture-btn-size);
  height: var(--capture-btn-size);
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--accent);
  color: var(--text-on-accent);
  border: 3px solid var(--surface-nav-solid);
  box-shadow:
    var(--shadow-lift),
    0 0 0 2px var(--sun);
  transition: background var(--ease), box-shadow var(--ease), transform var(--ease-lift);
}
.toolbar-capture-btn::before {
  content: "";
  position: absolute;
  inset: 0.5rem;
  border-radius: 50%;
  border: 1.5px solid var(--sun);
  border-color: color-mix(in srgb, var(--sun) 70%, var(--text-on-accent));
  pointer-events: none;
}
.toolbar-capture-btn::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: radial-gradient(
    circle at 32% 24%,
    color-mix(in srgb, var(--sun) 40%, transparent) 0%,
    transparent 48%
  );
  pointer-events: none;
}
.toolbar-capture-btn .ui-icon {
  width: 2.5rem;
  height: 2.5rem;
  position: relative;
  z-index: 1;
}
.toolbar-item-capture.active .toolbar-capture-btn,
.toolbar-item-capture:hover .toolbar-capture-btn {
  background: var(--accent-strong);
  transform: translateX(-50%) scale(1.04);
  box-shadow:
    var(--shadow-lift),
    0 0 0 2px var(--sun-strong);
}
.toolbar-item-capture:active .toolbar-capture-btn {
  transform: translateX(-50%) scale(0.97);
}
.toolbar-item-capture:focus-visible .toolbar-capture-btn {
  box-shadow:
    var(--shadow-lift),
    0 0 0 2px var(--sun-strong),
    var(--focus-ring);
}

/* Logged-out header - shown instead of the full icon-nav above when there's
   no session. Just the brand plus a compact login/signup icon (reusing
   .profile-avatar-link's own sizing) so the signup funnel isn't dead once
   / redirects everyone (including guests) to /feed, without the wide
   button pair this used to be, which overflowed and overlapped the brand
   at mobile widths. */

.page {
  position: relative;
  z-index: 1;
  max-width: 880px;
  margin: 0 auto;
  /* 48px of top padding predates the Capture overlay having zero hang
     (2026-10-11, --capture-btn-hang: 0 above) - it no longer needs to
     clear any protrusion into .page, but the small gap still reads fine
     above the first heading, so it's left as-is rather than touched for
     a site-wide, purely cosmetic nicety. */
  padding: 48px 24px 80px;
}

/* Auth pages */
.auth-page { max-width: 460px; padding-top: 48px; }
.auth-card {
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  padding: 40px 36px;
}
.brand-hero { text-align: center; margin-bottom: 28px; }
.brand-hero h1 { margin: 8px 0 2px; color: var(--forest-900); font-size: 2rem; }
.tagline { color: var(--ink-600); margin: 0; }
.auth-card h2 { margin-top: 0; color: var(--forest-900); }
.signup-sent { text-align: center; padding: 8px 0 0; }
.signup-sent h2 { margin: 10px 0 12px; color: var(--forest-900); }
.signup-sent p { margin: 0 0 10px; color: var(--ink-600); }
.signup-sent-hint { font-size: 0.85rem; }

/* .profile-card is included alongside `form` here because profile.html's
   Account card (site-redesign PR 9) has standalone theme/language
   <select>s that act immediately on change with no surrounding <form> -
   its Gemini-key input/checkbox likewise use the HTML `form=` attribute
   to join #profile-form in the Public-profile card above rather than
   being physically nested in a <form>, so neither would pick up any
   `form ...`-scoped styling without this. */
form label, .profile-card label, .modal-card label {
  display: block;
  font-weight: 500;
  margin-bottom: 16px;
  color: var(--ink-600);
  font-size: 0.9rem;
}
form input, form textarea, .profile-card input, .profile-card textarea, .profile-card select, .modal-card input {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 12px 14px;
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: inherit;
  background: var(--paper-100);
  transition: border-color .15s, background .15s;
}
form textarea, .profile-card textarea { resize: vertical; }
form input:focus, form textarea:focus, .profile-card input:focus, .profile-card textarea:focus, .profile-card select:focus, .modal-card input:focus {
  border-color: var(--sage-400);
  background: var(--surface-raised);
}
input:focus-visible, textarea:focus-visible, button:focus-visible, a:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}
.password-field { position: relative; }
.password-field input { padding-right: 56px; }
.password-toggle-btn {
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1;
  padding: 6px 8px;
  color: var(--ink-600);
}
.password-toggle-btn:hover { color: var(--forest-700); }

.btn-primary, .btn-secondary {
  display: inline-block;
  width: auto;
  padding: 13px 20px;
  border-radius: var(--radius-sm);
  border: none;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
  transition: transform .1s, box-shadow .15s, opacity .15s;
}
.btn-block { width: 100%; }
.btn-primary {
  background: linear-gradient(135deg, var(--forest-500), var(--forest-700));
  color: var(--text-on-accent);
  box-shadow: 0 8px 20px rgba(45, 106, 79, 0.3);
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 10px 24px rgba(45, 106, 79, 0.38); }
.btn-secondary {
  background: var(--sun-400);
  /* --text-on-sun is the token meant for type on a --sun fill; it equals
     --forest-900/--accent-strong in Nature (no visual change there), but
     Dusk inverts --accent-strong while keeping --sun light, which made
     this button light-text-on-amber (1.63:1) before this fix (design
     review #167/#168). Fixed at this shared rule, not per-page, since
     every .btn-secondary (capture, etc.) had the same Dusk failure. */
  color: var(--text-on-sun);
  margin-top: 12px;
}
.btn-secondary:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-secondary:not(:disabled):hover { transform: translateY(-1px); }

/* Ghost button: a lower-emphasis action that isn't a link (e.g. "Use my
   location", profile's Log out) - bordered/transparent rather than filled,
   distinct from both the filled .btn-primary/.btn-secondary pair and the
   no-chrome .link-btn. Token-only so Dusk redefines it for free. */
.btn-ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: auto;
  padding: 11px 18px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-default);
  background: transparent;
  color: var(--text-primary);
  font-size: 0.95rem;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  transition: background .15s, border-color .15s, transform .1s;
}
.btn-ghost:hover { background: var(--accent-wash-hover); border-color: var(--border-accent); }
.btn-ghost:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-ghost .ui-icon { width: 1.1em; height: 1.1em; }

.switch-auth { text-align: center; margin-top: 22px; color: var(--ink-600); font-size: 0.92rem; }

.banner {
  padding: 12px 16px;
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  margin-bottom: 18px;
}
.banner-success { background: var(--sage-200); color: var(--forest-900); }
.banner-error { background: var(--status-danger-bg); color: var(--danger); }
.banner-warning { background: var(--status-warning-bg); color: var(--ink-900); }

/* Unified species-info card (issue #218) - edibility (issue #55) and the
   four "about this species" fields (issue #158) are five fields of one
   species, now one card/one label style/one spacing rhythm instead of two
   differently-colored components. Edibility is marked only by the warning
   icon in its title - its yellow row background was removed per issue
   #222, so every row shares the card's own background. */
.species-info-wrap {
  margin: var(--space-3) 0;
  padding: var(--space-4);
  background: var(--surface-wash-sage);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}
.species-info-heading {
  margin: 0 0 var(--space-3);
  font-size: 1.05rem;
  color: var(--text-primary);
}
.species-info-list {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.species-info-item dt {
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.species-info-item dd {
  margin: var(--space-1) 0 0;
  line-height: var(--line-body);
  color: var(--text-primary);
}
.species-info-value { display: block; }
.species-info-item--edibility dt {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.species-info-item--edibility dt .ui-icon {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  color: var(--status-caution-fg);
}
.edibility-disclaimer,
.species-info-disclaimer {
  margin: var(--space-3) 0 0;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--text-secondary);
}
.species-info-item--edibility .edibility-disclaimer {
  margin: var(--space-2) 0 0;
}
.edibility-disclaimer a,
.species-info-disclaimer a {
  color: inherit;
  text-decoration: underline;
}

/* Capture page */
.hero-capture { text-align: center; margin-bottom: 28px; }
.hero-capture h1 { color: var(--forest-900); margin-bottom: 6px; font-size: 2rem; }
.challenges-header {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
}
.challenges-header h1 { margin-bottom: 0; }
.challenges-header .btn-secondary { margin-top: 0; }

.capture-card {
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  padding: 28px;
  /* Same 700px cap as .feed-card / .feed-stats-grid (issue #139). width:
     100% so the card actually fills that cap rather than shrinking to
     its content; margin auto still centers it inside .page. */
  width: 100%;
  max-width: 700px;
  margin: 0 auto 40px;
  text-align: center;
}
.preview-wrap {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--paper-100);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 18px;
  border: 2px dashed #dcd4c2;
}
.preview-wrap img { width: 100%; height: 100%; object-fit: cover; }
.capture-idle-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 0.85rem;
  color: var(--ink-600);
  text-align: center;
  width: 100%;
  height: 100%;
}
.capture-idle-placeholder .ui-icon { width: 2.25em; height: 2.25em; }
.capture-actions { display: flex; flex-direction: column; gap: 4px; }
/* PR 3 (site redesign) made .btn-primary/.btn-secondary auto-width by
   default - a real regression here specifically, since this container's
   flex-direction: column relies on the default align-items: stretch to
   keep the Submit action full-bleed width; an explicit width: auto opts
   a flex item out of stretch. Restore the full-width look here. */
.capture-actions .btn-primary, .capture-actions .btn-secondary { width: 100%; }
/* Submit reuses the existing .btn-secondary (the app's established
   yellow accent) - margin-top zeroed since .capture-actions already
   provides spacing via its own flex gap. After a shot the square
   preview can push this below the fold (the card is now feed-width);
   stick the button itself so it's always reachable. Hidden via
   display:none until a photo exists, so this does not leave an empty
   sticky bar on the idle page. */
.submit-review-btn {
  margin-top: 0;
  position: sticky;
  bottom: max(8px, env(safe-area-inset-bottom));
  z-index: 4;
}
/* 2026-10-02 (owner feedback): lives beside the close button in
   .capture-mode-top-bar now, not stacked above the photo as its own
   block - left-aligned, flexing to fill the remaining row width.
   --text-danger (red) per explicit request, kept as a semantic token
   so Nature/Dusk both restyle it rather than a hardcoded hex. */
.capture-multi-hint {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 0.72rem;
  line-height: 1.3;
  color: var(--text-danger);
  text-align: left;
}
.capture-status {
  margin: 0 0 10px;
  font-size: 0.85rem;
  color: var(--ink-600);
}
.capture-status[hidden] { display: none; }

/* Capture-page chrome: the native camera app is outside our control
   (issue #57), so "fullscreen / orientation-locked" can only apply to
   this page around each round-trip. Hide footer/leaves to give the
   gallery more of the viewport; lock is requested in JS when the
   platform allows it (typically Android Chrome / installed PWA). */
body:has(> .page.capture-page) .site-footer { display: none; }
body:has(> .page.capture-page) .leaf-decor { display: none; }
html:fullscreen .site-footer,
html:fullscreen .leaf-decor { display: none; }
/* !important is load-bearing here, not a shortcut: cookie-banner.js sets
   `banner.style.display = "flex"` as an INLINE style on a first visit, and an
   inline style beats a CSS rule of any specificity unless the rule is
   !important. Without it this declaration is simply inert, which is how the
   banner came to cover the whole Capture-mode bottom bar (see
   body.capture-mode-active .cookie-banner below for the same fix and the
   full story). */
html:fullscreen .cookie-banner { display: none !important; }

/* Capture mode (2026-09-29): a fullscreen, fixed, non-scrolling overlay
   that #taking-phase is repositioned into whenever <body> carries this
   class (toggled by capture-mode.js). CSS-level lock independent of the
   best-effort screen.orientation.lock() call in capture-burst.js's
   requestCaptureImmersive() - this fixed layout never visually reflows
   into landscape even on iPhone Safari, which has no reliable
   orientation-lock API at all. Hides the rest of the page's own chrome
   (topnav, hero, queue section) rather than just the footer/leaves. */
body.capture-mode-active { overflow: hidden; }
body.capture-mode-active .topnav,
body.capture-mode-active .hero-capture,
body.capture-mode-active .queue-section,
body.capture-mode-active .location-status,
body.capture-mode-active .capture-idle-actions { display: none; }

/* Critical, found by the final whole-branch review: on a first-ever visit the
   cookie-consent banner made the ENTIRE Capture-mode bottom bar (shutter AND
   Submit) untappable - the app's core action was dead for every new user.
   Two compounding defects, fixed by the two rules here:

   1. !important. cookie-banner.js sets `banner.style.display = "flex"` as an
      INLINE style, which beats a CSS rule of any specificity without
      !important - so the plain `display: none` this used to carry was simply
      inert. This is the narrow, legitimate use of !important (beating a
      JS-set inline style), not a general escape hatch.
   2. .page's z-index. #taking-phase below sets z-index: var(--z-modal) (20),
      but it lives inside <main class="page">, which is ITSELF a stacking
      context (position: relative; z-index: 1) - so that 20 only ever ranked
      against .page's own descendants. #cookie-banner is a SIBLING of <main>
      at var(--z-banner) (10), so it painted above the whole .page subtree
      regardless. Dropping .page to z-index: auto in Capture mode (it stays
      position: relative, so nothing else about its layout changes) makes
      --z-modal mean what its name says, and stops any future body-level
      fixed element from reintroducing the same class of bug. */
body.capture-mode-active .cookie-banner { display: none !important; }
body.capture-mode-active .page { z-index: auto; }

body.capture-mode-active .capture-card {
  max-width: none;
  padding: 0;
  box-shadow: none;
  background: none;
  margin: 0;
}
body.capture-mode-active #taking-phase {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: var(--surface-canvas);
  display: flex;
  flex-direction: column;
  padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
}
/* 2026-10-02 (owner feedback): close button + hint now share one row
   (.capture-mode-top-bar) instead of each being its own stacked block -
   saves vertical space for a taller photo pane. The button no longer
   sets its own display:flex/none directly; the row wrapper does that
   instead (so the hint text's layout isn't orphaned when the button is
   hidden in Discovery mode - #taking-phase itself is already
   display:none there regardless, but keeping the toggle on one element
   is simpler than two). */
.capture-mode-top-bar {
  display: none;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
body.capture-mode-active .capture-mode-top-bar { display: flex; }
.capture-mode-close-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: var(--surface-raised);
  color: var(--text-primary);
  box-shadow: var(--shadow-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}
body.capture-mode-active .preview-wrap {
  flex: 1 1 auto;
  min-height: 0;
  margin-bottom: 12px;
}
body.capture-mode-active .thumb-strip { flex: 0 0 auto; }

/* Important, found by the final whole-branch review: #capture-error is a
   SIBLING of #taking-phase inside .capture-card, not a child of it - so once
   #taking-phase becomes `position: fixed; inset: 0` with an opaque
   background, the banner was painted behind it and could not be seen at all.
   That silenced all four of this feature's Capture-mode messages, including
   capture.could_not_save_photo, the spec's designated feedback for a failed
   durable write. (is_visible() reported True throughout, because it checks
   the CSS box and not what is drawn on top - which is why the suite missed
   it. The rule here used to be `flex: 0 0 auto`, written as if the element
   were a flex child of the Capture-mode column; it never was.)

   Lifted into its own fixed overlay ABOVE #taking-phase rather than moved
   into it in the markup, because the element has to work in BOTH modes:
   enterCaptureMode() only runs after the first photo's durable write
   SUCCEEDS, so a first-photo failure (image_too_large / no_location_yet /
   could_not_save_photo) is reported while #taking-phase is still
   display:none - making it a child would have silenced exactly those.
   Offset 52px clears the 40px close button plus its 12px margin. */
body.capture-mode-active #capture-error {
  position: fixed;
  top: calc(max(16px, env(safe-area-inset-top)) + 52px);
  left: 16px;
  right: 16px;
  z-index: calc(var(--z-modal) + 1);
  margin: 0;
}

/* Bottom, thumb-reachable control column (2026-10-02, owner feedback on
   the shipped layout): the in-page shutter (opens the native camera
   again, delegating to the same openCaptureCamera() the top-toolbar
   shutter uses), centered and enlarged, stacked directly ABOVE Submit -
   not side-by-side as originally shipped. Fixed to the very bottom of
   the locked layout, never scrolled away. Token-only chrome so Nature
   and Dusk both restyle it, matching .toolbar-capture-btn's established
   visual language for the shutter glyph. align-items: stretch (the
   flex default) is what keeps Submit full-width here - the shutter
   opts out via its own align-self: center, since it has an explicit
   fixed width/height. */
.capture-mode-bottom-bar {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: auto;
  padding-top: 8px;
}
.capture-mode-shutter-btn {
  width: 6rem;
  height: 6rem;
  align-self: center;
  flex-shrink: 0;
  border-radius: 50%;
  border: 3px solid var(--surface-nav-solid);
  background: var(--accent);
  color: var(--text-on-accent);
  box-shadow: var(--shadow-lift), 0 0 0 2px var(--sun);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform var(--ease-lift), box-shadow var(--ease);
}
.capture-mode-shutter-btn:hover { transform: scale(1.04); }
.capture-mode-shutter-btn:active { transform: scale(0.97); }
.capture-mode-shutter-btn .ui-icon { width: 2.75rem; height: 2.75rem; }
.capture-mode-bottom-bar .submit-review-btn {
  position: static;
  margin: 0;
}

/* Discovery mode's entry point back into an already-started capture
   session (2026-09-29) - shown only when a draft exists and Capture mode
   isn't active (see capture-mode.js's renderDiscoveryEntryState()). */
.resume-capture-card {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 14px 16px;
  margin-bottom: 18px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-default);
  background: var(--surface-raised);
  color: var(--text-primary);
  font-weight: 600;
  cursor: pointer;
  text-align: left;
}
.resume-capture-card[hidden] { display: none; }
.resume-capture-thumb-wrap { flex-shrink: 0; width: 44px; height: 44px; border-radius: var(--radius-sm); overflow: hidden; }
.resume-capture-thumb-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Idle capture-card "Upload photos" (issue #227, click-wired same day,
   photo-library upload): a secondary circular action meant to sit next
   to the page's round camera shutter. Smaller than .toolbar-capture-btn
   (5.5rem) / .capture-mode-shutter-btn (6rem) so it doesn't compete;
   token-only chrome so Nature and Dusk restyle it. */
.capture-idle-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  margin: var(--space-4) 0 0;
}
.upload-photos-btn {
  width: 3.25rem;
  height: 3.25rem;
  flex-shrink: 0;
  border-radius: 50%;
  border: 2px solid var(--border-accent);
  background: var(--surface-raised);
  color: var(--accent);
  box-shadow: var(--shadow-soft);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform var(--ease-lift), box-shadow var(--ease), background var(--ease);
}
.upload-photos-btn:hover {
  transform: scale(1.04);
  background: var(--accent-wash);
}
.upload-photos-btn:active { transform: scale(0.97); }
.upload-photos-btn .ui-icon { width: 1.55rem; height: 1.55rem; }

/* Submit "sending" animation (2026-09-29): a brief filling shimmer in
   the same --sun/--sun-strong palette this button already uses, so the
   tap visibly registers even though the actual submission continues in
   the background after the mode switches away (see
   capture-burst.js's submit handler / capture.js's submitDraft()).
   Static filled state for prefers-reduced-motion, per this project's
   established a11y posture elsewhere in the redesign work. */
@keyframes submit-sending-sweep {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.submit-review-btn.is-sending {
  background: linear-gradient(90deg, var(--sun-strong), var(--sun), var(--sun-strong));
  background-size: 200% 100%;
  animation: submit-sending-sweep 0.5s linear;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .submit-review-btn.is-sending {
    animation: none;
    background: var(--sun-strong);
  }
}

/* If orientation lock is refused (iPhone Safari, desktop), keep the
   review UI usable in landscape by shrinking the square preview so
   Submit stays on-screen. */
@media screen and (orientation: landscape) and (max-height: 500px) {
  .capture-page .hero-capture { margin-bottom: 10px; }
  .capture-page .hero-capture .tagline { display: none; }
  .capture-page .preview-wrap {
    aspect-ratio: auto;
    height: 42vh;
    max-height: 42vh;
  }
}
.location-status { font-size: 0.85rem; color: var(--ink-600); margin: 14px 0 0; }
.thumb-strip {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding-bottom: 6px;
  margin-bottom: 14px;
}
/* Wraps each gallery thumbnail so the delete (x) button can be pinned to
   its top-right corner without disturbing .thumb-item's own sizing. */
.thumb-item-wrap { position: relative; flex: 0 0 auto; }
.thumb-item {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 2px solid transparent;
  flex: 0 0 auto;
  cursor: pointer;
  display: block;
}
.thumb-item-selected { border-color: var(--forest-500); }
.thumb-delete-btn {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: none;
  background: var(--danger);
  color: var(--text-on-accent);
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}
.thumb-delete-btn:hover { background: #a3380b; }

.queue-section { max-width: 700px; margin: 0 auto; }
.queue-section h2 { color: var(--forest-900); font-size: 1.3rem; }
.section-heading {
  max-width: 900px;
  margin: 32px auto 16px;
  color: var(--forest-900);
  font-size: 1.3rem;
}
.section-heading:first-of-type { margin-top: 0; }

.feed-stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px;
  margin-bottom: 22px;
}
/* Persona page renders this grid as a real <ul> (each card a plain <li>)
   so a screen reader announces "list, 4 items" instead of a bare run of
   numbers and labels - reset the list styling the element inherits from
   being a <ul> (design review #167/#168). feed.html's own stats grid
   stays a <div> and is unaffected (ID-scoped, not on .feed-stats-grid
   itself). Repeats margin-bottom's existing 22px since margin here is a
   shorthand that would otherwise reset it to the default 1em. */
#persona-stats { list-style: none; margin: 0 auto 22px; padding: 0; }
.feed-stat-card {
  background: linear-gradient(160deg, var(--accent-wash) 0%, var(--surface-raised) 70%);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  padding: 16px 10px 14px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
}
.feed-stat-icon {
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  line-height: 1;
}
.feed-stat-icon .ui-icon { width: 1.35em; height: 1.35em; }
.feed-stat-value {
  font-size: 1.55rem;
  font-weight: 800;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.feed-stat-label {
  font-size: 0.66rem;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  line-height: 1.2;
}
.discoveries-list { display: flex; flex-direction: column; gap: 12px; }
.discovery-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 14px;
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  padding: 12px 16px;
  box-shadow: var(--shadow-soft);
}
.discovery-row img { width: 56px; height: 56px; border-radius: 10px; object-fit: cover; flex-shrink: 0; background: var(--paper-100); }
/* Global Discovery Search: a quick-glance discoverer identifier overlaid
   on the thumbnail's own top-left corner (user feedback: "every discovery
   should have a user icon... about who did it") - purely decorative
   (aria-hidden), the "by {name}" text below already carries the real,
   accessible attribution. Scoped under .discovery-row-photo-wrap (not
   .discovery-row directly) so capture.js's/admin_discoveries.js's own
   unwrapped .discovery-row usages (always the current user / an admin
   table with its own user column) are unaffected. Two chained classes,
   not one, so this beats .discovery-row img's own 56px/10px-radius rule
   on specificity (0,2,0 > 0,1,1) rather than fighting it via !important. */
.discovery-row-photo-wrap { position: relative; flex-shrink: 0; }
.discovery-row-owner-badge {
  position: absolute;
  top: -4px;
  left: -4px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--surface-raised);
  box-shadow: var(--shadow-soft);
  border: 2px solid var(--surface-raised);
}
.discovery-row-photo-wrap .discovery-row-owner-badge-img { width: 100%; height: 100%; border-radius: 0; object-fit: cover; }
.discovery-row-owner-badge .ui-icon { width: 0.75em; height: 0.75em; color: var(--accent); }
.discovery-thumb-placeholder {
  width: 56px;
  height: 56px;
  border-radius: 10px;
  flex-shrink: 0;
  background: var(--paper-100);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.4rem;
}
.discovery-thumb-link { display: block; flex-shrink: 0; line-height: 0; }
.admin-thumb-link { display: inline-block; line-height: 0; }
.discovery-info { flex: 1; }
.discovery-name { font-weight: 600; color: var(--forest-900); }
.discovery-meta { font-size: 0.8rem; color: var(--ink-600); }
/* Diagnostic detail for a stuck/failed queued item (2026-10-04, real
   incident - see capture.js's queuedRowHtml() comment). Smaller and
   muted-red so it reads as "why", distinct from the plain timestamp line
   above it, without being as alarming as a full error banner. */
.discovery-error-detail { font-size: 0.72rem; color: var(--text-danger); word-break: break-word; }
.status-pill {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 4px 10px;
  border-radius: 999px;
}
/* --status-warning-fg (not --sun-500 - design review #167/#168): in
   Nature these were 1.85:1, failing AA; Dusk happened to pass only
   because --sun-strong and --status-warning-fg are the same hex there. */
.status-pending, .status-uploaded { background: var(--status-warning-bg); color: var(--status-warning-fg); }
.status-identified { background: var(--sage-200); color: var(--forest-700); }
.status-needs_review { background: var(--status-caution-bg); color: var(--status-caution-fg); }
/* Issue #227: discovery waiting on the user to drop a pin. Warning wash
   + caution ink (same pairing the edibility treatment uses) so it reads
   as "needs attention" without introducing a new hex. */
.status-location_needed {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--status-warning-bg);
  color: var(--status-caution-fg);
}
.status-location_needed .ui-icon { width: 1.15em; height: 1.15em; }
.status-failed { background: var(--status-danger-bg); color: var(--danger); }
.status-disputed { background: var(--status-caution-bg); color: var(--danger); }
.status-open { background: var(--sage-200); color: var(--forest-700); }
.status-resolved { background: var(--sage-200); color: var(--forest-700); }
.status-expired { background: var(--status-danger-bg); color: var(--danger); }
.status-cancelled { background: var(--status-danger-bg); color: var(--danger); }

.empty-state { color: var(--ink-600); text-align: center; padding: 24px; }

/* Achievements */
.achievements-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 18px;
}
.achievement-card {
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  padding: 20px 16px;
  text-align: center;
}
.achievement-icon { font-size: 2.2rem; display: block; margin-bottom: 8px; }
.achievement-name { font-weight: 700; color: var(--forest-900); }
.achievement-sci { font-size: 0.78rem; font-style: italic; color: var(--ink-600); margin: 2px 0 6px; }
.achievement-date { font-size: 0.72rem; color: var(--text-muted); }
.achievement-tier {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  margin: 4px 0 6px;
  font-size: 0.78rem;
  color: var(--ink-600);
}
.achievement-tier-icon { font-size: 1rem; }
.achievement-tier-label { text-transform: capitalize; font-weight: 600; }
.achievement-tier-count { color: var(--text-muted); }

/* Achievement badge sprite art (backend/app/templates/svg/badges.svg via
   same-document <use>) - roughly matches .achievement-icon's prior 2.2rem
   emoji scale. --badge-fill/--badge-rim/--badge-metal are declared with
   Nature defaults in :root; the .tier-* modifiers below only override
   --badge-metal/--badge-rim so a single reused shape (see achievements.js's
   LEADER_TIER_SPRITE) still reads as a different tier. */
.badge-mark { width: 40px; height: 40px; display: block; margin: 0 auto 8px; }
.badge-mark-fallback { color: var(--accent); }
.badge-mark-wrap { position: relative; display: inline-flex; margin: 0 auto 8px; }
.badge-mark-wrap .badge-mark { margin: 0; }
.badge-count {
  position: absolute;
  right: -6px;
  bottom: -4px;
  background: var(--accent-strong);
  color: var(--text-inverse);
  font-size: 0.65rem;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 999px;
  line-height: 1.4;
  box-shadow: var(--shadow-soft);
}
.badge-mark.tier-bronze { --badge-metal: #b87333; --badge-rim: #6b3e16; }
.badge-mark.tier-silver { --badge-metal: #9aa4ad; --badge-rim: #5c656d; }
.badge-mark.tier-gold { --badge-metal: #e8a317; --badge-rim: #8a5a00; }
.badge-mark.tier-diamond { --badge-metal: #cdeef7; --badge-rim: #2a7f9e; }
.stats-subheading { margin: 24px 0 10px; color: var(--forest-900); font-size: 1.05rem; }
.stats-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.stats-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: var(--surface-raised);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  box-shadow: var(--shadow-soft);
}
.stats-list-label { font-weight: 600; color: var(--forest-900); }
.stats-list-value { color: var(--ink-600); font-size: 0.9rem; }
.challenge-card { text-align: left; text-decoration: none; display: block; }
.challenge-pot { font-size: 0.85rem; color: var(--forest-700); margin: 6px 0; }

.victorina-panel {
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  padding: var(--space-6) var(--space-5);
  max-width: 480px;
  margin: 0 auto;
  text-align: center;
}
.victorina-reward-hint {
  display: inline-flex;
  align-items: center;
  gap: 0.2em;
  font-weight: 600;
  color: var(--accent);
  white-space: nowrap;
}
.victorina-idle-mark {
  width: 64px;
  height: 64px;
  margin: 0 auto var(--space-4);
  border-radius: var(--radius-md);
  background: var(--accent-wash);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
}
.victorina-idle-mark .ui-icon { width: 2em; height: 2em; }
.victorina-idle-copy {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.15rem;
  color: var(--text-primary);
  margin: 0 0 var(--space-5);
}
.victorina-start-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}
.victorina-progress { margin-bottom: var(--space-4); }
.victorina-progress-label {
  font-weight: 600;
  color: var(--accent);
  margin: 0 0 var(--space-3);
}
.victorina-steps {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--space-1);
}
.victorina-step {
  width: 10px;
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--border-default);
}
.victorina-step.is-done { background: var(--accent); }
.victorina-step.is-current {
  width: 18px;
  background: var(--sun);
  box-shadow: 0 0 0 3px var(--accent-wash);
}
.victorina-photo-wrap { margin-bottom: var(--space-4); }
.victorina-photo {
  width: 100%;
  max-width: 320px;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
}
.victorina-prompt {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--space-4);
}
.victorina-options {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}
.victorina-option-btn {
  width: 100%;
  min-height: 44px;
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  border: 1.5px solid var(--border-default);
  background: var(--surface-canvas-subtle);
  color: var(--text-primary);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  overflow-wrap: anywhere;
  transition: background var(--ease), border-color var(--ease), color var(--ease), transform var(--ease-lift);
}
#victorina-result-banner {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.victorina-option-btn:hover:not(:disabled) {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--accent-strong);
  transform: translateY(-1px);
}
.victorina-option-btn:disabled { opacity: 0.55; cursor: not-allowed; }
.victorina-result-mark {
  width: 56px;
  height: 56px;
  margin: 0 auto var(--space-4);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.victorina-result-mark .ui-icon { width: 1.75em; height: 1.75em; }
.victorina-result-mark-win {
  background: var(--status-success-bg);
  color: var(--status-success-fg);
}
.victorina-result-mark-lose {
  background: var(--status-danger-bg);
  color: var(--status-danger-fg);
}
.victorina-win {
  color: var(--status-success-fg);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.2rem;
  margin: 0 0 var(--space-2);
}
.victorina-win-reward {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  font-weight: 600;
  color: var(--accent);
  margin: 0 0 var(--space-5);
}
.victorina-lose {
  color: var(--status-danger-fg);
  font-weight: 700;
  margin: 0 0 var(--space-5);
}

/* Leaderboard */
.leaderboard-list { display: flex; flex-direction: column; gap: 10px; max-width: 560px; margin: 0 auto; }
.leaderboard-row {
  display: flex;
  align-items: center;
  gap: 16px;
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  padding: 14px 18px;
  box-shadow: var(--shadow-soft);
}
.leaderboard-rank {
  font-family: 'Fraunces', serif;
  font-weight: 700;
  font-size: 1.3rem;
  color: var(--forest-700);
  width: 34px;
}
.leaderboard-row:nth-child(1) .leaderboard-rank { color: var(--sun-500); }
.leaderboard-name { flex: 1; font-weight: 600; display: flex; flex-direction: column; gap: 4px; }
.leaderboard-leader-tag {
  align-self: flex-start;
  font-size: 0.68rem;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
}
.leaderboard-leader-tag-world { background: var(--sun-500); color: var(--text-on-accent); }
.leaderboard-leader-tag-region { background: var(--sage-200); color: var(--forest-700); }
.leaderboard-count { color: var(--ink-600); font-weight: 600; }
.leaderboard-avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--paper-100);
}
.leaderboard-avatar-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  background: var(--sage-200);
}
a.leaderboard-row { text-decoration: none; color: inherit; }
.leaderboard-tabs {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}
.leaderboard-tab {
  background: var(--surface-raised);
  border: 1px solid var(--sage-200);
  border-radius: 999px;
  padding: 8px 18px;
  font-weight: 600;
  color: var(--ink-600);
  cursor: pointer;
}
.leaderboard-tab.active {
  background: var(--forest-700);
  border-color: var(--forest-700);
  color: var(--text-on-accent);
}

/* Profile */
.profile-card {
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  padding: 32px 36px;
  max-width: 520px;
  margin: 0 auto 24px;
}

/* Standalone-page form card (challenge_new.html etc.) - same raised-card
   treatment as .profile-card, wider to comfortably fit a Leaflet map
   widget. Distinct from .edit-id-form, which is a sunken inline well
   meant for a correction form nested *inside* another card, not a
   standalone page's own top-level form. */
.form-card {
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  padding: 32px 36px;
  max-width: 620px;
  margin: 0 auto 24px;
}
.form-card select {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 12px 14px;
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: inherit;
  background: var(--surface-raised);
}
.form-card fieldset {
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: 16px;
  margin: 0 0 16px;
}
.form-card legend { font-weight: 600; color: var(--text-primary); padding: 0 6px; }
.form-card .area-picker-fieldset .area-hint { margin-top: 0; }
.form-card .area-picker-fieldset #use-my-location-btn { margin-bottom: 10px; }
.form-card .area-picker-fieldset #radius-field { margin-top: 10px; max-width: 260px; }
.avatar-row {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-bottom: 28px;
}
.avatar-preview-wrap {
  width: 84px;
  height: 84px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  background: var(--sage-200);
}
.avatar-preview { width: 100%; height: 100%; object-fit: cover; }
.avatar-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
}
.avatar-choose-btn { width: auto; padding: 9px 18px; }
.avatar-remove-btn { margin-left: 10px; font-size: 0.85rem; }
.avatar-hint { font-size: 0.78rem; color: var(--ink-600); margin: 8px 0 0; }
.profile-card textarea {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 12px 14px;
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: inherit;
  background: var(--paper-100);
  resize: vertical;
}
.profile-email { font-size: 0.85rem; color: var(--ink-600); margin: 18px 0 0; }
.gemini-key-status { font-size: 0.8rem; color: var(--ink-600); margin: -8px 0 10px; }
.gemini-key-hint { font-size: 0.8rem; color: var(--ink-600); margin: -6px 0 14px; }
.gemini-key-hint a { color: var(--forest-700); font-weight: 600; }
.danger-zone h2 { margin-top: 0; color: var(--danger); font-size: 1.1rem; }
.danger-zone p { color: var(--ink-600); font-size: 0.9rem; }
.delete-trigger-btn { width: auto; padding: 10px 20px; color: var(--danger); }
.danger-btn { background: var(--danger); }
/* Log out moved from its own page-width block into the Account card
   (site-redesign PR 9) as a .btn-ghost - .account-logout-row just adds
   the same top spacing .logout-form-page used to provide standalone. */
.account-logout-row { margin-top: 20px; }

.checkbox-label {
  display: flex !important;
  align-items: flex-start;
  gap: 8px;
  font-size: 0.85rem !important;
}
.checkbox-label input[type="checkbox"] {
  width: auto !important;
  margin-top: 3px;
  flex-shrink: 0;
}
.checkbox-label span { line-height: 1.4; }
.checkbox-label a { color: var(--forest-700); }

/* Legal pages (terms/privacy) */
.legal-doc {
  max-width: 760px;
  margin: 0 auto;
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  padding: 40px 44px 56px;
}
.legal-doc h1 { color: var(--forest-900); margin-bottom: 4px; }
.legal-updated { color: var(--ink-600); font-size: 0.85rem; margin-top: 0; }
.legal-doc h2 { color: var(--forest-700); font-size: 1.15rem; margin-top: 32px; }
.legal-doc p, .legal-doc li { color: var(--ink-900); line-height: 1.6; }
.legal-doc ul { padding-left: 22px; }
.legal-doc a { color: var(--forest-700); }

.site-footer {
  text-align: center;
  padding: 24px 16px;
  font-size: 0.8rem;
  color: var(--ink-600);
}
.site-footer div + div { margin-top: 6px; }
.site-footer a { color: var(--ink-600); text-decoration: underline; }

/* Cookie banner vs footer padding (site-redesign PR 8, then issue #126
   dropped the bottom tab bar so the mobile extra --nav-height-bottom
   offset is no longer needed). cookie-banner.js toggles this class on
   <html>, not just the banner's own display. */
html.cookie-banner-visible .page,
html.cookie-banner-visible .site-footer {
  padding-bottom: calc(var(--cookie-banner-height) + var(--space-8));
}

/* Admin */
.admin-tabs {
  display: flex;
  gap: 4px;
  max-width: 900px;
  margin: 0 auto 24px;
  border-bottom: 1.5px solid var(--border-default);
}
.admin-tab {
  padding: 10px 18px;
  text-decoration: none;
  color: var(--text-secondary);
  font-weight: 500;
  border-bottom: 2px solid transparent;
  margin-bottom: -1.5px;
}
.admin-tab:hover { color: var(--accent); }
.admin-tab.active { color: var(--accent); border-bottom-color: var(--sun-strong); }

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 16px;
  max-width: 900px;
  margin: 0 auto 28px;
}
.stat-tile {
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  padding: 20px 16px;
  text-align: center;
}
.stat-value { font-family: 'Fraunces', serif; font-size: 2rem; font-weight: 700; color: var(--accent); }
.stat-label { font-size: 0.82rem; color: var(--text-secondary); margin-top: 4px; }

.admin-panel {
  max-width: 900px;
  margin: 0 auto 28px;
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  padding: 22px 26px;
}
.admin-panel h2 { margin-top: 0; color: var(--accent-strong); font-size: 1.1rem; }
.status-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 0;
  border-bottom: 1px solid var(--surface-canvas-subtle);
}
.status-row:last-child { border-bottom: none; }

.admin-table-wrap {
  max-width: 1000px;
  margin: 0 auto;
  overflow-x: auto;
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
}
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
.admin-table th, .admin-table td {
  text-align: left;
  padding: 12px 14px;
  border-bottom: 1px solid var(--surface-canvas-subtle);
  white-space: nowrap;
}
.admin-table th { color: var(--text-secondary); font-weight: 600; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.03em; }
.admin-table tr:last-child td { border-bottom: none; }
.admin-actions { display: flex; gap: 10px; }
.admin-actions .link-btn { padding: 0; }
.link-btn.danger:hover { color: var(--danger); }
.admin-thumb { width: 44px; height: 44px; border-radius: 8px; object-fit: cover; display: block; }

.modal-overlay {
  position: fixed;
  inset: 0;
  /* --surface-overlay already existed for exactly this (a dimming scrim
     token, redefined to a neutral black in Dusk/System-dark rather than
     Nature's dark-forest tint) but this rule had a literal hex/rgba
     duplicate of the Nature value instead of the token itself - site-
     redesign PR 9's admin-CSS token pass. */
  background: var(--surface-overlay);
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
/* Author CSS always beats the UA stylesheet's [hidden]{display:none},
   regardless of specificity - .modal-overlay's own `display: flex` above
   would otherwise permanently defeat the `hidden` attribute #more-sheet
   is opened/closed with (same defensive pattern already applied to
   .lang-menu[hidden] above; #edit-modal/#delete-modal don't need this
   since they're toggled via inline style="display:none", not `hidden`). */
.modal-overlay[hidden] { display: none; }
.modal-card {
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  padding: 32px;
  max-width: 420px;
  width: 100%;
  box-shadow: var(--shadow-lift);
}
.modal-card h2 { margin-top: 0; color: var(--accent-strong); }
.modal-actions { display: flex; gap: 12px; margin-top: 20px; }
.modal-actions .btn-primary, .modal-actions .btn-secondary { width: auto; flex: 1; margin-top: 0; }

/* More sheet item list (site-redesign PR 8) - reuses .modal-overlay/
   .modal-card (above) for the dialog chrome itself. */
.more-sheet-list { display: flex; flex-direction: column; gap: 2px; margin: 16px 0; }
.more-sheet-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 44px;
  padding: 10px;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font: inherit;
  color: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.more-sheet-item:hover, .more-sheet-item:focus-visible { background: var(--surface-sunken); }
.more-sheet-item-icon { font-size: 1.25em; line-height: 1; width: 1.25em; text-align: center; }
.more-sheet-item-danger { color: var(--danger); width: 100%; }
.more-sheet-item-danger:hover { background: var(--status-danger-bg); }
.more-sheet-pref-row { justify-content: space-between; cursor: default; }
.more-sheet-pref-row:hover { background: none; }
.more-sheet-pref-label { display: flex; align-items: center; gap: 8px; }
/* The relocated theme button / lang_switcher() inside a pref row keep
   their own .profile-avatar-link circular sizing - no override needed. */
/* .logout-form is `display: inline` by default (shared with the Account
   card's .account-logout-row on /profile) - block so its .more-sheet-item
   button fills the row's full width like every other sheet item. */
.more-sheet-list .logout-form { display: block; }

/* Discovery detail */
.detail-card {
  max-width: 640px;
  margin: 0 auto 32px;
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  overflow: hidden;
}
/* aspect-ratio:1/1, not a fixed max-height - every current photo variant
   (crop_and_scale() always outputs a square) is genuinely square, but a
   max-height on a wider-than-480px container forced a landscape-ish crop
   box, and object-fit:cover then cropped the top/bottom off the square
   source - specifically the *bottom*, where the watermark lives, making
   it look clipped/missing even though the stored image is untouched.
   aspect-ratio keeps the box square regardless of container width, so
   cover has nothing left to crop for a genuinely square source. */
.detail-photo { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block; background: var(--paper-100); cursor: zoom-in; touch-action: pan-y; user-select: none; -webkit-user-select: none; }
/* 2026-10-03 (explicit request, corrected same day): fullscreen,
   genuinely SCROLLABLE viewer for the square watermarked display photo
   (discovery_detail.js opens this on a #detail-photo click) - explicitly
   NOT the raw original, which per product policy is never shown to
   anyone. Deliberately NOT a flex-centered modal like .modal-overlay
   (centering a flex child larger than its container can clip the
   scrollable region in some browsers); plain block + overflow:auto plus
   text-align:center on the overlay / inline-block on the image gives
   horizontal centering when the image fits and full scroll/pan in both
   directions when it doesn't. The actual pixel size (never scaled down,
   scaled up to fill the viewport's smaller dimension when the image is
   smaller than that) is set by discovery_detail.js's
   sizePhotoLightboxImage() in JS, not by this CSS - width/height: auto
   here is only the pre-load fallback, overwritten once the image decodes. */
.photo-lightbox-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: rgba(0, 0, 0, 0.92);
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  text-align: center;
  padding: 56px 16px 16px;
}
.photo-lightbox-overlay[hidden] { display: none; }
.photo-lightbox-img { display: inline-block; max-width: none; width: auto; height: auto; cursor: zoom-out; }
.photo-lightbox-close-btn {
  position: fixed;
  top: 12px;
  right: 12px;
  z-index: calc(var(--z-modal) + 1);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-raised);
  color: var(--text-primary);
  border: none;
  box-shadow: var(--shadow-soft);
  cursor: pointer;
}
/* Quick-glance discoverer identifier overlaid on the photo's own top-left
   corner (user feedback: "every discovery should have a user icon...
   about who did it") - purely decorative (aria-hidden, see the template),
   the real accessible name+link stays in .detail-owner below. A solid
   background + shadow (not just the bare avatar) so it stays legible
   against any photo content, matching the same corner-badge idea as
   feed.js's own avatar treatment but for a single large photo instead of
   a header row. */
.detail-photo-wrap { position: relative; }
.detail-photo-owner-badge {
  position: absolute;
  top: 12px;
  left: 12px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--surface-raised);
  box-shadow: var(--shadow-soft);
  border: 2px solid var(--surface-raised);
}
/* Multishot position badge, "2 / 3" (issue #224) - top-right corner,
   mirroring the owner badge in the top-left. */
.detail-photo-counter {
  position: absolute;
  top: 12px;
  right: 12px;
  padding: 2px 10px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.6;
  pointer-events: none;
}
.detail-photo-counter[hidden] { display: none; }
.detail-photo-owner-badge-img { width: 100%; height: 100%; object-fit: cover; }
.detail-photo-owner-badge-placeholder { color: var(--accent); width: 1.3em; height: 1.3em; }
/* Multishot gallery (2026-09-05) - the strip itself reuses .thumb-strip/
   .thumb-item/.thumb-item-selected from the capture page's own burst UI
   unchanged; this just adds the "doesn't match the main subject" marker,
   which that page never needed (nothing is submitted as "unconfirmed"
   there - that judgement only exists after identification). */
#detail-thumbs { margin: 10px 28px 0; }
.thumb-item-unconfirmed { opacity: 0.5; }
.thumb-item-unconfirmed-badge {
  position: relative;
}
.thumb-item-unconfirmed-badge::after {
  content: "?";
  position: absolute;
  top: -4px;
  right: -4px;
  background: var(--sun-500);
  color: var(--text-on-accent);
  font-size: 0.7rem;
  font-weight: 700;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.detail-body { padding: 24px 28px 28px; }
.detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.detail-header h1 { margin: 0; color: var(--forest-900); font-size: 1.5rem; }
.detail-sci { margin: 2px 0 0; font-style: italic; color: var(--ink-600); }
.detail-tags { margin: 6px 0 0; display: flex; flex-wrap: wrap; gap: 6px; }
.tag-chip {
  display: inline-block;
  background: var(--sage-200);
  color: var(--forest-700);
  font-size: 0.78rem;
  font-weight: 600;
  padding: 3px 10px;
  border-radius: 999px;
}
/* Like applies to the photo - sits right under the image, deliberately
   separate from the agree/disagree identification vote below. */
.photo-actions { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
/* Share (everyone) + Delete (owner/admin only) - grouped on the opposite
   side of the like button (site-redesign PR 9, UX row 20). */
.photo-actions-right { display: flex; align-items: center; gap: 8px; }
/* Empty by default (:empty rule below hides it); share-btn's click
   handler fills it with a transient "Link copied!" confirmation for the
   clipboard-fallback path (navigator.share has its own native UI, so
   this only ever shows on browsers/contexts without it). */
.share-status { font-size: 0.8rem; color: var(--text-secondary); }
.share-status:empty { display: none; }
/* Shared round-icon-button base - .delete-discovery-btn below extends
   this with its own danger-colored hover instead of duplicating the
   shape/sizing rules. */
.icon-round-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--paper-100);
  border: none;
  border-radius: 999px;
  width: 40px;
  height: 40px;
  color: var(--ink-600);
  cursor: pointer;
  flex-shrink: 0;
  transition: background .15s, color .15s;
}
.icon-round-btn:hover { background: var(--accent-wash-hover); color: var(--accent); }
.like-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--paper-100);
  border: none;
  border-radius: 999px;
  padding: 8px 16px;
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink-600);
  cursor: pointer;
  flex-shrink: 0;
  transition: background .15s, color .15s;
}
.like-btn.liked { background: var(--like-active-bg); color: var(--danger); }
.like-btn:hover { background: var(--sage-200); }
.like-btn.liked:hover { background: var(--like-active-bg); }
.like-btn.liked .ui-icon { fill: currentColor; }

/* Delete, shown only to the discovery's owner, on their own persona
   ("My discoveries") page's cards (issue #204) - not on the single-
   discovery page. */
.delete-discovery-btn:hover { background: var(--status-danger-bg); color: var(--danger); }

/* Agree/disagree applies to the identification, not the photo - grouped
   with the identification-correction controls right under the species
   name/tags, distinct from the like button above. */
.id-actions { margin-top: 14px; display: flex; flex-direction: column; gap: 10px; }
.vote-group-wrap { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.id-actions-label { font-size: 0.85rem; font-weight: 600; color: var(--ink-600); }
.vote-group { display: flex; gap: 8px; }
.vote-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--paper-100);
  border: none;
  border-radius: 999px;
  padding: 8px 14px;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--ink-600);
  cursor: pointer;
  transition: background .15s, color .15s;
}
.vote-btn.active { background: var(--sage-200); color: var(--forest-700); }
.vote-btn:hover { background: var(--sage-200); }

/* "Identify with Gemini" - an icon-only button using Gemini's own
   sparkle-gradient branding instead of a plain text label. */
.gemini-identify-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: none;
  border-radius: 50%;
  background: linear-gradient(135deg, #4285f4 0%, #9b72cb 50%, #d96570 100%);
  color: #fff;
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  vertical-align: middle;
  box-shadow: 0 2px 8px rgba(155, 114, 203, 0.4);
  transition: transform .15s, box-shadow .15s, opacity .15s;
}
.gemini-identify-btn:hover { transform: translateY(-1px) scale(1.05); box-shadow: 0 4px 12px rgba(155, 114, 203, 0.5); }
.gemini-identify-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
.gemini-key-hint-inline { font-size: 0.8rem; color: var(--ink-600); margin: 6px 0 0; }
.gemini-key-hint-inline a { color: var(--forest-700); font-weight: 600; }

.detail-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 14px 0;
  font-size: 0.85rem;
  color: var(--ink-600);
}
.detail-owner { display: flex; align-items: center; gap: 8px; text-decoration: none; color: inherit; }
.detail-owner-avatar { width: 26px; height: 26px; border-radius: 50%; object-fit: cover; }

.detail-map { height: 220px; border-radius: var(--radius-md); overflow: hidden; margin-bottom: 16px; z-index: 1; }
.detail-map-caption { margin: -12px 0 16px; font-size: 0.78rem; color: var(--ink-600); text-align: center; }
/* Small triangle marker for "other sightings of this species" on the map
   (Leaflet divIcon, see discovery_detail.js's initMap()) - visually
   distinct from this discovery's own round pin. The legend caption reuses
   the identical shape/color so the map and its key match exactly. */
.species-marker-triangle, .species-marker-legend-triangle {
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-bottom: 10px solid var(--sun-500);
}
.species-marker-legend-triangle { display: inline-block; vertical-align: middle; margin-right: 2px; }

.edit-id-form {
  margin-top: 10px;
  padding: 16px;
  background: var(--paper-100);
  border-radius: var(--radius-sm);
}
.edit-id-form select {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 12px 14px;
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: inherit;
  background: var(--surface-raised);
}
.edit-id-save-btn { width: auto; padding: 9px 20px; }

.comments-section { max-width: 640px; margin: 0 auto 60px; }
.comment-form { display: flex; gap: 10px; margin-bottom: 20px; align-items: flex-start; }
.comment-form textarea {
  flex: 1;
  padding: 10px 12px;
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: 0.95rem;
  resize: vertical;
  background: var(--paper-100);
}
.comment-submit-btn { width: auto; padding: 10px 18px; flex-shrink: 0; }

.comment {
  background: var(--surface-raised);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-soft);
  padding: 14px 16px;
  margin-bottom: 10px;
}
.comment-replies { margin-left: 28px; }
.comment-reply { background: var(--paper-100); box-shadow: none; }
.comment-header { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; }
.comment-author-link { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; color: inherit; }
.comment-avatar { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; }
.comment-avatar-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-wash);
  color: var(--accent);
  font-size: 1rem;
}
.comment-avatar-placeholder .ui-icon { width: 1em; height: 1em; }
.comment-date { color: var(--text-muted); margin-left: auto; font-size: 0.78rem; }
.comment-body { margin: 8px 0 4px; color: var(--ink-900); font-size: 0.92rem; line-height: 1.5; }
.reply-btn { font-size: 0.8rem; padding: 0; }
/* AI comment answers (issue #205 and its always-on follow-up) */
.comment-form, .reply-form { flex-wrap: wrap; }
.ai-status { margin: -10px 0 16px; font-size: 0.85rem; color: var(--text-muted); }
.ai-status[hidden] { display: none; }
.comment-ai { border-left: 3px solid var(--accent); }
.comment-avatar-ai { object-fit: contain; }
.reply-form { display: flex; gap: 8px; margin-top: 10px; align-items: flex-start; }
.reply-form textarea {
  flex: 1;
  padding: 8px 10px;
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: 0.88rem;
  resize: vertical;
  background: var(--surface-raised);
}
.reply-submit-btn { width: auto; padding: 8px 14px; flex-shrink: 0; font-size: 0.85rem; }

/* Cookie consent banner. z-index above the sticky top nav's var(--z-nav)
   so it stays tappable; sits at the bottom of the viewport on every
   width now that the toolbar lives at the top (issue #126). */
.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-banner);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  flex-wrap: wrap;
  padding: 16px 24px;
  background: var(--forest-900);
  color: var(--paper-50);
  box-shadow: var(--shadow-banner);
}
.cookie-banner p { margin: 0; max-width: 640px; font-size: 0.88rem; line-height: 1.4; }
.cookie-accept-btn { width: auto; padding: 10px 22px; flex-shrink: 0; }

/* User Discoveries page */
.back-btn { margin-bottom: 12px; display: inline-block; }
.user-header { display: flex; align-items: center; gap: 16px; justify-content: center; }
.user-header-avatar {
  width: 72px;
  height: 72px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--paper-100);
}
.user-header-avatar-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  background: var(--sage-200);
}
/* Persona page: the rank/rival sentence, standing on its own between the
   identity header and the stat-count grid (design review #167/#168) -
   previously plain body text (.tagline) sandwiched under the shadowed
   .feed-stats-grid, which read as the quietest thing on the page despite
   being the whole point of the feature. */
.persona-standing {
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  padding: var(--space-4);
  margin: 0 auto var(--space-5);
  text-align: center;
}
.persona-standing h2 { margin: 0; font-weight: 600; color: var(--text-primary); line-height: 1.3; font-size: 1.2rem; }
.persona-standing-meta { margin: var(--space-2) 0 0; color: var(--text-secondary); font-size: 0.95rem; }
/* .persona-standing/-meta set no display of their own, so the UA
   stylesheet's [hidden] { display: none } already applies - only
   .persona-rival needs an explicit override, since it sets display: flex
   itself (author CSS beats the UA [hidden] rule regardless of
   specificity - a bug class this codebase has hit twice before). */
.persona-rival {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-3) auto 0;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.4;
  text-decoration: none;
}
.persona-rival[hidden] { display: none; }
@media (min-width: 620px) {
  .persona-standing { width: 100%; max-width: 700px; }
}
/* Persona page: a single native <select> instead of a chip per tag - a
   heavy user's own tag list can run into the hundreds, which made the old
   flat chip row unreasonably long (real user feedback). Matches the
   Explore page's own tag-filter treatment; unlike Explore's accumulating
   multi-tag row, this is single-select (the timeline API only ever takes
   one `tag`), so there's no separate "chosen filters" row to style. */
.persona-tag-filter { text-align: center; margin-bottom: 20px; }
.persona-tag-filter label { display: inline-block; font-weight: 600; color: var(--text-secondary); font-size: 0.9rem; }
.persona-tag-filter select { display: block; margin-top: 6px; }
.tag-filter-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-bottom: 20px; }
.tag-filter-chip {
  background: var(--surface-raised);
  border: 1px solid var(--sage-200);
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--ink-600);
  cursor: pointer;
}
.tag-filter-chip.active { background: var(--forest-700); border-color: var(--forest-700); color: var(--text-on-accent); }
a.discovery-row { text-decoration: none; color: inherit; }
/* Multishot feed gallery (2026-09-05) - small, non-interactive strip of
   the discovery's other photos under its main thumbnail/text in a feed
   row. Purely visual; the whole row is already one link to the
   discovery's real (interactive) gallery on its detail page. */
/* Feed cards: author icon header + large square (single photo) or
   grouped mosaic (multishot) media + footer, replacing the old flat
   .discovery-row treatment for /feed specifically (capture.js/
   admin_discoveries.js keep using .discovery-row unchanged - this was
   feed-only, then persona.js/persona.html adopted .feed-card too when it
   replaced user_discoveries.html/.js). */
.feed-card {
  display: block;
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  overflow: hidden;
}
/* The global `a:focus-visible { outline: none; box-shadow: var(--focus-ring); }`
   rule gets clipped here: .feed-card-link is a full-bleed child of this
   overflow:hidden card, so a shadow drawn on the link itself is cut off on
   the top/sides and painted over on the bottom by the opaque
   .feed-card-footer - a keyboard user tabbing the timeline got no visible
   indicator at all (design review #167/#168). Draw the ring on the
   uncropped card instead, only while its own link has focus (:has, not a
   blanket .feed-card:focus-within, so a like/comment control focused
   inside a *different* card's footer doesn't light this one up); the
   second rule (0,3,0 specificity) beats the global a:focus-visible rule
   (0,1,1) so the clipped inner ring never leaves a sliver alongside it. */
.feed-card:has(.feed-card-link:focus-visible) {
  box-shadow: var(--shadow-soft), var(--focus-ring);
}
.feed-card .feed-card-link:focus-visible {
  box-shadow: none;
}
/* Capped and centered on wider screens - .feed-card-media's aspect-ratio:1/1
   means the photo scales with the card's own width, so left unconstrained
   on a desktop-width .page (max-width 880px) each card's square photo
   alone renders ~800px tall, fitting barely one card per screen. 700px
   (widened from 520px, reported as narrower than the stats bar above it -
   .feed-stats-grid has no cap of its own and spans .page's full width) is
   shared with .feed-stats-grid below so the two visually align, while
   still well short of .page's 880px to avoid reintroducing the original
   too-tall-photo problem. Mobile (already narrower than this cap) is
   unaffected. */
@media (min-width: 620px) {
  /* width: 100% is load-bearing, not redundant with max-width: .feed-card
     is a flex item inside .discoveries-list (flex-direction: column,
     default align-items: stretch) - per the flexbox spec, an auto margin
     on a flex item's cross axis (margin-left/right: auto here, used for
     centering) disables that axis's stretch, so without an explicit width
     the card falls back to shrink-to-fit its own content instead of
     filling the shared 700px cap. That's why a photo-less card (a
     challenge_event/area_milestone/new_user post, or any single-column
     post whose text content is narrow) rendered visibly narrower than a
     photo card next to it - reported live and confirmed via a real
     Playwright bounding-box check before this fix (uneven) and after
     (identical). */
  .feed-card, .feed-stats-grid { width: 100%; max-width: 700px; margin-left: auto; margin-right: auto; }
}
/* The clickable navigate-away region (header + media) - issue #90 moved
   like/comment controls out of the anchor entirely (a button inside an
   <a> works, but a separate interactive footer sibling is unambiguous and
   needs no stopPropagation/preventDefault dance), so only this inner link
   carries the old .feed-card's own text-decoration/color reset now. */
.feed-card-link { display: block; text-decoration: none; color: inherit; }
.feed-card-header { display: flex; align-items: center; gap: 10px; padding: 12px 16px; }
.feed-card-author-link { display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit; flex: 1; min-width: 0; }
.feed-card-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: var(--surface-canvas-subtle); }
.feed-card-avatar-fallback { display: flex; align-items: center; justify-content: center; font-size: 1.1rem; color: var(--accent); }
.feed-card-headtext { display: flex; flex-direction: column; min-width: 0; }
.feed-card-author { font-weight: 600; color: var(--text-primary); font-size: 0.92rem; }
.feed-card-reason {
  display: flex;
  align-items: flex-start;
  gap: 0.3em;
  font-size: 0.82rem;
  color: var(--text-secondary);
}
.feed-card-reason .ui-icon { flex-shrink: 0; color: var(--accent); }
.feed-card-reason.is-disputed { color: var(--status-caution-fg); }
.feed-card-reason.is-disputed .ui-icon { color: var(--status-caution-fg); }
.feed-card-media { display: grid; grid-template-columns: 1fr; grid-template-rows: 1fr; gap: 2px; aspect-ratio: 1 / 1; width: 100%; background: var(--surface-canvas-subtle); }
.feed-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.feed-card-media.feed-media-grid-2 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; }
.feed-card-media.feed-media-grid-3 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.feed-card-media.feed-media-grid-3 img:first-child { grid-row: span 2; }
.feed-card-media.feed-media-grid-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.feed-card-event {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 2.4 / 1;
  width: 100%;
  background: var(--accent-wash);
  color: var(--accent);
}
.feed-card-event .ui-icon { width: 3em; height: 3em; }
.feed-card-event .badge-mark { width: 96px; height: 96px; margin: 0; }
/* .badge-count's base rule (achievements grid) pairs --text-inverse on
   --accent-strong; this override only ever changed the background, not
   the color, so on this canvas-colored variant the number was
   --text-inverse on --surface-canvas - the same near-white/near-black hex
   in both themes, i.e. invisible (design review #167/#168). */
.feed-card-event .badge-count { position: absolute; bottom: 8px; right: 8px; background: var(--surface-canvas); color: var(--text-primary); border-radius: 50%; padding: 2px 8px; font-weight: 700; font-size: 0.9rem; }
/* Challenge and area-milestone body (issue #265). A short wash row, not
   .feed-card-event - persona badges still use that class at 2.4/1.
   No min-height: the row is as tall as its padding plus the 36px icon. */
.feed-card-note {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  background: var(--accent-wash);
  color: var(--accent);
  min-width: 0;
}
.feed-card-note-icon {
  flex: 0 0 36px;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
}
.feed-card-note-icon .ui-icon {
  width: 1.5rem;
  height: 1.5rem;
}
.feed-card-note-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.feed-card-note-headline {
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text-primary);
}
.feed-card-note-detail {
  font-size: 0.82rem;
  line-height: 1.35;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#feed-post-card h2 {
  display: flex;
  align-items: center;
  gap: 0.4em;
  color: var(--text-primary);
}
#feed-post-card h2 .ui-icon { color: var(--accent); flex-shrink: 0; }
.feed-post-detail {
  margin: var(--space-2) 0 var(--space-4);
  font-size: 0.95rem;
  line-height: 1.35;
  color: var(--text-secondary);
  text-align: start;
  overflow-wrap: break-word;
}
.feed-card-footer { padding: 10px 16px 0; font-size: 0.82rem; color: var(--text-secondary); font-weight: 600; }
/* Discovery text preview (edibility + "about this species", 2026-10-06,
   reformatted into per-field items 2026-10-09 per issue #218). One box of
   per-field items, collapsed to its first 4 lines (label + 3 text lines).
   Issue #222: this used -webkit-line-clamp on a -webkit-box, which only
   clamps reliably over inline content - with several block items iOS
   WebKit shrank the items to the clamp height but still painted their
   text, so fields drew over each other and scrollHeight never exceeded
   clientHeight (no "See more"). A plain max-height + overflow:hidden clamp
   behaves the same in every engine; JS adds --overflowing to fade the
   last line out when something is actually hidden. The icon is inline;
   .ui-icon's default 1.25em would enlarge the label line if not
   overridden. */
.feed-card-info-wrap { padding: 8px 16px 0; }
.feed-card-info {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.35;
  color: var(--text-secondary);
  /* label line (0.72rem) + 3 text lines (0.82rem), both at line-height 1.35 */
  max-height: calc(1.35 * 0.72rem + 3 * 1.35 * 0.82rem);
  overflow: hidden;
}
.feed-card-info--overflowing {
  -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent);
  mask-image: linear-gradient(to bottom, #000 70%, transparent);
}
.feed-card-info-item { margin: 0 0 var(--space-1); }
.feed-card-info-item:last-child { margin-bottom: 0; }
.feed-card-info-label {
  display: block;
  font-weight: 700;
  font-size: 0.72rem;
  line-height: 1.35;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--accent);
}
.feed-card-info-label .ui-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  color: var(--status-caution-fg);
}
.feed-card-info-value {
  display: block;
  font-weight: 400;
  color: var(--text-secondary);
}
.feed-card-info--expanded {
  max-height: none;
  overflow: visible;
  -webkit-mask-image: none;
  mask-image: none;
}
.feed-card-info-more { margin-top: 4px; font-size: 0.82rem; }
/* Like/comment-toggle row (issue #90) - reuses .like-btn as-is (same
   control already proven on the discovery detail page/feed permalink
   page), just sized down slightly to sit comfortably in a feed row. */
.feed-card-actions { display: flex; align-items: center; gap: 6px; padding: 10px 16px 12px; }
.feed-card-actions .like-btn { padding: 6px 12px; font-size: 0.88rem; }
/* Owner-only delete on the persona page's own discovery cards (issue #204),
   pushed to the far right, away from the counts. */
.feed-card-actions .persona-delete-btn { margin-left: auto; width: 36px; height: 36px; }
.feed-comment-toggle-btn { font-size: 0.85rem; padding: 6px 4px; }
.feed-comments-panel { padding: 0 16px 16px; border-top: 1px solid var(--border-subtle); }
.feed-comments-panel .comments-list { margin: 12px 0; }
.feed-comments-panel .comment-form { margin-bottom: 0; }
.feed-load-more-wrap { text-align: center; margin-top: var(--space-4); }
.load-more-btn { display: block; margin: 20px auto 0; }
.section-link { font-size: 0.85rem; font-weight: 600; margin-left: 8px; }
.section-link-wrap { text-align: center; margin-top: -8px; }

/* Discovery search */
/* The one prominent, always-visible control on this page (direct owner
   request, 2026-10-11: "this is the most important search"). Everything
   else lives in .advanced-search below. */
.search-primary {
  max-width: 640px;
  margin: 0 auto 16px;
}
.search-primary-label {
  display: block;
  font-weight: 600;
  margin-bottom: 8px;
  color: var(--text-secondary);
  font-size: 0.9rem;
}
.search-primary-input-wrap { position: relative; }
.search-primary-icon {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  width: 1.3rem;
  height: 1.3rem;
  color: var(--text-secondary);
  pointer-events: none;
}
.search-primary-input {
  display: block;
  width: 100%;
  padding: 16px 16px 16px 48px;
  font-size: 1.15rem;
  font-family: inherit;
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  box-shadow: var(--shadow-soft);
  transition: border-color .15s, box-shadow .15s;
}
.search-primary-input:focus {
  outline: none;
  border-color: var(--sage-400);
  box-shadow: var(--focus-ring);
}

/* A native <details>/<summary> disclosure - accessible (keyboard,
   aria-expanded) with no custom JS for the open/close mechanics.
   discovery_search.js lazily creates the Leaflet map only the first time
   this is opened. */
.advanced-search { max-width: 640px; margin: 0 auto 24px; }
.advanced-search summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 4px;
  font-weight: 600;
  color: var(--text-secondary);
  list-style: none;
}
.advanced-search summary::-webkit-details-marker { display: none; }
.advanced-search summary::before {
  content: "▸";
  display: inline-block;
  transition: transform .15s;
}
.advanced-search[open] summary::before { transform: rotate(90deg); }
.advanced-search summary:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }

.search-filters {
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  padding: 20px;
  margin-top: 8px;
}
.search-filters label { display: block; margin-bottom: 12px; }
/* This page's inputs/select were never actually styled before this pass
   (no <form> wrapper here for the existing generic `form input` rule to
   match, and no dedicated rule of its own either) - adding real styling
   while already restructuring this page rather than leaving it as a
   second, separate gap. */
.search-filters input,
.search-filters select {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 12px 14px;
  border: 1.5px solid var(--border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: inherit;
  background: var(--paper-100);
  transition: border-color .15s, background .15s;
}
.search-filters input:focus,
.search-filters select:focus {
  outline: none;
  border-color: var(--sage-400);
  background: var(--surface-raised);
}
.area-hint { font-size: 0.85rem; color: var(--ink-600); margin: 8px 0 6px; }
.search-map { height: 220px; border-radius: var(--radius-md); overflow: hidden; }
#use-my-location-btn { margin-bottom: 10px; }
#radius-field { margin-top: 10px; max-width: 260px; }

/* Mobile: minimalistic but practical */
@media (max-width: 640px) {
  .topnav { padding: calc(14px + env(safe-area-inset-top)) 16px 10px; }
  .topnav-right { gap: 8px; }
  .corn-balance { font-size: 0.8rem; }
  .profile-avatar-link { width: 28px; height: 28px; }
  .profile-avatar-fallback { font-size: 1rem; }
  .header-icon-btn { width: 28px; height: 28px; }
  /* Tier 3 (Achievements/Leaderboard) moves to the More sheet at this
     width, leaving 3 + Capture + 3 (issue #237); tier 2 (Upload/
     Victorina) follows under 520px for 2 + Capture + 2. */
  .toolbar {
    display: flex;
    flex-wrap: nowrap;
    justify-content: space-around;
    gap: 0;
    padding-top: var(--space-3);
    padding-bottom: var(--space-2);
  }
  /* Real bug found live (2026-10-11, direct owner report: "capture button
     now overlays Explore button"): this used to be `display: contents`,
     flattening both clusters into ONE flat flex row of 6 individual
     items (3 left + 3 right incl. the Capture spacer) laid out by
     .toolbar's own justify-content:space-around above. That only
     centers the Capture overlay correctly when the two SIDES have an
     EQUAL item count - .toolbar-item-capture's in-flow spacer is itself
     one of the 6 flattened items, so its true midpoint drifts toward
     whichever side has fewer items. Adding Upload made the left side
     (Feed/Upload/Explore) outnumber the right (Challenges/Victorina) 3
     to 2, and the drift was big enough (44px of a fixed 88px circle,
     confirmed via a real headless-browser measurement before and after)
     to visibly cover Explore's whole icon.
     Keeping each .toolbar-cluster as a REAL flex container (own
     space-around) instead of flattening it isolates each side's layout
     to its own half of the row, independent of how many items are
     visible on the OTHER side - confirmed via the same real-browser
     measurement this drops back to a few px, matching the small,
     pre-existing, unnoticed touch that existed even before Upload was
     added (this toolbar has never had quite enough room at the
     narrowest phone widths for a precisely zero-overlap circle, but a
     few px at the very edge was never reported as a problem the way 44px
     visibly covering an icon's label was). */
  .toolbar-cluster {
    display: flex;
    justify-content: space-around;
    /* Equal halves either side of a fixed-width Capture slot (issue
       #237): with the same item count on each side this puts Capture on
       the true center and gives every label the most room. Without
       flex:1 the clusters shrank to content and truncated labels. */
    flex: 1 1 0;
    min-width: 0;
  }
  /* Matches (rather than exceeds, via !important) the base rule's own
     :first-child/:last-child specificity (0,2,0) - a plain .toolbar-cluster
     override above is only (0,1,0) and would silently lose to those two
     more specific base rules without this. */
  .toolbar-cluster:first-child,
  .toolbar-cluster:last-child {
    justify-content: space-around;
  }
  .toolbar-item {
    flex: 1;
    min-width: 0;
    min-height: 44px;
    padding: 4px 2px;
    color: var(--text-secondary);
    border-bottom: none;
    border-radius: 0;
  }
  .toolbar-item-capture {
    flex: 0 0 5.5rem;
    min-width: 0;
    padding-top: 0;
  }
  .toolbar-item:hover, .toolbar-item.active {
    background: none;
    border-bottom: none;
    color: var(--accent-strong);
  }
  .toolbar-item.active .toolbar-label { color: var(--accent-strong); }
  .toolbar-item.toolbar-item-tier3 { display: none; }
  .toolbar-icon { font-size: 1.15rem; }
  .toolbar-label { font-size: 0.56rem; }
  .nav-icon { font-size: 1.15rem; }
  /* 2026-10-03 (explicit request), corrected 2026-10-04: the "icon only,
     no wordmark" mobile logo applies only while Capture mode's fullscreen
     overlay is actually active (same correction/reasoning as
     .topnav-top's identical rule above) - every other state, including
     the capture page's own normal Discovery-mode view, shows the full
     "Imagepedia" wordmark at every width, left-aligned. */
  body.capture-mode-active .brand span:last-child { display: none; }
  /* Same note as the desktop .page rule above: this top pad predates
     --capture-btn-hang: 0 (2026-10-11) and no longer needs to clear any
     protrusion, left as-is since the gap still reads fine. */
  .page { padding: 48px 14px 80px; }
  .site-footer { padding-bottom: var(--space-6); }
  .auth-card { padding: 28px 22px; }
  .capture-card { padding: 20px; }
  .hero-capture h1 { font-size: 1.5rem; }
  /* UX table row 23: decorative leaves always-on (previously just
     shrunk to 160px) overlapped chrome on mobile - hide entirely. */
  .leaf-decor { display: none; }
  .achievements-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
  .feed-stats-grid { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 8px; }
  .feed-stat-card { padding: 12px 6px 10px; }
  .feed-stat-icon { font-size: 1.25rem; }
  .victorina-options { gap: var(--space-2); }
  .victorina-option-btn { padding: var(--space-3); font-size: 0.92rem; }
  .feed-stat-value { font-size: 1.25rem; }
  .feed-stat-label { font-size: 0.6rem; }
  /* Four persona cards in the shared auto-fit grid wrap 3+1 at this
     width (.page's 14px padding leaves ~362px, and 4 columns of
     minmax(100px,1fr) don't fit) - the leftover single card reads as the
     grid's weakest point. Scoped to the persona page's own id so the
     feed's 5-card stats grid and the achievements grid are unaffected
     (design review #167/#168). */
  #persona-stats.feed-stats-grid { grid-template-columns: repeat(2, 1fr); }
  #persona-stats .feed-stat-label { font-size: 0.75rem; }
  /* This app's own mobile touch-target floor (.toolbar-item uses the same
     44px) - the tag chips and the profile-page Back link were both
     shorter than that (design review #167/#168). Scoped to #back-btn
     specifically, not the shared .link-btn, so other link-btn uses (e.g.
     comment reply links) don't grow. */
  .tag-filter-chip { min-height: 44px; padding: var(--space-2) var(--space-4); }
  #back-btn { min-height: 44px; padding: var(--space-2) var(--space-3); }
  /* Site-redesign PR 9, UX row 24: nowrap forces every admin table into
     wide horizontal scrolling on a narrow screen even for columns that
     would happily wrap onto 2 lines - allow wrapping under 640px instead
     (.admin-table-wrap's own overflow-x: auto still covers any column
     that genuinely can't wrap, e.g. a long unbroken email address). */
  .admin-table th, .admin-table td { white-space: normal; }
}

.status-queued, .status-uploading { background: var(--status-warning-bg); color: var(--status-warning-fg); }
.status-blocked_insufficient_corns, .status-blocked_session_expired { background: var(--status-caution-bg); color: var(--status-caution-fg); }
.status-failed_permanent, .status-failed_corrupted { background: var(--status-danger-bg); color: var(--danger); }

/* Feedback form image attachment (issue #235) */
.field-hint { margin: -0.5rem 0 0.75rem; font-size: 0.8rem; color: var(--text-secondary); }
.feedback-attachment-preview { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.feedback-attachment-preview[hidden] { display: none; }
.feedback-attachment-preview img { max-width: 120px; max-height: 120px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); object-fit: contain; }
