@import url('https://fonts.googleapis.com/css2?family=Anton&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');

:root {
  --asphalt:      #17181A;
  --asphalt-2:     #212226;
  --asphalt-3:     #2B2C31;
  --paper:         #E7E3D6;
  --paper-dim:     #B9B6AA;
  --ink:           #F4F2EA;
  --hazard:        #F2A900;
  --hazard-ink:    #1A1400;
  --alert:         #D6402C;
  --alert-dim:     #7A2A20;
  --clean:         #3F8F5F;
  --clean-dim:     #285A3B;
  --steel:         #A8AEB4;
  --border:        rgba(244,242,234,0.14);
  --radius: 2px;
  --font-display: 'Anton', sans-serif;
  --font-body: 'IBM Plex Sans', sans-serif;
  --font-mono: 'IBM Plex Mono', monospace;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--asphalt);
  color: var(--ink);
  font-family: var(--font-body);
  scroll-behavior: smooth;
}

body { min-height: 100vh; display: flex; flex-direction: column; }
main { flex: 1 0 auto; }
footer { flex-shrink: 0; }

a { color: inherit; }

h1, h2, h3 {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 1.02;
  margin: 0;
}

.eyebrow {
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.72rem;
  color: var(--hazard);
}

.mono { font-family: var(--font-mono); }

/* ---------- Hazard stripe motif ---------- */
.hazard-stripe {
  height: 10px;
  background: repeating-linear-gradient(
    135deg,
    var(--hazard),
    var(--hazard) 14px,
    var(--asphalt) 14px,
    var(--asphalt) 28px
  );
}

/* ---------- Interstitial ---------- */
#interstitial {
  position: fixed;
  inset: 0;
  /* Higher than Leaflet's own panes/controls (which sit around
     z-index 1000) so the map underneath can never bleed through the
     interstitial while it's up - Leaflet inits immediately on load,
     before the interstitial has been dismissed. */
  z-index: 9999;
  background: var(--asphalt);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 24px;
}
#interstitial .frame {
  border: 3px solid var(--hazard);
  padding: 48px 32px;
  max-width: 640px;
  position: relative;
}
#interstitial .frame::before,
#interstitial .frame::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 10px;
  background: repeating-linear-gradient(135deg, var(--hazard), var(--hazard) 14px, var(--asphalt) 14px, var(--asphalt) 28px);
}
#interstitial .frame::before { top: -13px; }
#interstitial .frame::after { bottom: -13px; }
#interstitial h1 {
  font-size: clamp(2.2rem, 7vw, 3.6rem);
  color: var(--ink);
  margin-bottom: 6px;
}
#interstitial h1 span { color: var(--alert); }
#interstitial p {
  font-family: var(--font-mono);
  color: var(--paper-dim);
  font-size: 1rem;
  margin: 18px 0 30px;
  letter-spacing: 0.02em;
}
#interstitial button {
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: var(--hazard);
  color: var(--hazard-ink);
  border: none;
  padding: 14px 28px;
  cursor: pointer;
  font-size: 0.85rem;
}
#interstitial button:hover { background: #ffbe2e; }
#interstitial .stamp {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--steel);
  margin-top: 22px;
  letter-spacing: 0.08em;
}

/* ---------- Header / nav ---------- */
header.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(23,24,26,0.92);
  backdrop-filter: blur(6px);
  border-bottom: 1px solid var(--border);
}
.nav-inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: 14px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  position: relative;
}
.brand {
  /* It's a <button> so the logo can act as a "back to map" link -
     reset all the default button chrome back to plain text. */
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: inherit;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: 1.15rem;
  letter-spacing: 0.01em;
}
.brand-mark { width: 20px; height: 20px; flex-shrink: 0; }
.brand .by { font-family: var(--font-mono); font-size: 0.62rem; color: var(--steel); text-transform: uppercase; letter-spacing: 0.1em; }

/* Nav + campaign CTA share one wrapper so they collapse into a single
   dropdown together on mobile, campaign CTA last so it lands at the
   bottom of that dropdown. */
.nav-menu {
  display: flex;
  align-items: center;
  gap: 20px;
}
nav.primary-nav { display: flex; gap: 4px; flex-wrap: wrap; }
nav.primary-nav button {
  background: none;
  border: none;
  color: var(--paper-dim);
  font-family: var(--font-mono);
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 8px 12px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
}
nav.primary-nav button:hover { color: var(--ink); }
nav.primary-nav button.active { color: var(--hazard); border-bottom-color: var(--hazard); }
.campaign-cta {
  background: var(--alert);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 9px 16px;
  text-decoration: none;
  white-space: nowrap;
}
.campaign-cta:hover { background: #ef4c34; }

/* Hamburger button - hidden on desktop */
.hamburger {
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  width: 40px;
  height: 40px;
  background: none;
  border: 1px solid var(--border);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.hamburger span {
  display: block;
  width: 20px;
  height: 2px;
  background: var(--ink);
  transition: transform 0.15s ease, opacity 0.15s ease;
}
.hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.hamburger.open span:nth-child(2) { opacity: 0; }
.hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

@media (max-width: 860px) {
  .hamburger { display: flex; }

  /* Closed by default: a plain dropdown panel under the header,
     only shown once .open is toggled on by the hamburger button. */
  .nav-menu {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--asphalt-2);
    border-bottom: 1px solid var(--border);
    padding: 6px 24px 20px;
  }
  .nav-menu.open { display: flex; }

  nav.primary-nav { flex-direction: column; gap: 0; }
  nav.primary-nav button {
    text-align: left;
    padding: 14px 4px;
    border-bottom: 1px solid var(--border);
  }

  /* Back the campaign sits last in the DOM, so it naturally lands at
     the bottom of the stacked mobile menu. */
  .campaign-cta {
    margin-top: 16px;
    text-align: center;
  }
}

/* ---------- Layout ---------- */
main { max-width: 1180px; margin: 0 auto; padding: 40px 24px 100px; }
.view { display: none; }
.view.active { display: block; }

.page-head { margin-bottom: 32px; }
.page-head h2 { font-size: clamp(1.8rem, 4vw, 2.6rem); color: var(--ink); }
/* ~3/4 of main's 1180px content width - was capped at 640px, which on
   a wide desktop left an odd-looking gap of empty space to the right
   of the intro paragraph on every page. The map page's page-head is
   the one exception (see .map-head-row below): it shares its row with
   the "Jump to a postcode" form, so it keeps the narrower width there
   instead of competing with the form for space. */
.page-head p { color: var(--paper-dim); max-width: 860px; margin-top: 10px; font-size: 0.98rem; }
/* A page-head that starts a new section partway down a page (e.g.
   "Get involved" and "Sponsoring a clean-up" on the campaign page,
   "Recent clean-ups" on the home page) rather than a view's very own
   top heading - a rule above it, same weight as the borders used
   elsewhere on the site, gives it a clean break from whatever content
   came before instead of just relying on margin to read as a new
   section. */
.page-head-divider { margin-top: 48px; padding-top: 40px; border-top: 1px solid var(--border); }

/* ---------- Hero (home) ---------- */
.hero {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 40px;
  align-items: center;
  padding: 26px 0 34px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 28px;
  position: relative;
}
@media (max-width: 860px) { .hero { grid-template-columns: 1fr; } }

/* Very subtle litter silhouette sitting along the bottom of the hero -
   screen blend-mode discards the image's near-black background and
   keeps only the lighter litter shapes, so it doesn't need to match
   --asphalt exactly; the low opacity on top of that is what keeps it
   reading as texture rather than a photo competing with the headline.
   Also shown on the stacked mobile hero, but dimmer (see the
   max-width:860px rule below) - the wide landscape crop gets zoomed
   in a lot more by object-fit:cover on that narrow tall layout (it
   ends up covering the whole text block rather than just a floor
   line), so it needs to be noticeably fainter there to read as
   texture rather than a murky patch behind the copy. The real content
   divs get position:relative + z-index so they stack above this layer
   - important that this selector doesn't also catch .hero-bg itself
   (see the :not()), or it'd override position:absolute below and
   collapse this layer to zero height. */
.hero-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.hero-bg img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center bottom;
  mix-blend-mode: screen;
  opacity: 0.2;
}
.hero > div:not(.hero-bg), .hero-stats-col { position: relative; z-index: 1; }
@media (max-width: 860px) { .hero-bg img { opacity: 0.1; } }

.hero h1 {
  font-size: clamp(2.4rem, 6vw, 4rem);
  color: var(--ink);
  /* Shrinks the H1's own box to its widest rendered line (it breaks
     across two lines via a <br> in the markup, and the two lines
     aren't the same width) rather than filling the whole grid column -
     doesn't change how it looks, but means JS can measure its real
     width to line the paragraph below up with it (see app.js). */
  width: fit-content;
  max-width: 100%;
}
.hero .red { color: var(--alert); }
.hero .lede-strong { color: var(--ink); }
.hero p.lede { font-size: 1.05rem; color: var(--paper-dim); margin: 18px 0 0; }

/* Stats + report buttons live in the slot the stock photo used to
   occupy. The three stats stay side by side (number over its own
   label) even on narrow screens - only the buttons below them stack
   full-width. */
.hero-stats-col {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 22px;
}
/* Evenly spaced so the rightmost stat's right edge lines up with the
   right edge of the buttons below (both span the full column width),
   and each number sits centered above its own label rather than
   flush to the label's left edge. */
.hero-stats-col .stat-row { flex-direction: row; flex-wrap: nowrap; justify-content: space-between; gap: 10px; margin-top: 0; }
.hero-stats-col .stat-row .stat { text-align: center; }
.hero-stats-col .btn-row { flex-direction: column; }
.hero-stats-col .btn-row .btn { width: 100%; text-align: center; }

.stat-row { display: flex; gap: 28px; margin-top: 26px; flex-wrap: wrap; }
.stat { font-family: var(--font-mono); }
.stat .num { font-size: 1.8rem; font-weight: 600; color: var(--hazard); display: block; }
.stat .label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--steel); }
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 28px; }
.btn {
  font-family: var(--font-mono);
  text-transform: uppercase;
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  padding: 13px 22px;
  border: 1px solid var(--hazard);
  color: var(--hazard);
  background: none;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
.btn:hover { background: var(--hazard); color: var(--hazard-ink); }
.btn.solid { background: var(--hazard); color: var(--hazard-ink); }
.btn.solid:hover { background: #ffbe2e; }
.btn.alert { border-color: var(--alert); color: var(--alert); }
.btn.alert:hover { background: var(--alert); color: var(--ink); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn:disabled:hover { background: none; color: var(--hazard); }
.btn.solid:disabled:hover { background: var(--hazard); color: var(--hazard-ink); }

/* A plain hyperlink for secondary actions like "Use my location" -
   same accent colour as .btn but without the button chrome. */
.text-link {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.05em;
  color: var(--hazard);
  text-decoration: underline;
  cursor: pointer;
}
.text-link:hover { color: #ffbe2e; }

/* Same colour/underline treatment as .text-link, but keeps whatever
   font it's sitting in rather than switching to mono - for a link
   sitting inline inside a normal paragraph (see the map page-head in
   index.html), where switching fonts mid-sentence would look odd. */
.inline-link { color: var(--hazard); text-decoration: underline; cursor: pointer; }
.inline-link:hover { color: #ffbe2e; }

/* ---------- Map ---------- */
/* Deliberately not id="map" - that collided with the "#map" URL
   fragment used for view routing (see js/app.js), and browsers will
   still natively scroll to an element whose id matches the current
   URL fragment even when the fragment was set via history.replaceState,
   which was cutting the top of the page off under the sticky header
   whenever you navigated to the map view. */
/* Page-head + "jump to a postcode" form side by side on desktop (same
   3fr/1fr split as .hero) so the form doesn't strand itself on the
   left with empty space to its right - collapses to stacked below
   860px like everything else. */
.map-head-row {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 40px;
  /* Bottom-aligned rather than top-aligned: the description text runs
     longer than the form, so top-aligning left the form stranded up
     near the heading with a gap of empty space below it before the
     map actually started. Bottom-aligning drops it down flush with
     the map's top edge instead. */
  align-items: end;
  margin-bottom: 32px;
}
@media (max-width: 860px) { .map-head-row { grid-template-columns: 1fr; gap: 20px; } }
.map-postcode-form { margin-bottom: 0; }
/* Keeps the map page-head's paragraph at the original, narrower width
   rather than the site-wide ~860px (see .page-head p above) - this is
   the one page-head that already shares its row with something else
   (the postcode form), so it doesn't have the same empty-space issue
   the other pages had. */
.map-head-row .page-head p { max-width: 640px; }

#hotspot-map {
  height: 520px;
  width: 100%;
  border: 1px solid var(--border);
  background: var(--asphalt-2);
}
/* The CartoDB dark basemap's roads are a low-contrast grey on a
   near-black background - fine on a dim screen, but washes out
   completely on a phone or laptop in daylight. Brightening/adding
   contrast just to the tile layer (not the pins, halos or legend on
   top of it) makes the road lines themselves stand out more without
   turning the whole map a different colour. Applies to both maps -
   the upload page's map uses the same basemap. */
#hotspot-map .leaflet-tile-pane,
#upload-map .leaflet-tile-pane {
  filter: brightness(1.45) contrast(1.2);
}
.map-legend {
  display: flex;
  gap: 18px;
  margin-top: 12px;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--paper-dim);
  flex-wrap: wrap;
}
.map-legend span { display: flex; align-items: center; gap: 6px; }
.swatch { width: 10px; height: 10px; display: inline-block; }

/* ---------- Cards / lists ---------- */
.card {
  background: var(--asphalt-2);
  border: 1px solid var(--border);
  padding: 20px;
}
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
@media (max-width: 780px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }

/* Search & Fix photo grid opens a fullscreen lightbox on click, but
   only above the width where the grid drops to one column (see
   js/search.js' lightboxEnabled/LIGHTBOX_MIN_WIDTH, kept in step with
   the breakpoint just above) - below that a card's photo already
   spans the full screen on its own, so there's nothing to open. */
@media (min-width: 781px) {
  #search-results .photo-card img[data-lightbox-id] { cursor: pointer; }
}

.select-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 12px 14px;
  margin-bottom: 16px;
  border: 1px solid var(--border);
  background: var(--asphalt-2);
  font-family: var(--font-mono);
  font-size: 0.78rem;
}
.select-bar .select-actions { display: flex; align-items: center; gap: 12px; }
.select-bar #select-count { color: var(--steel); }

.fix-panel { margin-bottom: 20px; }
.fix-panel p { margin: 0 0 12px; color: var(--paper-dim); font-size: 0.95rem; }
.fix-panel .personal-info-hint { color: var(--alert); font-size: 0.8rem; margin-top: -4px; }
.fix-panel textarea { margin-top: 12px; }
.fix-panel .row { display: flex; gap: 12px; align-items: center; margin-top: 14px; flex-wrap: wrap; }
/* Small opt-in checkbox style, styled as a quiet aside rather than a
   normal form field - used by the campaign form's "I'm interested in"
   ticks (see index.html). (Previously also used by the fix panel's
   "No photo for this one" exception, which has since been removed -
   an after-photo is mandatory for every fix now.) */
.exception-checkbox {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: none;
  letter-spacing: 0;
  color: var(--steel);
  cursor: pointer;
}
.exception-checkbox input { width: auto; margin: 0; cursor: pointer; }
input[type=file]:disabled { opacity: 0.4; cursor: not-allowed; }

/* ---------- Per-report fix photo slots ----------
   Fixing one or several reports at once (see fixSlotHtml/showFixPanel
   in js/search.js) gives each report its own tap-to-pick, tap-to-flip
   photo slot instead of one shared file input - the whole point is a
   guaranteed 1:1 pairing between one "before" report and one "after"
   photo, so there's never a shared pool of files that could end up
   matched to the wrong spot. */
.fix-slots-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 14px;
  margin: 4px 0 18px;
}
.fix-slot { background: var(--asphalt-3); border: 1px solid var(--border); padding: 10px; }
.fix-slot .flip-stage { cursor: pointer; }
.fix-slot-loc {
  font-family: var(--font-mono); font-size: 0.62rem; color: var(--paper-dim);
  text-transform: uppercase; letter-spacing: 0.03em;
  margin: 8px 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fix-slot-pick { width: 100%; margin-top: 8px; padding: 7px 8px; font-size: 0.66rem; }
.fix-slot.has-photo .fix-slot-pick { border-color: var(--clean); color: var(--clean); }

.photo-card { background: var(--asphalt-2); border: 1px solid var(--border); overflow: hidden; position: relative; }
/* 3:4 portrait rather than a short landscape strip - photos are asked
   to be taken in portrait (capture="environment" on a phone defaults
   to that), and a fixed short landscape box was cropping most of a
   tall portrait photo away to fill it. Applies to a plain photo, the
   no-photo placeholder, and the before/after flip stage below, so all
   three keep the same card height regardless of which one a given
   card happens to show. */
.photo-card img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; display: block; background: var(--asphalt-3); }
.photo-placeholder {
  width: 100%; aspect-ratio: 3 / 4;
  display: flex; align-items: center; justify-content: center;
  background: var(--asphalt-3); color: var(--steel);
  font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em;
}
.select-checkbox {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 2;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10,10,11,0.7);
  border: 1px solid var(--border);
}
.select-checkbox input { width: 16px; height: 16px; margin: 0; cursor: pointer; }
.photo-card .meta { padding: 12px 14px 16px; }
/* Location text + the "show on map" pin button share one row - .loc
   is the flexible side (truncated with an ellipsis rather than
   wrapping, so a long street name can't grow the tile), the button is
   a fixed-size icon that never gets squeezed. */
.photo-card .meta .loc-row { display: flex; align-items: center; gap: 8px; }
.photo-card .meta .loc {
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--hazard);
  text-transform: uppercase; letter-spacing: 0.04em;
  flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Small icon-only button, distinct from the text-link style buttons
   elsewhere on the card (flag-link, vote-btns) since it needs to stay
   compact and recognisable at a glance rather than reading as prose -
   see window.showReportOnMap in js/map.js. */
.map-pin-btn {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  background: none;
  color: var(--steel);
  cursor: pointer;
  padding: 0;
}
.map-pin-btn:hover { color: var(--hazard); border-color: var(--hazard); }
.map-pin-btn svg { width: 13px; height: 13px; }
/* Category + postcode sit together as small compact tags rather than
   running text, so adding the postcode alongside the street name
   above doesn't cost the tile any real extra height - see
   .category-pill / .postcode-pill below. */
.pill-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 4px 0 8px; }
.photo-card .meta .cap { font-size: 0.88rem; color: var(--paper-dim); margin: 6px 0 10px; min-height: 1em; }
.photo-card .meta .row { display: flex; justify-content: space-between; align-items: center; font-family: var(--font-mono); font-size: 0.7rem; color: var(--steel); }
.vote-btns { display: flex; gap: 8px; align-items: center; }
.vote-btns button {
  background: none; border: 1px solid var(--border); color: var(--paper-dim);
  font-family: var(--font-mono); font-size: 0.72rem; padding: 4px 9px; cursor: pointer;
}
.vote-btns button:hover:not(:disabled) { border-color: var(--hazard); color: var(--hazard); }
.vote-btns button:disabled { opacity: 0.4; cursor: default; }
.flag-link { font-family: var(--font-mono); font-size: 0.65rem; color: var(--steel); background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0; }
.flag-link:hover { color: var(--alert); }
.status-badge { font-family: var(--font-mono); font-size: 0.62rem; text-transform: uppercase; padding: 2px 7px; letter-spacing: 0.05em; }
.status-badge.active { background: var(--alert-dim); color: #ffb3a3; }
.status-badge.resolved { background: var(--clean-dim); color: #a9e3bd; }
.fixed-by { font-family: var(--font-mono); font-size: 0.72rem; color: var(--clean); margin: -2px 0 10px; }
.sponsor-credit { font-family: var(--font-mono); font-size: 0.72rem; color: var(--hazard); margin: -2px 0 10px; }
.category-pill {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--steel);
  border: 1px solid var(--border);
  padding: 2px 7px;
}
/* Shown alongside the category pill only when a report has a postcode
   on top of (or instead of) a street name - see cardHtml/
   beforeAfterCardHtml - so the full location is visible without
   costing the .loc line above any extra width or wrapping. */
.postcode-pill {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  letter-spacing: 0.04em;
  color: var(--paper-dim);
  border: 1px solid var(--border);
  padding: 2px 7px;
}
/* How long a spot sat reported before it got cleaned up - sits in the
   bottom-right corner of a before/after card's own .row (see
   beforeAfterCardHtml in js/util.js), which already lays its children
   out with justify-content:space-between, so this lands at the far
   right of the box for free, next to the "Fixed ... ago" text on the
   left, rather than overlaying the photo itself. */
.days-pill {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--paper-dim);
  border: 1px solid var(--border);
  padding: 2px 7px;
}

/* ---------- Before & after flip card ----------
   A resolved report that's linked back to its own original "before"
   photo (see beforeAfterCardHtml / the pairing pass in
   renderReportCards, js/search.js) gets this instead of a plain
   photo-card: a simple 3D flip between the two photos, toggled
   automatically every 2 seconds for as long as Search your street
   stays the visible view (see startFlip in js/search.js) - a
   continuous, hands-free before/after effect without generating an
   actual animated .gif file, so it loads instantly and needs no extra
   library. Sits inside a .photo-card exactly like a normal card, just
   with this in place of a plain <img>. */
.flip-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 4;
  perspective: 1200px;
}
.flip-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transition: transform 0.7s cubic-bezier(0.4, 0.2, 0.2, 1);
  transform-style: preserve-3d;
}
.flip-stage.flipped .flip-inner { transform: rotateY(180deg); }
.flip-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.flip-face img { width: 100%; height: 100%; object-fit: cover; display: block; background: var(--asphalt-3); }
.flip-face.flip-back { transform: rotateY(180deg); }
.flip-tag {
  position: absolute;
  top: 10px;
  left: 10px;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 3px 8px;
  background: rgba(23,24,26,0.82);
}
.flip-tag.before { color: #ffcfc4; }
.flip-tag.after { color: #a9e3bd; }
/* Optional business logo badge - sits over a photo's top-right corner
   (the "Before"/"After" tag already owns the top-left, see .flip-tag
   above). Used both on the after-face of a flip card and on a plain
   resolved photo-card (see js/util.js and js/search.js) - the
   ".photo-card img" rule above sizes every <img> in a card to fill it,
   so this overrides width/aspect-ratio/object-fit back down to a small
   badge, and the class-count wins the specificity fight regardless of
   source order. A light background behind it keeps a logo with a
   transparent background (light or dark) legible over any photo. */
.photo-card .sponsor-logo-badge {
  position: absolute;
  top: 10px;
  right: 10px;
  width: auto;
  height: auto;
  max-width: 95px;
  max-height: 45px;
  aspect-ratio: auto;
  object-fit: contain;
  background: rgba(255,255,255,0.92);
  border-radius: 4px;
  padding: 5px 7px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.35);
}
/* Respect a visitor's reduced-motion preference - swap the continuous
   flip for a plain crossfade instead of turning it off outright, so
   the before/after comparison itself is still visible either way. */
@media (prefers-reduced-motion: reduce) {
  .flip-inner { transition: opacity 0.7s ease; transform: none !important; }
  .flip-stage.flipped .flip-inner { transform: none; }
  .flip-face.flip-back { transform: none; opacity: 0; }
  .flip-stage.flipped .flip-face.flip-front { opacity: 0; }
  .flip-stage.flipped .flip-face.flip-back { opacity: 1; }
  .flip-face { transition: opacity 0.7s ease; opacity: 1; }
}

/* ---------- Leaderboard ---------- */
.rank-row {
  display: grid;
  grid-template-columns: 44px 1fr auto auto;
  align-items: center;
  gap: 14px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  margin-bottom: 8px;
  background: var(--asphalt-2);
}
.rank-row .rank { font-family: var(--font-display); font-size: 1.4rem; color: var(--steel); }
.rank-row.sev-red .rank { color: var(--alert); }
.rank-row.sev-amber .rank { color: var(--hazard); }
.rank-row.sev-green .rank { color: var(--clean); }
.rank-row .area { font-family: var(--font-mono); font-size: 0.95rem; }
.rank-row .count { font-family: var(--font-mono); font-size: 0.8rem; color: var(--paper-dim); text-align: right; }
.severity-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 8px; }

/* ---------- Forms ----------
   Not scoped to `form` - the upload postcode field now lives outside
   its <form> (moved next to the map, see index.html) but still needs
   to look and space like every other field. */
.field { margin-bottom: 18px; }
label { display: block; font-family: var(--font-mono); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--steel); margin-bottom: 7px; }
input[type=text], input[type=email], input[type=search], textarea, input[type=file], select {
  width: 100%;
  background: var(--asphalt-3);
  border: 1px solid var(--border);
  color: var(--ink);
  padding: 11px 12px;
  font-family: var(--font-body);
  font-size: 1rem;
}
textarea { resize: vertical; min-height: 90px; }
input:focus, textarea:focus, select:focus { outline: 2px solid var(--hazard); outline-offset: 1px; }
.hint { font-family: var(--font-mono); font-size: 0.82rem; color: var(--steel); margin-top: 6px; }
.form-msg { font-family: var(--font-mono); font-size: 0.88rem; margin-top: 14px; padding: 10px 12px; display: none; }
.form-msg.show { display: block; }
.form-msg.ok { background: var(--clean-dim); color: #c3f0d2; }
.form-msg.err { background: var(--alert-dim); color: #ffcfc4; }

/* ---------- Calendar list ---------- */
.event-row { display: flex; gap: 18px; padding: 16px; border: 1px solid var(--border); margin-bottom: 10px; align-items: flex-start; flex-wrap: wrap; }
.event-date { font-family: var(--font-mono); background: var(--hazard); color: var(--hazard-ink); padding: 8px 12px; text-align: center; min-width: 74px; }
.event-date .d { font-size: 1.3rem; font-weight: 700; display: block; line-height: 1; }
.event-date .m { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em; }
.event-body { flex: 1; min-width: 200px; }
.event-body h3 { font-family: var(--font-body); font-weight: 600; text-transform: none; font-size: 1.02rem; }
.event-body p { color: var(--paper-dim); font-size: 0.88rem; margin: 6px 0 0; }
.event-body .when { font-family: var(--font-mono); font-size: 0.75rem; color: var(--hazard); margin-top: 6px; }
/* Pushed to the right with margin-left:auto (rather than relying on
   .event-row's own justify-content, which would also space out
   .event-date/.event-body) and centred vertically within the row
   regardless of how tall .event-body's text ends up - this is what
   fills the empty space next to a short event description now that
   the event list is full-width on desktop (see .page-head p above -
   this section stopped sharing a column with anything once
   "Sponsoring a clean-up" got pulled out into its own section). Wraps
   onto its own line below the rest on narrow screens (see
   .event-row's flex-wrap) rather than squeezing everything into one
   cramped row. */
.event-rsvp { margin-left: auto; align-self: center; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; }
.rsvp-btn { padding: 8px 16px; font-size: 0.7rem; white-space: nowrap; }
.rsvp-btn:disabled { opacity: 0.7; cursor: default; border-color: var(--clean); color: var(--clean); }
.rsvp-count { font-family: var(--font-mono); font-size: 0.68rem; color: var(--steel); white-space: nowrap; }

/* ---------- Campaign banner ---------- */
.campaign-banner {
  background: var(--alert-dim);
  border: 1px solid var(--alert);
  padding: 24px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  margin-bottom: 40px;
  flex-wrap: wrap;
}
/* Capped so a wide desktop can't stretch this to a single very long
   line right up against the button - short banner copy like this
   reads better wrapped over two lines than spanning the full box. */
.campaign-banner p { margin: 0; max-width: 560px; font-family: var(--font-mono); font-size: 0.85rem; color: var(--ink); }
.campaign-banner .btn { color: var(--ink); }
.campaign-banner .btn:hover { color: var(--ink); }

/* ---------- Footer ---------- */
footer {
  border-top: 1px solid var(--border);
  padding: 30px 24px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--steel);
}
.footer-links {
  margin-top: 12px;
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
}
.footer-links a { text-decoration: underline; color: var(--steel); }
.footer-links a:hover { color: var(--hazard); }

/* ---------- Legal pages (privacy / terms) ---------- */
.legal-page { max-width: 720px; }
.legal-page h3 {
  font-family: var(--font-body);
  font-weight: 600;
  text-transform: none;
  font-size: 1.05rem;
  color: var(--ink);
  margin: 30px 0 8px;
}
.legal-page h3:first-child { margin-top: 0; }
.legal-page p {
  color: var(--paper-dim);
  font-size: 0.95rem;
  line-height: 1.6;
  margin: 0 0 10px;
}

/* ---------- Who's responsible ---------- */
.responsible-list { display: flex; flex-direction: column; gap: 16px; max-width: 760px; }
.responsible-card h3 {
  font-family: var(--font-body);
  font-weight: 600;
  text-transform: none;
  font-size: 1.05rem;
  color: var(--ink);
  margin-bottom: 6px;
}
.responsible-who {
  font-family: var(--font-mono);
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--hazard);
  margin: 0 0 10px;
}
.responsible-card p:not(.responsible-who) { color: var(--paper-dim); font-size: 0.92rem; line-height: 1.6; margin: 0 0 16px; }
.responsible-card .btn { padding: 9px 16px; font-size: 0.72rem; }

/* ---------- Empty state ---------- */
.empty-state { font-family: var(--font-mono); color: var(--steel); font-size: 0.85rem; padding: 30px 0; text-align: center; }

/* ---------- Admin ---------- */
/* Mod page sub-navigation - Photo review / Sponsorship messages /
   Clean-up events. Reuses the same look as the site's main nav
   (nav.primary-nav in the header) rather than inventing a second tab
   style, just scoped to the admin panel instead of the header. */
.admin-subnav { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 26px; border-bottom: 1px solid var(--border); }
.admin-subnav button {
  background: none;
  border: none;
  color: var(--paper-dim);
  font-family: var(--font-mono);
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 10px 14px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.admin-subnav button:hover { color: var(--ink); }
.admin-subnav button.active { color: var(--hazard); border-bottom-color: var(--hazard); }
.admin-page { display: none; }
.admin-page.active { display: block; }

/* Small on/off switch - used for the "Allow RSVPs" toggle on the
   Clean-up events admin page (both the add form and each event row's
   quick switch). A real checkbox under the hood (keyboard/screen
   reader friendly), just repainted as a sliding track+knob rather
   than the default browser checkbox. */
.toggle-switch { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--steel); cursor: pointer; }
.toggle-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.toggle-switch .track { position: relative; width: 34px; height: 18px; background: var(--asphalt-3); border: 1px solid var(--border); flex-shrink: 0; transition: background 0.15s ease, border-color 0.15s ease; }
.toggle-switch .track::after { content: ""; position: absolute; top: 1px; left: 1px; width: 14px; height: 14px; background: var(--paper-dim); transition: transform 0.15s ease, background 0.15s ease; }
.toggle-switch input:checked + .track { background: var(--clean-dim); border-color: var(--clean); }
.toggle-switch input:checked + .track::after { transform: translateX(16px); background: var(--clean); }
.toggle-switch input:focus-visible + .track { outline: 2px solid var(--hazard); outline-offset: 2px; }

/* flex-wrap so the action buttons drop to their own line instead of
   forcing the row wider than the screen - at narrow widths a fixed
   160px photo, the location/status text, and two buttons all on one
   unbreakable line don't fit, which was causing the whole page to
   scroll sideways on mobile. */
.admin-row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; padding: 12px; border: 1px solid var(--border); margin-bottom: 8px; }
.admin-row img { width: 160px; height: 160px; object-fit: cover; flex-shrink: 0; }
.admin-row-no-photo {
  width: 160px; height: 160px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--asphalt-3); color: var(--steel);
  font-family: var(--font-mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em;
}
.admin-row .info { flex: 1; min-width: 180px; font-family: var(--font-mono); font-size: 0.75rem; color: var(--paper-dim); }
.fix-target { display: flex; align-items: center; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
.fix-target img { width: 44px; height: 44px; object-fit: cover; flex-shrink: 0; border: 1px solid var(--border); }
.fix-target-more { color: var(--steel); font-size: 0.7rem; }
.admin-row .actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* Inline edit form (postcode / street / description) swapped into a
   row's .info slot by the "Edit" action - see startEditRow in
   js/admin.js. Deliberately more compact than the site-wide .field
   spacing (18px margins, 1rem inputs) since it has to sit comfortably
   inside a single admin row rather than a whole page. */
.edit-form label { font-size: 0.66rem; margin-bottom: 4px; }
.edit-form input[type=text], .edit-form textarea, .edit-form input[type=file], .edit-form input[type=datetime-local] {
  padding: 8px 10px;
  font-size: 0.85rem;
  margin-bottom: 10px;
}
.edit-form textarea { min-height: 56px; }
.edit-form .form-msg { margin-top: 8px; padding: 6px 10px; font-size: 0.78rem; }
.sponsor-logo-preview { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.sponsor-logo-preview img { width: 44px; height: 44px; object-fit: contain; background: rgba(255,255,255,0.92); border-radius: 4px; padding: 4px; }
.sponsor-logo-preview .exception-checkbox { margin-top: 0; }

/* ---------- Photo lightbox ----------
   Clicking a map pin opens this instead of a small popup - a
   fullscreen viewer with prev/next arrows so several photos at the
   same postcode (from different people) can be flicked through, most
   recent first. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 6000;
  background: rgba(10,10,11,0.94);
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.lightbox.open { display: flex; }
.lightbox-stage {
  position: relative;
  max-width: min(920px, 92vw);
  max-height: 88vh;
  width: 100%;
}
.lightbox-stage img {
  display: block;
  width: 100%;
  max-height: 88vh;
  object-fit: contain;
  background: var(--asphalt-2);
}
.lightbox-topbar {
  position: absolute;
  top: 0; left: 0; right: 0;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 16px;
  background: linear-gradient(to bottom, rgba(10,10,11,0.85), transparent);
}
.lightbox-topbar-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  flex-shrink: 0;
}
.lightbox-fixed-btn {
  padding: 9px 16px;
  font-size: 0.72rem;
  white-space: nowrap;
}
.lightbox-loc {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--hazard);
}
.lightbox-cap {
  font-size: 0.88rem;
  color: var(--paper);
  margin-top: 4px;
  max-width: 60vw;
}
.lightbox-date {
  position: absolute;
  bottom: 12px;
  right: 12px;
  background: rgba(23,24,26,0.82);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  padding: 5px 10px;
  letter-spacing: 0.04em;
}
.lightbox-counter {
  position: absolute;
  bottom: 12px;
  left: 12px;
  background: rgba(23,24,26,0.82);
  color: var(--paper-dim);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  padding: 5px 10px;
  letter-spacing: 0.04em;
}
.lightbox-close {
  position: absolute;
  top: 18px;
  right: 22px;
  background: none;
  border: none;
  color: var(--ink);
  font-size: 2rem;
  line-height: 1;
  cursor: pointer;
  padding: 6px;
}
.lightbox-nav {
  background: rgba(23,24,26,0.7);
  border: 1px solid var(--border);
  color: var(--ink);
  font-size: 2rem;
  line-height: 1;
  width: 52px;
  height: 52px;
  flex-shrink: 0;
  cursor: pointer;
  margin: 0 8px;
}
.lightbox-nav:hover { background: var(--hazard); color: var(--hazard-ink); border-color: var(--hazard); }
@media (max-width: 640px) {
  .lightbox-nav { width: 40px; height: 40px; font-size: 1.5rem; margin: 0 2px; }
  .lightbox-close { top: 10px; right: 12px; }
  .lightbox-cap { max-width: 100%; }
}
