/* SquatAdvisor — design tokens and layout.
   Mobile-first throughout: the core use case is someone on a pavement who needs an
   answer in under fifteen seconds, on a phone, probably one-handed, possibly in a hurry.
   Nothing here blocks on JavaScript. */

:root {
  /* **Declared, because its absence lets a phone restyle our form controls.**
     Not reproducible in Chromium at any width or colour scheme, but the symptom on a real
     handset was exact: on /business every currency chip rendered as an empty white box except
     the selected one — and the selected one is the only chip with an explicit `color`. That is
     what a user-agent substituting its own dark-mode text colour looks like, and with no
     `color-scheme` declared a UA is entitled to do it. The palette below is hardcoded light, so
     saying so is honest rather than a workaround. */
  color-scheme: light;
  --navy: #12285a;
  --blue: #1a6dff;
  --blue-dark: #0d4fd6;
  --green: #22aa4b;
  --amber: #f5a623;
  --paper: #f7f9fc;
  --line: #dfe6f0;

  --ink: #17203a;
  --ink-soft: #5a6785;
  --white: #ffffff;

  --radius: 12px;
  --radius-sm: 8px;
  --gap: 16px;
  --wrap: 1100px;

  --shadow-sm: 0 1px 2px rgba(18, 40, 90, .08), 0 1px 3px rgba(18, 40, 90, .06);
  --shadow-md: 0 2px 8px rgba(18, 40, 90, .10);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
          Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 17px;              /* readable at arm's length outdoors */
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
}

h1, h2, h3 { line-height: 1.2; margin: 0 0 .4em; color: var(--navy); }
h1 { font-size: 1.65rem; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.05rem; }

a { color: var(--blue-dark); }
img { max-width: 100%; height: auto; }

.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 var(--gap); }
.section { padding: 28px var(--gap); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--navy); color: #fff; padding: 10px 16px;
}
.skip-link:focus { left: 0; }

:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }

/* ---------- header ---------- */

.site-header { background: var(--navy); color: #fff; }

/* Mobile: brand and account on line one, search full-width on line two. Sharing one row
   pushed the auth links into a wrap on narrow screens. */
.header-inner { padding-top: 10px; padding-bottom: 10px; }
.header-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; min-width: 0;
}

.brand { display: flex; align-items: center; gap: 8px; color: #fff; text-decoration: none; }
.brand-mark { width: 30px; height: 30px; flex: none; }
.brand-text { font-size: 1.15rem; letter-spacing: -.01em; }
.brand-text strong { font-weight: 700; }

.header-search { display: flex; gap: 8px; width: 100%; min-width: 0; margin-top: 10px; }
.header-search input {
  flex: 1 1 auto; min-width: 0;
  padding: 9px 12px; font: inherit; font-size: 16px;   /* 16px stops iOS zooming on focus */
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, .14); color: #fff;
}
.header-search input::placeholder { color: rgba(255, 255, 255, .7); }
.header-search input:focus { background: #fff; color: var(--ink); }
.header-search button {
  padding: 9px 14px; font: inherit; font-weight: 600; cursor: pointer;
  border: 0; border-radius: var(--radius-sm); background: var(--blue); color: #fff;
}
.header-search button:hover { background: var(--blue-dark); }

/* Rigid on purpose. Everything in here is a control with a fixed size, and the only
   flexible thing on the row is the search field — so when the row runs out of width the
   search box narrows and the account block stays exactly as it is. Nothing wraps. */
.header-account {
  display: flex; align-items: center; gap: 10px;
  flex: none; white-space: nowrap;
}

/* Handle and sign-out side by side, on one line, always. */
.header-user { display: flex; flex-direction: row; align-items: center; gap: 8px; white-space: nowrap; }
.header-user-link { display: flex; align-items: center; gap: 8px; text-decoration: none; }
.header-user-link:hover .header-handle { text-decoration: underline; }

/* The initial, shown instead of the handle on narrow screens. Sized like every other
   control on the row so the line does not change height when it swaps in. */
.header-avatar {
  display: grid; place-items: center; flex: none;
  width: 32px; height: 32px; border-radius: 50%;
  background: rgba(255, 255, 255, .18); color: #fff;
  font-size: .95rem; font-weight: 700; line-height: 1;
}

.header-handle {
  display: none;                       /* revealed at 720px, where there is room for it */
  font-size: .9rem; font-weight: 600; color: #fff;
  white-space: nowrap;
  max-width: 22ch;                     /* a generated handle is about 19 characters */
}
/* The moderator's door. Amber rather than white so it reads as a different kind of link
   from "your contributions" — it leads to other people's work, not your own.

   **Icon only at every width, like the Properties link beside it.** It used to spell itself
   out above 720px, which left two adjacent doors to two dashboards with one wearing a word and
   the other not — an asymmetry that reads as an oversight rather than a decision. Its name is
   visually-hidden text for the same reason as its neighbour's: a control named only above a
   breakpoint is unnamed below one. Both icons are 20px so the row does not look ragged. */
.header-admin {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 40px; padding: 8px 4px;
  text-decoration: none; color: var(--amber);
}
.header-admin:hover { opacity: .8; }
.header-admin .icon { width: 20px; height: 20px; flex: none; }

/* **The icon has to be sized here, because there is no bare `.icon` rule.** Every use in this
   sheet gives it a width in its own context, and this one never did — so the SVG was 0x0 and the
   word beside it was the only thing giving the link any size at all. Dropping the word without
   this would have left a link 0px wide: present in the DOM, named for a screen reader, and
   impossible to click. Measured, not assumed. */
.header-business {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 40px; padding: 8px 4px;   /* the row's tap target, like its siblings */
  color: #fff; text-decoration: none;
}
.header-business .icon { width: 20px; height: 20px; flex: none; }
.header-business:hover { opacity: .8; }

.header-signin { color: #fff; font-weight: 600; text-decoration: none; padding: 8px 4px; }
.header-signin:hover { text-decoration: underline; }
.signout-form { margin: 0; }
/* **Styled for the navy header, and used on white pages.** The colours here were light-blue on
   navy hovering to white, which is right in the header and invisible everywhere else: "Change PIN"
   and "Switch off — they have left" turned white on a white background at the moment somebody
   pointed at them.

   So the base is a link on paper, and the header keeps its own. Scoped by where it sits rather
   than by adding a modifier class, because every existing use is one or the other and a modifier
   would have to be remembered at each new one. */
.link-button {
  background: none; border: 0; padding: 8px 4px; cursor: pointer;
  font: inherit; font-size: .9rem; color: var(--blue); text-decoration: underline;
}
.link-button:hover { color: var(--blue-dark); }

.site-header .link-button { color: #9dc2ff; }
.site-header .link-button:hover { color: #fff; }
/* The icon sits inside the link-styled button, so it must not inherit the underline. */
.signout-button { display: inline-flex; align-items: center; gap: 6px; }
.signout-button .signout-icon { text-decoration: none; }
.signout-icon { display: none; }

/* ---------- country / language picker ---------- */

/* The header must not change height when the panel opens. The panel is taken out of flow
   and anchored under the button; the header gets a stacking context so the overlay sits
   above the page rather than being clipped by anything below it. */
.site-header { position: relative; z-index: 50; }

.picker { position: relative; flex: none; }
.picker-button {
  display: flex; align-items: center;
  min-height: 40px; padding: 6px; cursor: pointer;
  border-radius: var(--radius-sm);
  list-style: none;                      /* no disclosure triangle... */
}
.picker-button::-webkit-details-marker { display: none; }   /* ...including Safari's */
.picker-button::marker { content: ''; }
.picker-button:hover { background: rgba(255, 255, 255, .12); }
.picker[open] .picker-button { background: rgba(255, 255, 255, .18); }

.picker-flag { display: block; line-height: 0; }
.picker-flag .flag {
  width: 26px; height: 18px; display: block;
  border-radius: 2px; box-shadow: 0 0 0 1px rgba(255, 255, 255, .35);
}

.picker-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  width: min(19rem, calc(100vw - 32px));
  max-height: min(70vh, 460px); overflow-y: auto;
  background: #fff; color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(18, 40, 90, .22);
  padding: 12px 14px;
}

/* **On a phone the panel is a sheet, not a dropdown.** Anchored with `right: 0` to a 38px button
   sitting near the right of the header, a 304px panel starts at about x=-38 on a 375px screen —
   its left edge and the first characters of every line are off the display. Which is what "the
   line is clipped" actually was: nothing in the CSS was truncating it, the whole panel had walked
   off the edge.
   Fixed to the viewport instead, so its position cannot depend on where the button happens to be,
   and at the bottom because that is where a thumb is. */
/* **A closed picker must not lay out at all.** Chrome keeps a closed `<details>`'s contents out
   of layout with containment rather than `display: none`, and `position: fixed` escapes that — so
   making the panel a fixed sheet caused a 432px-wide page at 375px on every page of the site,
   from a panel nobody had opened. Said explicitly here, which is true in both positioning modes
   and does not depend on knowing how a UA implements the closed state. */
details.picker:not([open]) .picker-panel { display: none; }

@media (max-width: 560px) {
  .picker-panel {
    position: fixed; top: auto; bottom: 12px; left: 12px; right: 12px;
    width: auto; max-height: 60vh;
  }
}

.picker-group { border: 0; margin: 0 0 10px; padding: 0; }
.picker-group-lang { border-top: 1px solid var(--line); padding-top: 10px; }
.picker-group legend {
  font-size: .75rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: var(--ink-soft); padding: 0;
}
.picker-hint {
  margin: 4px 0 8px; font-size: .8rem; color: var(--ink-soft);
  /* **Wraps, explicitly.** The header sets `white-space: nowrap` on its own row so a long
     generated handle cannot break across four lines, and this paragraph inherited it — inside a
     304px panel it laid out 400px wide and started at x=-82 on a 375px screen, i.e. off the left
     edge. Nothing was clipping it in the CSS sense; it simply ran out of the panel. */
  white-space: normal; line-height: 1.4;
}

.picker-list { list-style: none; margin: 0; padding: 0; }
.picker-list li { list-style: none; }

/* Each row is a submit button, so choosing one applies it. Stripped back to a row: the
   browser's button chrome is removed here, and the tick plus the tint carry the current
   selection. 44px minimum because this is a phone control. */
.picker-option {
  display: flex; align-items: center; gap: 10px; width: 100%;
  min-height: 44px; padding: 9px 8px; border-radius: var(--radius-sm); cursor: pointer;
  font: inherit; text-align: left; color: inherit;
  background: none; border: 0;
}
.picker-option:hover { background: var(--paper); }
/* Offset inwards: the panel scrolls, and an outline drawn outside the row is clipped. */
.picker-option:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
.picker-option-flag { line-height: 0; flex: none; }
.picker-option-flag .flag {
  width: 24px; height: 16px; display: block;
  border-radius: 2px; box-shadow: 0 0 0 1px rgba(18, 40, 90, .12);
}
.picker-option-name { font-size: .95rem; flex: 1 1 auto; }
.picker-check { color: var(--blue); font-weight: 700; opacity: 0; flex: none; }
/* aria-current is the state, and the styling reads it directly rather than shadowing it in
   a class — the two cannot then disagree. */
.picker-option[aria-current="true"] .picker-check { opacity: 1; }
.picker-option[aria-current="true"] .picker-option-name { font-weight: 700; }
.picker-option[aria-current="true"] { background: rgba(26, 109, 255, .08); }

.notice {
  margin: 0 0 14px; padding: 10px 14px; border-radius: var(--radius);
  font-size: .92rem;
}
.notice-info {
  background: rgba(26, 109, 255, .08); border: 1px solid rgba(26, 109, 255, .3);
  color: #0b3f9e;
}

/* ---------- venue imagery ---------- */

/* Small, to the right of the text, vertically centred — the card keeps the compact height
   it had before there were any images. align-self:center rather than stretch, so a short
   card does not grow to meet a square thumbnail. */
.spot-thumb {
  flex: none; align-self: center;
  width: 72px; height: 72px;
  border-radius: var(--radius-sm); overflow: hidden; background: var(--paper);
}
.spot-thumb img, .spot-thumb svg { width: 100%; height: 100%; object-fit: cover; display: block; }

.venue-hero {
  border-radius: var(--radius); overflow: hidden; background: var(--paper);
  border: 1px solid var(--line); margin: 12px 0 16px;
  width: 100%; height: 280px;
}
.venue-hero img, .venue-hero svg { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Placeholders are line art, not photography — they should read as "no photo yet", not as
   a failed image. */
.venue-photo-placeholder { --placeholder-bg: #e7edf7; --placeholder-ink: #a9bad4; }

/* ---------- sign in ---------- */

.auth-page { max-width: 520px; }
.auth-card {
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
  padding: 22px; box-shadow: var(--shadow-sm);
}
.auth-lead { margin: 0 0 16px; color: var(--ink-soft); }
.auth-small { margin: 14px 0 0; font-size: .85rem; color: var(--ink-soft); }
.auth-error {
  margin: 0 0 14px; padding: 10px 12px; border-radius: var(--radius-sm);
  background: rgba(245, 166, 35, .14); border: 1px solid rgba(245, 166, 35, .5);
  color: #8a5a06;
}
.auth-form { display: flex; flex-direction: column; gap: 8px; }
.auth-form label { font-size: .9rem; font-weight: 600; }
.auth-form input {
  padding: 14px 16px; font: inherit; font-size: 16px;
  border: 1px solid var(--line); border-radius: var(--radius); background: #fff;
}
.auth-form .btn { margin-top: 4px; }

/* ---------- buttons ---------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px;             /* thumb-sized, one-handed, in a hurry */
  padding: 12px 20px;
  font: inherit; font-weight: 600; text-decoration: none; cursor: pointer;
  border: 1px solid transparent; border-radius: var(--radius);
}
.btn-primary { background: var(--blue); color: #fff; }
.btn-primary:hover { background: var(--blue-dark); }
.btn-locate { background: var(--green); color: #fff; width: 100%; }
.btn-locate:hover { filter: brightness(.94); }
.btn .icon { width: 20px; height: 20px; flex: none; }

/* An outlined button for actions that sit beside the primary one — per-spot directions,
   where one filled blue button per bathroom would turn the page into a wall of blue. */
.btn-secondary { background: var(--white); color: var(--blue-dark); border-color: var(--line); }
.btn-secondary:hover { background: var(--paper); border-color: var(--blue); }
/* Still 44px: smaller than the page's primary action, never smaller than a thumb. */
.btn-sm { min-height: 44px; padding: 9px 16px; font-size: .95rem; }
.btn-sm .icon { width: 18px; height: 18px; }

/* ---------- hero + search ---------- */

.hero { background: var(--white); border-bottom: 1px solid var(--line); padding: 24px 0 28px; }
.hero h1 { font-size: 1.8rem; }
.hero-sub { margin: 0 0 20px; color: var(--ink-soft); }

.search-panel { display: flex; flex-direction: column; gap: 10px; }
.search-panel input {
  width: 100%; padding: 14px 16px; font: inherit; font-size: 16px;
  border: 1px solid var(--line); border-radius: var(--radius); background: #fff;
}
.search-panel .btn { width: 100%; }

.near-me { margin-top: 12px; }
.near-me-status { margin: 8px 0 0; font-size: .9rem; color: var(--ink-soft); min-height: 1.2em; }
/* **An empty status must not reserve space.** It is empty until geolocation says something, and
   17px of empty paragraph under the button made `.near-me` 77px tall against the invitation's
   52px — so `align-items: center` centred two boxes whose buttons then sat 15px apart, and the
   hero row read as two things rather than one row of controls. */
.near-me-status:empty { display: none; }
.note, .empty {
  margin: 12px 0; padding: 12px 14px; color: var(--ink-soft);
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
}

/* ---------- spot cards ---------- */

.spot-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }

/* **The card is the flex row, not the link.**
   It used to be `.spot-link`, which wrapped the whole card and carried the row layout, the
   padding and the gap. The link moved onto the venue name so a rename pencil could sit beside
   it without nesting one <a> inside another — and the layout has to move with it, or the
   thumbnail stops being a flex sibling of the body, drops below it, and the card grows tall
   with no padding at all. That is exactly what happened. */
.spot-card {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 14px 16px;
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  position: relative;                 /* anchors the stretched link below */
}
.spot-card:hover { box-shadow: var(--shadow-md); }

/* Now an ordinary inline link on the title. `::after` stretches its hit area over the whole
   card, so the card still behaves as one target. */
.spot-link { color: inherit; text-decoration: none; }
.spot-link::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.spot-card:hover .spot-link { text-decoration: underline; }

.spot-body { min-width: 0; flex: 1 1 auto; }

.spot-head { display: flex; align-items: baseline; gap: 10px; justify-content: space-between; }
.spot-venue { margin: 0; font-size: 1.1rem; }
.spot-distance { flex: none; font-weight: 600; color: var(--blue-dark); font-variant-numeric: tabular-nums; }
.spot-meta { margin: 2px 0 8px; font-size: .9rem; color: var(--ink-soft); }
.spot-rating { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.spot-count { font-size: .88rem; color: var(--ink-soft); }

/* ---------- rating: droplets ---------- */

:root { --drop-size: 22px; }

.rating { display: inline-flex; align-items: center; gap: 8px; }
.rating-drops { position: relative; display: inline-block; line-height: 0; }
.rating-base, .rating-fill { display: inline-flex; gap: 2px; }
.rating-fill {
  position: absolute; inset: 0 auto 0 0;
  overflow: hidden; white-space: nowrap;   /* width is set inline, per rating */
}

.drop {
  width: var(--drop-size); height: var(--drop-size);
  min-width: 16px; min-height: 16px;
  flex: none;
}

.rating-base .drop { fill: var(--line); }
.rating-fill .drop { fill: var(--amber); }

.rating-num { font-weight: 700; color: var(--navy); font-variant-numeric: tabular-nums; }
/* "4.2 drops" — the unit names the shape it sits next to. */
.rating-unit { font-weight: 400; font-size: .85em; color: var(--ink-soft); }
.rating-empty { font-size: .9rem; color: var(--ink-soft); }

/* ---------- chips ---------- */

.chips { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; padding: 0; }
.chip {
  font-size: .82rem; padding: 4px 10px; border-radius: 999px;
  background: var(--paper); border: 1px solid var(--line); color: var(--ink-soft);
}
.chip-ok { background: rgba(34, 170, 75, .10); border-color: rgba(34, 170, 75, .35); color: #17743a; }
.chip-warn { background: rgba(245, 166, 35, .12); border-color: rgba(245, 166, 35, .4); color: #8a5a06; }
.chips-lg .chip { font-size: .9rem; padding: 6px 12px; }

/* ---------- how it works ---------- */

.how-grid { display: grid; gap: 14px; }
.how-item {
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 16px;
}
.how-item h3 { color: var(--navy); }
.how-item p { margin: 0; color: var(--ink-soft); font-size: .95rem; }

/* The logo PNG has a transparent background and its "squat" wordmark is dark navy, so
   anything dark behind it swallows half the word. It needs white, not brand colour. */
/* A 1068x718 panel taking a full screen height, for an image that is decoration below the fold.
   Capped and centred: it is the site's mark, not its content. */
.hero-image-wrap { text-align: center; }
.hero-image { max-width: 420px; width: 100%; height: auto; }
.hero-image {
  border-radius: var(--radius);
  background: #fff;
  border: 1px solid var(--line);
  padding: 8px;
}

/* ---------- search page ---------- */

.search-layout { padding-top: 20px; padding-bottom: 32px; display: grid; gap: 20px; }
.search-title { font-size: 1.35rem; }
.search-summary { margin: 0 0 14px; color: var(--ink-soft); }

.filters {
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 14px; margin-bottom: 16px;
}
.filter-row { display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; }
.filter-row + .filter-row { margin-top: 10px; }
.filter-check { display: inline-flex; align-items: center; gap: 8px; font-size: .95rem; }
.filter-check input { width: 20px; height: 20px; accent-color: var(--blue); }
.filter-select { display: inline-flex; align-items: center; gap: 8px; font-size: .95rem; }
.filter-select select {
  /* 16px so iOS does not zoom the page on focus, and a min-height so the control is a real tap
     target. Without it, padding alone made these 39px — visibly shorter than the 44px button
     sitting beside them in the same row, and under any reasonable thumb. Measured, not guessed:
     check-filters.js asserts it at every breakpoint. */
  font: inherit; font-size: 16px; padding: 8px 10px; min-height: 44px;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
}
.filters .btn { min-height: 44px; padding: 10px 18px; }

.map {
  width: 100%; height: 260px; border-radius: var(--radius);
  border: 1px solid var(--line); background: #e8eef7; overflow: hidden;
}
.map-fallback { padding: 16px; margin: 0; color: var(--ink-soft); font-size: .92rem; }
.map-venue { height: 220px; }
.maplibregl-popup-content { font-family: var(--font); font-size: .92rem; }

/* ---------- venue page ---------- */

.breadcrumb { padding: 14px 0 4px; font-size: .88rem; }
.breadcrumb ol { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.breadcrumb li + li::before { content: "›"; margin-right: 6px; color: var(--ink-soft); }
.breadcrumb [aria-current] { color: var(--ink-soft); }

.venue-head { padding-bottom: 8px; }
.venue-meta { margin: 0 0 14px; color: var(--ink-soft); }
/* Action rows: one row, shared baseline, matching heights.
 
   Both of these used to set `width: 100%` on their buttons, so every control took a line of
   its own — "Directions" above "Add a bathroom here", and on a bathroom the save form and
   the report link each wrapping below the two buttons. Four controls, four rows, nothing
   aligned with anything.

   `align-items: stretch` rather than `center`, so that when one label wraps to two lines
   the others grow to match instead of sitting at different heights. */
/* **Split, because these two rows want opposite things.** They shared one declaration, and
   setting `nowrap` for the spot actions took the venue-level row with it — "Add a bathroom here"
   went from a full-width button to a squeezed three-line column beside Directions. The venue row
   has two long labels and no icon fallback, so it still wraps. */
.venue-actions {
  display: flex; flex-wrap: wrap; align-items: stretch; gap: 8px; margin: 0 0 8px;
}
.spot-actions {
  /* One line at every width — see the note in venue.ejs. The review action absorbs the slack and
     is allowed to shrink (min-width:0, or a flex item refuses to go below its content width and
     pushes the row wider than the card); the three icon actions never shrink. */
  display: flex; flex-wrap: nowrap; align-items: stretch; gap: 8px;
}
.spot-actions .spot-action-primary { flex: 1 1 auto; min-width: 0; }
.spot-actions .spot-action-icon { flex: 0 0 auto; }

/* ---------------------------------------------------------------------------------------
   When the three secondary actions reduce to their icons

   **The row's width decides this, and the row's width is not the viewport.** It used to be a
   media query at 480px, which is a statement about the browser window and not about the box the
   four buttons have to fit inside. Measured on the venue page at exactly 480px, signed in: the
   row needs 453px and has 414px, so the words came back 39px early and the primary label
   clipped. Signed out it fitted, which is why it survived — the row that overflows is the one
   with a fourth button on it, and an anonymous check never has one.

   So it keys on the container. `.spot-detail` becomes a query container and the row asks how
   much room it actually has. Two consequences beyond fixing the 480px case: a card in a
   narrower column gets the same answer, and no future layout change can move the row without
   moving the rule with it.

   **And on the row's own contents, because the count is not fixed.** Save renders only for a
   signed-in reader, so the same viewport carries a three- or four-action row. Binary-searched,
   words forced on, looking for the narrowest card where nothing clips and everything stays on
   one line:

     3 actions (anonymous)   — 364px of row (a 398px card)
     4 actions (signed in)   — 453px of row (a 487px card)

   **The row figure is the one in the queries, not the card figure.** `container-type:
   inline-size` queries the container's *content* box, which is the row's width — the card is
   34px wider, being 16px of padding and a 1px border on each side. Written with the card
   numbers the words came back 34px late: at a 540px viewport the card is 508px, comfortably
   past 487, and the row had only 474px so it stayed on icons.

   One threshold cannot serve both: 453 takes the words off an anonymous reader for 89px of
   width that would have held them, and 364 puts a signed-in reader back into the overflow. So
   `:has(.save-form)` asks the row how many buttons it is carrying. Each number is measured
   rather than chosen, and ~/.local/squatadvisor-visual/spot-actions-threshold.js re-derives both
   in a browser if a label changes.

   Icons-only is the **default** rather than the narrow-screen override, so a browser that
   understands none of this gets the state that always fits rather than the one that overflows.
   --------------------------------------------------------------------------------------- */
.spot-detail { container-type: inline-size; container-name: spot-card; }

.spot-action-icon .action-text { display: none; }
.spot-action-icon { padding-left: 11px; padding-right: 11px; gap: 0; }

@container spot-card (min-width: 364px) {
  .spot-action-icon .action-text { display: inline; }
  /* Back to what .btn-sm and .btn give every other button — 16px and 8px — rather than a
     third set of numbers. The thresholds above were measured against these. */
  .spot-action-icon { padding-left: 16px; padding-right: 16px; gap: 8px; }
}

/* The four-action row needs 453px, so between the two thresholds it stays on icons while the
   three-action row already has its words. More specific than the rule above and after it, so it
   wins without an `!important`. */
@container spot-card (max-width: 453px) {
  .spot-actions:has(.save-form) .spot-action-icon .action-text { display: none; }
  .spot-actions:has(.save-form) .spot-action-icon {
    padding-left: 11px; padding-right: 11px; gap: 0;
  }
}

/* **The fallback is the old media query, and it is deliberately a duplicate.** Container queries
   are Chrome 105 / Safari 16 / Firefox 110; older than that and `@container` is dropped, leaving
   icons-only at every width — safe, since it always fits, but the words would never return on a
   desktop. Scoped inside `@supports not` so exactly one of the two is ever live and they cannot
   both apply to the same browser. 480px is wrong for the four-action row, which is why it is the
   fallback rather than the rule; the failure it leaves behind is the one that shipped, not a
   new one. */
@supports not (container-type: inline-size) {
  @media (min-width: 480px) {
    .spot-action-icon .action-text { display: inline; }
    /* Back to what .btn-sm and .btn give every other button — 16px and 8px — rather than a
     third set of numbers. The thresholds above were measured against these. */
  .spot-action-icon { padding-left: 16px; padding-right: 16px; gap: 8px; }
  }
}
.report-action { text-decoration: none; }
.report-action:hover { color: #a32020; border-color: #a32020; }
.venue-actions .btn,
.spot-actions .btn { width: auto; }

/* The save control is a button inside a form, so the form has to stretch for the button to
   match the height of its neighbours rather than sit short inside a taller row. */
.spot-actions .save-form { display: flex; align-items: stretch; }
.spot-actions .save-form .btn { height: 100%; }

/* Report stays a quiet text link — it should not look like a fourth button — but it still
   has to sit on the row rather than dangle under it. */
.spot-actions .report-link {
  display: inline-flex; align-items: center; padding: 0 6px; min-height: 44px;
}

/* Narrow enough that only one or two fit per row: let them grow, so a lone button on a row is a
   thumb-sized target rather than a word floating on the left.
   **Only the venue row and only the spot row's primary action.** This used to say
   `.spot-actions .btn`, which matched the icon actions too — at equal specificity to
   `.spot-actions .spot-action-icon` and later in the file, so it won, and the three icons grew and
   pushed themselves onto a second line. The row it was meant to help is the venue one. */
@media (max-width: 480px) {
  .venue-actions .btn { flex: 1 1 auto; }
  .spot-actions .spot-action-primary { flex: 1 1 auto; }
}

.spot-detail {
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 16px; margin: 0 0 14px;
}
.spot-detail-head { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; justify-content: space-between; }
.spot-detail-head h2 { margin: 0; }
.spot-gender { font-weight: 400; font-size: .9rem; color: var(--ink-soft); }

.score-breakdown { display: grid; gap: 8px; margin: 14px 0 0; }
.score-breakdown > div { display: flex; align-items: center; gap: 12px; }
.score-breakdown dt { width: 7.5em; flex: none; font-size: .92rem; color: var(--ink-soft); }
.score-breakdown dd { margin: 0; }

.facts-heading { margin: 16px 0 0; font-size: .95rem; color: var(--ink-soft); }

.entry-code {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
  margin: 14px 0 0; padding: 12px 14px;
  background: rgba(34, 170, 75, .08); border: 1px solid rgba(34, 170, 75, .3);
  border-radius: var(--radius);
}
.entry-code-label { font-size: .85rem; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .04em; }
.entry-code-value { font-size: 1.3rem; font-weight: 700; letter-spacing: .08em; color: var(--navy); }
.entry-code-age { font-size: .85rem; color: var(--ink-soft); }
.entry-code-stale { background: rgba(245, 166, 35, .12); border-color: rgba(245, 166, 35, .45); }
.entry-code-stale .entry-code-age { color: #8a5a06; font-weight: 600; }

/* Directions sit at the foot of the spot, after the facts and the code — someone reads
   what the bathroom is like, then decides to go. */
.spot-actions { margin: 16px 0 0; }

.reviews { margin-top: 24px; }
.review-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.review {
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
}
.review-head { display: flex; gap: 10px; justify-content: space-between; align-items: baseline; }
.review-author { font-weight: 600; }
.review-when { font-size: .85rem; color: var(--ink-soft); }
.review-scores { display: flex; align-items: center; gap: 8px; margin: 6px 0; }

/* The narrative on the review form. A fieldset rather than another .field so it reads as a
   section of the form in its own right — it spent its life fourth in the running order,
   between two dropdowns and a date, and got overlooked by the person who put it there. */
.review-body-group legend { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.review-body-group legend label { font-weight: 600; }
.review-body-group textarea { width: 100%; font-size: 16px; line-height: 1.5; }
.review-score-label { font-size: .85rem; color: var(--ink-soft); }
/* The review itself, not a footnote to the drops above it. Slightly larger than body text
   and a touch looser, because this is the thing a reader came for — and the thing a venue
   owner would pay to be able to answer. `pre-line` keeps whatever paragraphing the author
   typed; running it into one block is a small disrespect that adds up over four sentences. */
.review-body {
  margin: 8px 0 0; padding: 0 0 0 12px;
  border-left: 3px solid var(--line);
  font-size: 1.02rem; line-height: 1.6; white-space: pre-line;
}

/* The map follows the reviews in the document, and with no margin the last review card and
   the map frame shared an edge — two bordered boxes touching read as one broken box. */
.venue-map-wrap { display: block; margin: 28px 0 0; }

/* ---------- contributing ---------- */

/* Green, not blue: it is the one control on the page that asks the reader to give rather
   than take, and it should not compete with Search for the same visual slot. */
.btn-add { background: var(--green); color: #fff; }
.btn-add:hover { filter: brightness(.94); }
.btn-add .icon { width: 18px; height: 18px; }

/* In the header it has to survive a 320px screen next to a flag and a handle, so it loses
   its words before it loses its place. */
.header-add { min-height: 40px; padding: 8px 12px; font-size: .9rem; border-radius: var(--radius-sm); }
.header-add-text { display: none; }
.header-add-short { display: inline; }

.hero-add { margin: 18px 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; color: var(--ink-soft); }

/* A form's fields, grouped by what they are about. The border is faint on purpose: it
   separates without turning a five-minute form into a stack of boxes. */
.field-group { border: 0; border-top: 1px solid var(--line); margin: 8px 0 0; padding: 14px 0 0; }
.field-group legend { font-size: .95rem; font-weight: 700; color: var(--navy); padding: 0 8px 0 0; }
/* ---------- optional detail, folded away ---------- */

/* Built on <details>, so it opens with JavaScript off like everything else here. Closed by
   default: the form's job is to get a place and a bathroom out of someone standing in a
   corridor, and eleven facts between them and the submit button is how that person becomes
   someone who nearly contributed. */
.optional-details {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--white); margin: 4px 0 0;
}
.optional-details > summary {
  display: block;                        /* replaces the default marker layout */
  cursor: pointer; padding: 14px 16px 14px 40px; position: relative;
  border-radius: var(--radius);
}
.optional-details > summary::-webkit-details-marker { display: none; }
.optional-details > summary::marker { content: ''; }
/* A chevron drawn in CSS, so there is no glyph to go missing and no image to load. */
.optional-details > summary::before {
  content: ''; position: absolute; left: 16px; top: 1.35em;
  width: 8px; height: 8px; margin-top: -5px;
  border-right: 2px solid var(--blue-dark); border-bottom: 2px solid var(--blue-dark);
  transform: rotate(-45deg); transition: transform .15s ease;
}
.optional-details[open] > summary::before { transform: rotate(45deg); margin-top: -2px; }
.optional-details > summary:hover { background: var(--paper); }
.optional-details > summary:focus-visible { outline: 3px solid var(--blue); outline-offset: -3px; }

.optional-summary-title { display: block; font-weight: 700; color: var(--navy); }
.optional-summary-note { display: block; margin-top: 2px; font-size: .85rem; color: var(--ink-soft); }

.optional-body { padding: 4px 16px 16px; border-top: 1px solid var(--line); }
.optional-body > .field { margin-top: 14px; }

/* ---------- the facts list ---------- */

/* Eleven facts used to be eleven full-width fields with the label hard left and the control
   hard right, which put a lane of empty space between a question and its answer and made
   each label read as belonging to nothing. They are rows now: label and control adjacent,
   each control only as wide as its answer, a fixed row height so the control column is not
   ragged, and hairlines so the eye can travel down them. */
.fact-group { margin: 0 0 18px; }
.fact-group-title {
  margin: 0 0 6px; font-size: .75rem; font-weight: 700; color: var(--ink-soft);
  text-transform: uppercase; letter-spacing: .06em;
}
.fact-list {
  list-style: none; margin: 0; padding: 0;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: #fff; overflow: hidden;
}
.fact-row {
  display: grid;
  /* Narrow screens: label takes what is left, control keeps its own width on the same
     line. The row is short enough here that they are still next to each other. */
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 4px 12px;
  min-height: 52px; padding: 8px 12px;
}
.fact-row + .fact-row { border-top: 1px solid var(--line); }
.fact-row:nth-child(even) { background: var(--paper); }
.fact-row-label { font-size: .95rem; font-weight: 600; min-width: 0; }
.fact-row-control { justify-self: end; }
.fact-row-hint, .fact-row-error { grid-column: 1 / -1; margin: 0; font-size: .8rem; }
.fact-row-hint { color: var(--ink-soft); }
.fact-row-error { color: #a32020; font-weight: 600; }

/* Controls sized to their answers rather than to the form. Yes/No/Don't know is three
   short words; a stall count is two digits. */
.fact-row select, .fact-row input {
  font: inherit; font-size: 16px;      /* 16px stops iOS zooming on focus */
  padding: 8px 10px; min-height: 40px;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
  width: auto; max-width: 100%;
}
.control-xs { width: 5.5rem; }
.control-sm { width: 9.5rem; }
.control-md { width: 12.5rem; }

/* On a narrow phone the widest control has to give a little back, or a label like "Drying
   your hands" wraps to two lines beside it. */
@media (max-width: 480px) {
  .control-md { width: 10.5rem; }
  .control-sm { width: 8.5rem; }
  .fact-row { padding: 8px 10px; }
}
.field textarea {
  font: inherit; font-size: 16px; padding: 11px 12px;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
  width: 100%; resize: vertical;
}
.req { font-weight: 400; font-size: .8rem; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .04em; }

/* The location picker: the map is 240px of enhancement above two fields that already work. */
.map-pin { height: 260px; margin: 0 0 10px; }
.locate-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 10px; }
.locate-row .near-me { margin: 0; }

/* What the geocoder matched, in its own words. Muted rather than loud: it is a thing to
   check, not a thing that went wrong. */
.geocode-result { margin: 0 0 10px; font-size: .92rem; }
.geocode-result p { margin: 0; }
.geocode-found { color: var(--ink-soft); }
.geocode-error { color: #8d1c1c; }

/* The pin is nowhere near the town in the address. A warning, not a refusal — some
   bathrooms really are fifty kilometres from anywhere. */
.notice-warn {
  background: rgba(214, 143, 0, .10); border: 1px solid rgba(214, 143, 0, .4); color: #7a5200;
}
.notice-warn p { margin: 0 0 8px; }
.check { display: flex; gap: 8px; align-items: center; font-weight: 600; }
.check input { width: 18px; height: 18px; }

/* The optional per-bathroom pin. Quieter than the venue's picker, because for almost every
   bathroom the right answer is to leave it alone. */
.spot-location .field-hint { margin-top: 0; }
.spot-location-toggle { margin: 10px 0; min-height: 44px; }
.spot-location-body[hidden] { display: none; }

/* Which pin the map is showing: the bathroom's own, or the building's front door. */
.map-pop-kind {
  display: inline-block; margin-top: 2px; font-size: .78rem; font-weight: 600;
  color: var(--blue);
}
.map-pop-kind-venue { color: var(--ink-soft); }
.map-pop-detail { color: var(--ink-soft); }

/* Same distinction in the HTML, so it survives with JavaScript off — the map does not. */
.pin-badge {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: .78rem; font-weight: 600; color: var(--blue);
  border: 1px solid rgba(26, 109, 255, .35); border-radius: 999px; padding: 1px 8px;
}
.pin-badge .icon { width: 12px; height: 12px; }

/* Imported, and not yet checked by anyone. Amber rather than blue: it is a caveat, not a
   feature, and it should not look like the "own location" badge beside it. */
.pin-badge-osm { color: #7a5200; border-color: rgba(214, 143, 0, .45); }

/* Provenance on a venue page. Carries two jobs at once — telling a reader nothing here has
   been confirmed, and being the ODbL attribution notice — so it sits above the content
   rather than in a footer nobody reads. */
.provenance {
  display: flex; gap: 8px; align-items: flex-start;
  margin: 0 0 14px; padding: 10px 12px; font-size: .9rem; line-height: 1.5;
  background: rgba(214, 143, 0, .08); border: 1px solid rgba(214, 143, 0, .35);
  border-radius: var(--radius); color: #6b4800;
}
.provenance .icon { width: 18px; height: 18px; flex: none; margin-top: 2px; }
.provenance a { color: inherit; }

/* What the distances in the list below are measured from, when that is not the reader.
   Sits directly on top of the list — the whole point is proximity to the numbers it
   explains — and is quiet enough not to compete with the results themselves. */
.results-origin {
  margin: 0 0 12px;
  padding: 10px 12px;
  background: var(--paper);
  border-left: 3px solid var(--blue);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.results-origin-head {
  margin: 0; font-size: 1rem; font-weight: 600; color: var(--navy);
  /* A long Nominatim display name is several commas deep; let it wrap rather than push the
     column wide. */
  overflow-wrap: anywhere;
}
.results-origin-note {
  margin: 2px 0 0; font-size: .8rem; color: var(--ink-soft);
}

/* What a filter withheld, said above the list it withheld it from — the same placement, and the
   same reasoning, as .results-origin: it explains the results, so it has to sit where they are. */
/* Item 9: this was a short <p> floating at the left of a 1440px column, with most of the row
   empty beside it — which reads as a layout that did not finish rather than as an invitation.
   A bordered strip with the text and the button at opposite ends looks deliberate at any width,
   and stacks below 560px where side-by-side would squeeze both. */
/* A decision on your own contribution. Toned rather than coloured alone: the word says what
   happened, and the stripe only makes it findable in a list. */
.me-decision { border-left: 3px solid var(--line); padding-left: 10px; }
.me-decision-good { border-left-color: var(--green); }
.me-decision-bad { border-left-color: #c8372d; }
.me-decision-note { border-left-color: var(--amber); }
.me-decision-what { font-weight: 600; color: var(--ink); }
/* Named apart from the tone class above, which is `.me-decision-note` on a list item. Sharing
   one name meant the paragraph's margin and the item's border fought over the same selector. */
.me-decision-caveat { margin: 10px 0 0; font-size: .85rem; color: var(--ink-soft); }

/* One line of numbers under the hero. Quiet — it is reassurance, not a headline. */
.coverage {
  margin: 14px 0 0; color: var(--ink-soft); font-size: .95rem;
}
.coverage strong { color: var(--ink); font-variant-numeric: tabular-nums; }

/* Cities to browse. A grid that fills at every breakpoint, because the whole reason this section
   leads the page is that it has enough to fill one — see REVIEWS_TO_SHOW in lib/places.js. */
.city-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;
}
.city-card {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 12px 14px; min-height: 48px;
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-sm);
  text-decoration: none; color: var(--ink); font-weight: 600;
}
.city-card:hover { border-color: var(--blue); }
.city-name { overflow-wrap: anywhere; }
.city-count {
  flex: none; color: var(--ink-soft); font-weight: 500; font-size: .9rem;
  font-variant-numeric: tabular-nums;
}
@media (min-width: 640px) { .city-list { grid-template-columns: repeat(4, 1fr); } }

/* The near-me button and the "add one" invitation: stacked on a phone, one row on desktop.
   Column by default so the phone case needs no media query to be correct. */
.hero-actions { display: flex; flex-direction: column; gap: 12px; margin: 16px 0 0; }

.hero-add {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-wrap: wrap; padding: 14px 16px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--white);
}
.hero-add-text { margin: 0; color: var(--ink); font-weight: 600; }
@media (max-width: 560px) {
  .hero-add { flex-direction: column; align-items: stretch; text-align: center; }
  .hero-add .btn { width: 100%; }
}

/* On desktop the invitation joins the row rather than taking one of its own, and drops the box:
   beside a plain button, a bordered panel reads as a different kind of thing rather than as the
   second half of a pair.
   **Its button goes secondary here too.** Two green buttons side by side claim equal importance,
   and they are not equal: finding a bathroom is what someone came for, and adding one is what
   they might do afterwards. On mobile the two are on separate rows with the invitation inside its
   own bordered strip, so nothing is competing and the green stays. */
@media (min-width: 720px) {
  .hero-actions { flex-direction: row; align-items: center; gap: 20px; flex-wrap: wrap; }
  /* Margins are part of a flex item's outer box, so `align-items: center` centres the box
     *including* them: .near-me carried 12px on top and .hero-add 18px, which put the two buttons'
     centres 3px apart — close enough to look like a mistake rather than a choice. The wrapper's
     gap is the only spacing on this row now. */
  .hero-actions > * { margin-top: 0; margin-bottom: 0; }
  .hero-add {
    padding: 0; border: 0; background: none;
    gap: 12px; flex: 0 1 auto;
  }
  .hero-add-text { font-weight: 500; color: var(--ink-soft); }
  .hero-add .btn-add {
    background: var(--white); color: var(--blue-dark); border: 1px solid var(--line);
  }
  .hero-add .btn-add:hover { background: var(--paper); border-color: var(--blue); filter: none; }
}

.filter-note {
  margin: 0 0 12px; padding: 10px 12px;
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius-sm);
  font-size: .85rem; color: var(--ink-soft); line-height: 1.5;
}
.filter-note a { color: var(--blue-dark); }

.results-attribution {
  margin: 14px 0 0; font-size: .85rem; color: var(--ink-soft); line-height: 1.5;
}

/* /data is prose, and prose wants a measure rather than the full width of a search page.
   The top padding is not decoration: `main.wrap` has none of its own, so the h1 sat flush
   against the header bar with nothing between them. */
.prose { max-width: 46rem; padding-top: 24px; }
.prose h1 { margin-top: 0; }
.prose h2 { margin: 26px 0 8px; }
.prose h3 { margin: 18px 0 6px; font-size: 1rem; }
.prose p, .prose li { line-height: 1.65; }
.prose ul { padding-left: 20px; }
.prose .lede { font-size: 1.08rem; color: var(--ink-soft); }
.data-download { margin: 22px 0; display: grid; gap: 8px; justify-items: start; }

/* A location complaint, judged where it arrives: the pin, the address, and a way to move
   one to match the other without opening the edit form in another tab. */
.report-location {
  margin: 10px 0; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper);
}
.report-pin-kind { margin: 8px 0 0; font-size: .88rem; color: var(--ink-soft); }
.report-suggestion { margin: 8px 0 0; font-size: .88rem; }
.report-fix { display: grid; gap: 8px; margin-top: 10px; }
.report-fix-coords { display: flex; flex-wrap: wrap; gap: 10px; }
.report-fix-coords label { display: grid; gap: 2px; font-size: .8rem; color: var(--ink-soft); }
.report-fix-coords input {
  font: inherit; font-size: 16px; padding: 6px 8px; min-height: 40px; width: 11rem;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}

/* Rating radios. Five whole rows of drops, tappable, with the real radio underneath for
   keyboard and screen-reader use — a rating behind a dropdown is a rating people skip. */
.rating-group { border-top: 0; padding-top: 0; }
.rating-choice { display: flex; flex-direction: column; gap: 6px; }
.rating-option {
  display: flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 6px 10px; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
}
.rating-option:hover { background: var(--paper); }
.rating-option input { width: 20px; height: 20px; accent-color: var(--blue); flex: none; }
.rating-option:has(input:checked) { border-color: var(--blue); background: rgba(26, 109, 255, .07); }
.rating-option:focus-within { outline: 2px solid var(--blue); outline-offset: 1px; }

/* ---------- confirming facts ---------- */

/* The chip keeps its shape and gains a tick. Confirming has to stay a one-tap action on a
   page someone is reading in a hurry, so it lives on the fact itself rather than behind a
   heading called "contribute". */
.fact-chip { display: inline-flex; align-items: center; gap: 8px; padding-right: 4px; }
.fact-text { min-width: 0; }
.fact-tally {
  font-size: .78rem; font-weight: 700; color: var(--ink-soft);
  background: rgba(18, 40, 90, .07); border-radius: 999px; padding: 1px 7px;
  font-variant-numeric: tabular-nums;
}
.fact-confirm { margin: 0; display: flex; }
.fact-confirm button {
  min-width: 32px; min-height: 32px; padding: 0 6px; cursor: pointer;
  font: inherit; font-size: .95rem; font-weight: 700; line-height: 1;
  color: #17743a; background: #fff;
  border: 1px solid rgba(34, 170, 75, .45); border-radius: 999px;
}
.fact-confirm button:hover { background: rgba(34, 170, 75, .14); }

/* Correcting is rarer than confirming, so it collapses. */
.fact-fix { margin: 12px 0 0; }
.fact-fix > summary {
  cursor: pointer; font-size: .9rem; font-weight: 600; color: var(--blue-dark);
  padding: 8px 0; min-height: 40px; display: flex; align-items: center;
}
.fact-fix-list { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 8px; }
.fact-fix-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: 0; padding: 8px; background: var(--paper); border-radius: var(--radius-sm);
}
.fact-fix-row label { flex: 1 1 9rem; font-size: .9rem; font-weight: 600; }
.fact-fix-row select, .fact-fix-row input {
  font: inherit; font-size: 16px; padding: 8px 10px; min-height: 40px;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
  flex: 0 1 11rem; min-width: 0;
}
.fact-fix-row .btn { min-height: 40px; padding: 6px 14px; flex: none; }

.notice-ok {
  background: rgba(34, 170, 75, .10); border: 1px solid rgba(34, 170, 75, .4); color: #17743a;
}

/* ---------- thanks ---------- */

.review-foot { margin: 10px 0 0; }
.thanks-form { margin: 0; }
/* Reads as a control, not a link: this one writes to the database. 44px because it is a
   real target on a phone, not an afterthought under the text. */
.thanks-btn {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 8px 14px;
  font: inherit; font-size: .92rem; font-weight: 600; cursor: pointer;
  color: var(--blue-dark); background: var(--white);
  border: 1px solid var(--line); border-radius: 999px;
}
.thanks-btn:hover { background: var(--paper); border-color: var(--blue); }
.thanks-btn .icon { width: 18px; height: 18px; fill: currentColor; flex: none; }
/* Already thanked: filled, and the label says "Thanked" — colour alone never carries a
   state, since a third of the difference is invisible to some readers. */
.thanks-btn.is-on {
  background: rgba(34, 170, 75, .12); border-color: rgba(34, 170, 75, .45); color: #17743a;
}
.thanks-count {
  font-variant-numeric: tabular-nums; padding-left: 8px;
  border-left: 1px solid var(--line); color: var(--ink-soft); font-weight: 700;
}
.thanks-static { font-size: .88rem; color: var(--ink-soft); }

/* ---------- your contributions (/me) ---------- */

.me { padding-top: 20px; padding-bottom: 32px; }
.me-head { margin-bottom: 16px; }
.me-sub { margin: 0; color: var(--ink-soft); }
.me-email { margin: 2px 0 0; font-weight: 400; font-size: .9rem; color: var(--ink-soft); }

.me-stats {
  list-style: none; margin: 0 0 24px; padding: 0;
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;
}
.me-stats li {
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 14px;
}
.me-stat-n { display: block; font-size: 1.6rem; font-weight: 700; color: var(--navy); font-variant-numeric: tabular-nums; }
.me-stat-l { font-size: .85rem; color: var(--ink-soft); }
/* Thanks is the only number here that is other people's opinion of the work rather than a
   count of it, so it is the one that gets the colour. */
.me-stat-thanks { border-color: rgba(34, 170, 75, .4); background: rgba(34, 170, 75, .06); }
.me-stat-thanks .me-stat-n { color: #17743a; }

.me-section { margin: 0 0 28px; }
.me-queue .notice { margin-bottom: 12px; }

.me-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.me-item {
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 14px;
  display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: baseline;
}
.me-item-lg { align-items: flex-start; justify-content: space-between; }
.me-item-main { flex: 1 1 16rem; min-width: 0; }
.me-item-side {
  flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
}
.me-item-kind {
  flex: none; font-size: .7rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink-soft);
}
.me-item-name { flex: 1 1 auto; min-width: 0; }
.me-item-when { flex: none; font-size: .85rem; color: var(--ink-soft); }
.me-item-title { margin: 0 0 2px; font-size: 1.05rem; }
.me-item-sub { font-weight: 400; font-size: .9rem; color: var(--ink-soft); }
.me-item-meta { margin: 2px 0 0; font-size: .85rem; color: var(--ink-soft); }
.me-item-body { margin: 6px 0 0; font-size: .95rem; }
.me-item-rating { display: flex; align-items: center; gap: 8px; margin: 6px 0 0; }
.me-item-note { margin: 6px 0 0; font-size: .85rem; color: var(--ink-soft); }
.me-item-window { margin: 6px 0 0; font-size: .82rem; color: var(--blue-dark); }
.me-item-window.is-closed { color: var(--ink-soft); }
.me-thanks { font-size: .85rem; font-weight: 600; color: #17743a; white-space: nowrap; }

/* ---------- edit forms ---------- */

.edit-page { padding-bottom: 32px; max-width: 720px; }
.edit-subject { margin: 0 0 12px; color: var(--ink-soft); }
.edit-window {
  margin: 0 0 16px; padding: 10px 14px; border-radius: var(--radius);
  font-size: .9rem; font-weight: 600;
  background: rgba(26, 109, 255, .08); border: 1px solid rgba(26, 109, 255, .3); color: #0b3f9e;
}
/* Closed reads as a statement of fact, not a warning: nothing went wrong, the window simply
   passed, and there is a way forward on the page. */
.edit-window.is-closed {
  background: var(--paper); border-color: var(--line); color: var(--ink-soft);
}

.edit-form { display: grid; gap: 14px; }
.edit-section-h { margin: 10px 0 0; font-size: 1.05rem; }
.edit-facts-warning { margin-top: 0; }

/* align-content: start is load-bearing. A .field is a grid of label / control / hint, and
   two fields side by side in a .field-row are stretched to the same height by the outer
   grid. Without this, the spare height in the shorter one is handed to its rows, and the
   input slides down the column — so a "Two letters." hint under Country pushed Postcode's
   box out of line with it. Rows keep their natural height; the leftover space stays at the
   bottom of the column where nobody can see it. */
.field { display: grid; gap: 4px; min-width: 0; align-content: start; }
.field label, .field-label { font-size: .9rem; font-weight: 600; }
/* **Not checkboxes or radios.** This rule is written for text boxes and selects — full width, a
   44px tap target, 16px so iOS does not zoom — and it was applying to every input in a `.field`,
   so a checkbox became a 44px-tall, full-width, padded box with its label pushed above it. The
   proximity toggle on the site form was exactly that, twice: shrunk once by overriding width and
   height, which left `min-height` and `padding` still doing it.
   Excluded here rather than overridden per use, because the next checkbox somebody puts in a
   `.field` would meet the same thing. */
.field input:not([type="checkbox"]):not([type="radio"]), .field select {
  font: inherit; font-size: 16px;          /* 16px stops iOS zooming on focus */
  padding: 11px 12px; min-height: 44px;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
  width: 100%; max-width: 100%;
}
.field-row { display: grid; gap: 14px; align-items: start; }
.field-grid { display: grid; gap: 14px; align-items: start; }
.field-hint { margin: 0; font-size: .82rem; color: var(--ink-soft); }
.field-error { margin: 0; font-size: .85rem; font-weight: 600; color: #a32020; }
.field-frozen .field-static {
  display: inline-block; padding: 8px 10px;
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius-sm);
  font-size: .9rem; color: var(--ink-soft); overflow-wrap: anywhere;
}

.edit-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 6px; }
.edit-actions .btn { flex: 1 1 auto; }

.notice-error {
  background: rgba(163, 32, 32, .08); border: 1px solid rgba(163, 32, 32, .35); color: #8d1c1c;
}

/* ---------- inline rename (moderators) ---------- */

/* Inline, so the pencil sits *beside* the title rather than under it. `align-items: center`
   rather than baseline, because a 16px icon has no useful baseline and lining its box up with
   the text's baseline drops it below the letters. No `flex-wrap`: wrapping is exactly the
   failure being avoided — on a narrow card the pencil went to its own line. */
.editable { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; }
.editable-value { display: inline; }

/* When the wrapper *is* the heading (the venue <h1>), it should still behave like one —
   block-level, full width, its own margins — rather than shrink-wrapping the way an
   inline-flex box does. */
.editable-heading { display: flex; flex-wrap: wrap; }

/* Quiet until wanted. It is a tool for someone doing a job, not a call to action, and it sits
   next to content the reader is here for.

   Sized in `em` and with a negative block margin so it cannot grow the line box it is in. A
   fixed 28px box inside a 1.1rem heading made every search card taller than it was before the
   pencil existed — the icon has to fit the text, not the other way round. */
.editable-pencil {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5em; height: 1.5em; margin-block: -0.4em; flex: none;
  border-radius: var(--radius);
  color: var(--ink-soft); opacity: .55;
  text-decoration: none;
  /* Above the stretched card link, or it is unclickable on a search result. */
  position: relative; z-index: 2;
}
.editable-pencil svg { width: 1em; height: 1em; }
.editable-pencil:hover, .editable-pencil:focus-visible { opacity: 1; color: var(--blue); background: var(--paper); }

.editable-form { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; position: relative; z-index: 2; }
.editable-input {
  font: inherit; font-size: 1rem;      /* 16px, so iOS does not zoom on focus */
  padding: 6px 8px; min-width: 16ch; max-width: 42ch; width: 100%;
  border: 1px solid var(--blue); border-radius: var(--radius);
}
.editable-note { font-size: .8rem; color: var(--ink-soft); }
.editable-note.is-error { color: var(--red, #c0392b); }

.btn-small { padding: 6px 10px; font-size: .85rem; min-height: 0; }
.btn-quiet { background: transparent; border: 1px solid var(--line); color: var(--ink-soft); }

/* `.spot-card` and `.spot-link` are laid out with the other card rules above, next to the
   thumbnail they have to stay siblings of. Anything that must remain clickable over the
   stretched link — the pencil — raises its own z-index. */

/* ---------- admin: auto-generated names ---------- */

.name-stats { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-wrap: wrap; gap: 16px; }
.name-stats li { font-size: .9rem; color: var(--ink-soft); }
.name-stats strong { color: var(--navy); font-size: 1.1rem; }

.name-table { width: 100%; border-collapse: collapse; font-size: .92rem; }
.name-table th, .name-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
.name-table th { font-size: .78rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-soft); }
.name-where, .name-use { color: var(--ink-soft); white-space: nowrap; }

.name-flag {
  display: inline-block; padding: 2px 7px; border-radius: 999px;
  font-size: .72rem; background: var(--paper); color: var(--ink-soft);
  border: 1px solid var(--line); white-space: nowrap;
}
.name-flag-worst { background: #fdecea; border-color: #f5c6c0; color: #a33227; }
.name-flag-bad { background: #fff4e0; border-color: #f0d9a8; color: #8a5a12; }
.name-flag-mild { opacity: .7; }

.name-paging { display: flex; gap: 16px; align-items: center; margin: 16px 0; font-size: .9rem; }
.name-osm { font-size: .78rem; margin-left: 6px; white-space: nowrap; }
.admin-hint { font-size: .88rem; color: var(--ink-soft); margin: 0 0 12px; }
.admin-hint kbd {
  font: inherit; font-size: .82em; padding: 1px 5px;
  border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 4px;
  background: var(--white);
}

/* The list and its map. One column on a phone — a 300px map above a table of a hundred rows
   would push the work off the screen — and side by side once there is room, with the map
   sticky so it stays visible while the list scrolls under it. */
.names-layout { display: grid; gap: 16px; align-items: start; }
.names-map-canvas { height: 260px; border-radius: var(--radius); overflow: hidden; background: var(--paper); }
.names-map-label { margin: 6px 0 0; font-size: .82rem; color: var(--ink-soft); }
.names-map-idle { margin: 0; padding: 16px; font-size: .85rem; color: var(--ink-soft);
                  border: 1px dashed var(--line); border-radius: var(--radius); text-align: center; }

@media (min-width: 900px) {
  .names-layout { grid-template-columns: minmax(0, 1fr) 320px; }
  .names-map { position: sticky; top: 16px; }
}

/* A row that has just been renamed, so a moderator working down the list can see how far
   they have got without re-reading names they have already fixed. */
.editable.is-renamed .editable-value { color: var(--green); }

/* ---------- ads ---------- */

/* Styles for a slot that renders nothing today. They exist so that the day a network is
   configured is a config change and not a layout problem: the space, the label and the
   separation from the content are already decided. */
.ad {
  display: block; margin: 24px 0 0;
  padding: 10px; text-align: center;
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
}
/* Small, muted, and above the unit — visible enough to be honest, quiet enough not to
   compete with the results it sits under. */
.ad-label {
  display: block; margin-bottom: 6px;
  font-size: .7rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-soft);
}
/* The unit reserves its height at every breakpoint, so the page does not move when a slot
   fills, empties, or is filled late by a network script. A banner that arrives after layout
   and pushes the results down is the single most annoying thing an ad can do to someone
   already scrolling, and it is entirely avoidable: the sizes are known in advance.

   `min-height` rather than `height` so a creative that is slightly off-size grows the box
   instead of being clipped. Centred both ways so an undersized one sits properly. */
.ad-unit {
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; margin: 0 auto;
}
.ad-unit > * { max-width: 100%; }
.ad-unit img { display: block; height: auto; }

/* 320x50 is the base case: mobile-first, like everything else here. */
.ad-leaderboard .ad-unit { min-height: 50px; max-width: 320px; }

/* 300x250 at every width. On a wide screen it sits under the map in the column that is
   otherwise empty; on a phone that column is simply the next thing in the flow, so the unit
   becomes a centred inline block without changing shape. One size, one reserved height, no
   breakpoint where the sidebar ad can move anything. */
.ad-sidebar .ad-unit { min-height: 250px; max-width: 300px; }

@media (min-width: 740px) {
  .ad-leaderboard .ad-unit { min-height: 90px; max-width: 728px; }
}

/* The placeholder. Dashed rather than solid so it reads as "reserved" rather than as a real
   placement that failed to load, and built entirely from type — no logo, no product, no
   brand of any kind. See the partial for why that matters. */
.ad-placeholder {
  flex-direction: column; gap: 2px;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  background:
    repeating-linear-gradient(45deg,
      transparent, transparent 10px,
      rgba(18, 40, 90, .02) 10px, rgba(18, 40, 90, .02) 20px);
  color: var(--ink-soft);
  text-align: center;
  width: 100%;
}
.ad-ph-size {
  font-size: .65rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-soft); opacity: .75;
}
.ad-ph-copy { font-size: .95rem; font-weight: 600; color: var(--navy); }
.ad-ph-link { font-size: .75rem; color: var(--blue); }

/* At 320x50 there is no room for three stacked lines, so the size label goes and the other
   two sit on one row. The box still reserves exactly 50px. */
@media (max-width: 739px) {
  .ad-leaderboard .ad-placeholder { flex-direction: row; gap: 8px; }
  .ad-leaderboard .ad-ph-size { display: none; }
  .ad-leaderboard .ad-ph-copy { font-size: .85rem; }
  .ad-leaderboard .ad-ph-link { font-size: .7rem; }
}

/* ---------- footer ---------- */

.site-footer {
  margin-top: 32px; padding: 24px 0 40px;
  background: var(--navy); color: rgba(255, 255, 255, .8);
}
.footer-tag { margin: 0 0 10px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; font-size: .8rem; color: #fff; }
.footer-small { margin: 4px 0; font-size: .85rem; }
.site-footer a { color: #9dc2ff; }

/* ---------- wider screens ---------- */

/* **Below 480px the wordmark keeps its words and the controls give theirs up.** It used to be
   the other way round: `.brand-text { display: none }` left the pin alone on the most common
   viewport, which is the worst place to economise on the site's own name.
   The pixels come from the add button and sign-out, both of which stay legible as glyphs and
   both of which carry an aria-label, so nothing is lost to a screen reader. */
@media (max-width: 479px) {
  .brand-text { display: inline; font-size: .98rem; }

  /* Icon-only: no label, and square-ish padding so the button does not keep the width its text
     used to need. */
  .header-add-text, .header-add-short { display: none; }
  .header-add { padding-left: 10px; padding-right: 10px; gap: 0; }
  .signout-text { display: none; }
  .signout-button { padding: 8px 2px; text-decoration: none; }
  .signout-icon { display: block; width: 20px; height: 20px; }

  /* The cluster is `flex: none; white-space: nowrap` by design — a generated handle must never
     break across lines — so it cannot give way, and the wordmark returning meant the row no
     longer fitted 375px: brand 151 + account 208 + gap in a 343px row, i.e. a 12px sideways
     scroll. The width comes back from the gaps between five controls rather than from dropping
     one of them, since each is a distinct affordance and the moderation link in particular has
     no other entry point on the site. */
  /* The signed-OUT header has to fit too, which the first version of this did not check: with
     "Sign in / Register" the cluster measured 261px against a 343px row, so the page scrolled
     sideways by 57px for every visitor who was not logged in — i.e. almost all of them. */
  /* Hidden wider than this too — see the note below, which is where the width actually runs out. */

  .header-account { gap: 5px; }
  .header-user { gap: 5px; }
  .picker-button { padding-left: 6px; padding-right: 6px; }
  .header-admin, .header-business { padding-left: 2px; padding-right: 2px; }
  /* A third door is one too many for 375px once the handle and sign-out are on the row. The
     audit is reachable from /me at every width, so this is the one that stands down. */
  .header-business + .header-business { display: none; }
  /* Three pixels, and they come from the row that needs them rather than from every reader's
     header. See the two-door note below. */
  .header-user:has(.header-admin):has(.header-business) { gap: 3px; }
}

/* **The row with two doors on it is a different row, and it is the one that does not fit.**
   A moderator who also holds a listing carries both the shield and the block, and between 480px
   and 620px that row still showed "Sign out" in words — 511px of controls in a 480px viewport.
   Neither door can be dropped: each is the only entry point to its surface, and which one a
   person needs depends on which hat they are wearing.

   So the words go instead, on that row only, at widths where an ordinary reader keeps them.
   Sign-out already has an icon and a visually-hidden name for exactly this, so nothing is lost
   and nothing new had to be built. `:has()` rather than a body class because the condition is
   "this row has both", which is a fact about the markup and not about the page. */
@media (max-width: 620px) {
  /* **The signed-out row runs out of width before 480px, not at it.** The note above records
     fixing this at 375px and the fix was scoped to 479px, so at exactly 480 "Sign in / Register"
     still rendered in full and the header scrolled sideways by 24px — for every visitor who is
     not signed in, which is almost all of them. Found by measuring the states rather than the
     one being worked on; it predates the two-door row below and is not caused by it. */
  .signin-extra { display: none; }

  .header-user:has(.header-admin):has(.header-business) .signout-text { display: none; }
  .header-user:has(.header-admin):has(.header-business) .signout-icon {
    display: block; width: 20px; height: 20px;
  }
}

@media (min-width: 480px) {
  .signout-text { display: inline; }
}

@media (min-width: 720px) {
  /* Wide enough for one row: brand, search, account. display:contents dissolves the
     mobile-only .header-top wrapper so its children join the same flex line. */
  .header-inner { display: flex; align-items: center; gap: var(--gap); }
  .header-top { display: contents; }
  .brand { order: 1; flex: none; }
  /* The one elastic element on the row, and the only one allowed to shrink. */
  .header-search { order: 2; margin-top: 0; flex: 1 1 8rem; min-width: 0; }

  /* **`margin-left: auto`, because the search box is not always there.** The row's spacing came
     entirely from `.header-search` flexing to fill it, so on a page that suppresses the box —
     the home page, and any other that grows its own — brand and account bunched at the left
     with the rest of the row empty. An auto margin pushes the account block right whether or
     not anything elastic precedes it, and is worth nothing when the search box is present,
     since it has already taken the free space. */
  .header-account { order: 3; margin-left: auto; }

  /* Room for the handle in full. The avatar stands down rather than doubling up. */
  .header-handle { display: inline; }
  .header-avatar { display: none; }

  /* **A square target, but only where the row has the width to give.** Below this the two icon
     doors fall back to padding-sized boxes: the vertical target stays 40px, and the horizontal
     one matches the picker and sign-out glyphs already sitting beside them. Forcing 40px wide at
     every width cost 14px a door and put the 480px row 7px over for every moderator — measured,
     after shipping it that way once. */
  .header-admin, .header-business { min-width: 40px; }
}

@media (min-width: 640px) {
  h1 { font-size: 2rem; }
  .hero h1 { font-size: 2.2rem; }
  .search-panel { flex-direction: row; }
  .search-panel .btn, .venue-actions .btn, .spot-actions .btn { width: auto; }
  .btn-locate { width: auto; }
  .spot-list { grid-template-columns: repeat(2, 1fr); }
  /* A hole bottom-right reads as a missing card rather than as the end of a list. An odd last
     card takes the whole row instead — which is true at any column count, and needs no guess
     about how many items the page happened to fetch. */
  .spot-list > li:last-child:nth-child(odd) { grid-column: 1 / -1; }
  .how-grid { grid-template-columns: repeat(3, 1fr); }
  .map { height: 340px; }
  .me-stats { grid-template-columns: repeat(4, 1fr); }
  .rating-choice { flex-direction: row; flex-wrap: wrap; }
  .rating-option { flex: 1 1 auto; }

  /* Wide enough to pack label and control to the left instead of spreading them to the
     edges: the answer stays a reading distance from its question, and the controls still
     line up because the label column is fixed. */
  .fact-row { grid-template-columns: 14rem auto; justify-content: start; }
  .fact-row-control { justify-self: start; }
  .field-row { grid-template-columns: repeat(2, 1fr); }
  .field-grid { grid-template-columns: repeat(2, 1fr); }
  .edit-actions .btn { flex: 0 0 auto; }
}

/* "Add a bathroom" in full only once the row can afford it. The account block is rigid, so
   every word added here comes out of the search field: at 768px with a twenty-character
   handle the full label would leave the search box about 50px wide. Below this it is a
   plus and the word "Add", which is the same button. */
@media (min-width: 1024px) {
  .header-add-text { display: inline; }
  .header-add-short { display: none; }
}

@media (min-width: 900px) {
  .search-layout { grid-template-columns: minmax(0, 1fr) minmax(0, 420px); align-items: start; }
  .search-results .spot-list { grid-template-columns: 1fr; }
  .search-map { position: sticky; top: 16px; }
  .map { height: calc(100vh - 120px); max-height: 640px; }
  .page-venue .venue-map-wrap { max-width: 520px; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* A flag sitting inline in a sentence, aligned to the text baseline. */
.inline-flag { display: inline-block; vertical-align: -2px; line-height: 0; }
.inline-flag .flag {
  width: 20px; height: 13px; border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(18, 40, 90, .15);
}

/* ---------- moderation ---------- */

.admin { padding-top: 20px; padding-bottom: 40px; }
.admin-lead { margin: 0 0 16px; color: var(--ink-soft); font-size: .95rem; }

/* Tabs carry their own counts: a queue you have to open to discover is a queue that grows. */
.admin-nav {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
  margin: 0 0 20px; padding: 4px 6px;
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
}
/* **A group wraps as a unit, which is what makes the wrap readable.** Eleven links on one line
   broke wherever the width ran out, mid-list, so the second row was a continuation of nothing.
   The groups also say what the links are: seven things waiting on a person, three you go to. */
.admin-nav-group {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
  padding-left: 10px; border-left: 1px solid var(--line);
}
.admin-nav-home { font-weight: 700; }
@media (max-width: 640px) {
  /* At phone width the divider lands mid-column, where it separates nothing. */
  .admin-nav-group { padding-left: 0; border-left: 0; }
}
/* **Sized to fit eleven on one line**, which the original 14px padding and .95rem could not:
   measured at 1440 it needed 1290px against 1216 available. The tap target keeps its 44px
   height — the padding that came off is horizontal, which costs nothing to a thumb. */
.admin-nav a {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 9px; min-height: 44px;
  border-radius: var(--radius-sm); text-decoration: none;
  color: var(--ink-soft); font-weight: 600; font-size: .9rem;
  white-space: nowrap;
}
.admin-nav a:hover { background: var(--paper); color: var(--navy); }
.admin-nav a.is-current { background: var(--navy); color: #fff; }
.admin-badge {
  display: inline-grid; place-items: center; min-width: 22px; height: 22px; padding: 0 6px;
  border-radius: 999px; background: var(--blue); color: #fff;
  font-size: .75rem; font-variant-numeric: tabular-nums;
}
.admin-badge-warn { background: var(--amber); color: #4a3000; }
.admin-nav a.is-current .admin-badge { background: rgba(255, 255, 255, .25); }

.admin-section { margin: 0 0 28px; }
.admin-subhead { margin: 18px 0 8px; font-size: .95rem; color: var(--ink-soft); }
.admin-count { font-weight: 400; color: var(--ink-soft); font-size: .9rem; }

.admin-tiles { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; grid-template-columns: repeat(2, 1fr); }
.admin-tiles-sm { grid-template-columns: repeat(2, 1fr); }
.admin-tile {
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px;
}
.admin-tile a { text-decoration: none; color: inherit; display: block; }
.admin-tile.is-live { border-color: var(--blue); background: rgba(26, 109, 255, .05); }
.admin-tile.is-warn { border-color: rgba(245, 166, 35, .5); background: rgba(245, 166, 35, .08); }
.is-dormant { opacity: .55; }
.tile-n { display: block; font-size: 1.7rem; font-weight: 700; color: var(--navy); font-variant-numeric: tabular-nums; line-height: 1.1; }
.tile-l { display: block; font-size: .85rem; color: var(--ink-soft); }
.tile-sub { display: block; margin-top: 6px; font-size: .78rem; color: var(--ink-soft); }
.tile-spark { display: block; margin-top: 8px; }
.spark { display: block; width: 100%; height: 32px; }

/* The queue: a checkbox, the item, and who sent it. */
.queue-form { margin: 0; }
.queue-bar {
  position: sticky; top: 0; z-index: 20;
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  padding: 10px; margin: 0 0 14px;
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.queue-reason {
  flex: 1 1 12rem; min-width: 0;
  font: inherit; font-size: 16px; padding: 8px 10px; min-height: 40px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.queue-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.queue-item {
  display: flex; gap: 12px; align-items: flex-start;
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 14px;
}
.queue-check { flex: none; display: grid; place-items: center; min-width: 24px; min-height: 24px; }
.queue-check input { width: 22px; height: 22px; accent-color: var(--blue); }
.queue-body { min-width: 0; flex: 1 1 auto; }
.queue-body h3 { margin: 0 0 4px; font-size: 1.02rem; }
.queue-kind { font-weight: 400; font-size: .88rem; color: var(--ink-soft); }
.queue-meta { margin: 0 0 6px; font-size: .85rem; color: var(--ink-soft); }
.queue-text { margin: 6px 0; font-size: .95rem; }
.queue-rating { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
.queue-author { font-size: .85rem; }

/* Everything a moderator needs to judge one item, on the item. The thumbnail and the text
   sit side by side on a wide screen and stack on a narrow one — a queue gets worked
   through on a phone as often as at a desk. */
.queue-detail {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-start; margin: 8px 0;
}
.queue-detail-text { min-width: 0; flex: 1 1 14rem; }

/* One OSM tile, offset inside a smaller window, with the pin drawn on top. The image is
   absolutely positioned so the inline `left`/`top` from h.mapThumb can place it. */
.queue-thumb {
  position: relative; display: block; flex: none; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: #e8eef7; text-decoration: none;
}
.queue-thumb img { position: absolute; display: block; max-width: none; }
.queue-thumb-pin {
  position: absolute; width: 14px; height: 14px; margin: -7px 0 0 -7px;
  border-radius: 50%; background: var(--blue); border: 2px solid #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .5);
}
.queue-thumb-coords {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 2px 5px; font-size: .72rem; color: #fff;
  background: rgba(11, 26, 51, .72); font-variant-numeric: tabular-nums;
}
.queue-thumb-empty {
  display: grid; place-items: center; width: 220px; height: 150px; margin: 0;
  font-size: .85rem; color: #8d1c1c;
  border: 1px dashed rgba(163, 32, 32, .4); border-radius: var(--radius-sm);
}

/* The address as typed, on its own lines — this is being read against a pin. */
.queue-address {
  margin: 0 0 8px; font-style: normal; font-size: .9rem; white-space: pre-line;
  line-height: 1.35;
}
.queue-address-empty, .queue-facts-empty, .queue-text-empty {
  color: var(--ink-soft); font-style: italic; font-size: .88rem; margin: 0 0 8px;
}

/* Stated facts, two columns of label/value that stay readable when one value is "Yes". */
.queue-facts {
  margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 2px 12px; font-size: .86rem;
}
.queue-fact { display: flex; gap: 6px; min-width: 0; }
.queue-fact dt { color: var(--ink-soft); }
.queue-fact dd { margin: 0; font-weight: 600; }

.queue-ratings { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 4px 0; }
blockquote.queue-text {
  border-left: 3px solid var(--line); padding: 2px 0 2px 10px;
  white-space: pre-line;
}
.queue-author-meta { color: var(--ink-soft); }
.is-warn-text { color: #8a5a06; font-weight: 600; }

/* Reports, grouped by target with the content inline. */
.report-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; }
.report-group {
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
}
.report-head h2 { margin: 0 0 8px; font-size: 1.05rem; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.report-count { font-weight: 400; font-size: .85rem; color: var(--ink-soft); }
.report-where { margin: 0 0 6px; }
.report-text {
  margin: 8px 0; padding: 8px 12px; border-left: 3px solid var(--line);
  color: var(--ink); font-size: .95rem;
}
.report-author { margin: 0 0 8px; font-size: .85rem; color: var(--ink-soft); }
.report-reasons { list-style: none; margin: 8px 0; padding: 0; display: grid; gap: 6px; font-size: .85rem; }
.report-detail { margin: 4px 0 0; color: var(--ink-soft); }
.report-when { color: var(--ink-soft); }
.report-actions { display: grid; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
.report-buttons { display: flex; flex-wrap: wrap; gap: 8px; }

/* Disputed facts. */
.dispute-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.dispute {
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
}
.dispute.is-live { border-color: rgba(245, 166, 35, .6); background: rgba(245, 166, 35, .06); }
.dispute-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; }
.dispute-head h2 { margin: 0; font-size: 1.02rem; }
.dispute-where { font-weight: 400; font-size: .9rem; color: var(--ink-soft); }
.dispute-current { margin: 8px 0 4px; }
.dispute-challengers { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.dispute-votes { font-size: .82rem; color: var(--ink-soft); }
.dispute-links { margin: 8px 0 0; font-size: .85rem; }
.dispute-actions { display: grid; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }

/* Users. */
.user-filters .filter-select input { font: inherit; font-size: 16px; padding: 8px 10px; min-height: 40px; border: 1px solid var(--line); border-radius: var(--radius-sm); }
.table-scroll { overflow-x: auto; }
.admin-table { width: 100%; border-collapse: collapse; font-size: .92rem; }
.admin-table th, .admin-table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; }
.admin-table thead th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-soft); }
.admin-table tbody tr:hover { background: var(--paper); }
.admin-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.admin-email { color: var(--ink-soft); }

.user-actions { display: grid; gap: 12px; }
.user-action {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  background: var(--white); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 14px; margin: 0;
}
.user-action label { font-weight: 600; font-size: .9rem; flex: 0 0 8rem; }
.user-action select, .user-action input[type="text"] {
  font: inherit; font-size: 16px; padding: 8px 10px; min-height: 40px;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
}
.user-action .field-hint { flex: 1 1 100%; margin: 0; }

/* The one action with no undo gets its own colour and its own confirmation. */
.btn-danger { background: #fff; color: #a32020; border-color: rgba(163, 32, 32, .4); }
.btn-danger:hover { background: rgba(163, 32, 32, .08); border-color: #a32020; }
.danger-zone { margin-top: 18px; border: 1px solid rgba(163, 32, 32, .35); border-radius: var(--radius); background: rgba(163, 32, 32, .03); }
.danger-zone > summary { cursor: pointer; padding: 12px 16px; font-weight: 700; color: #a32020; min-height: 44px; display: flex; align-items: center; }
.danger-body { padding: 0 16px 16px; }
.danger-body p { font-size: .9rem; }

.history-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.history-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; padding: 8px 10px; border-radius: var(--radius-sm); }
.history-row:nth-child(even) { background: var(--paper); }
.history-what { flex: 1 1 12rem; min-width: 0; }
.history-meta { color: var(--ink-soft); }
.history-when { font-size: .82rem; color: var(--ink-soft); }

.log-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.log-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; padding: 8px 10px; border-radius: var(--radius-sm); font-size: .88rem; }
.log-row:nth-child(even) { background: var(--paper); }
.log-action {
  font-weight: 700; font-size: .75rem; text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 8px; border-radius: 999px; background: rgba(18, 40, 90, .08); color: var(--navy);
}
.log-action-suspend, .log-action-anonymise, .log-action-remove, .log-action-reject { background: rgba(163, 32, 32, .1); color: #a32020; }
.log-action-publish, .log-action-unsuspend, .log-action-restore { background: rgba(34, 170, 75, .12); color: #17743a; }
.log-action-auto_hide { background: rgba(245, 166, 35, .16); color: #8a5a06; }
.log-target { color: var(--ink-soft); }
.log-system { font-style: italic; color: var(--ink-soft); }
.log-when { margin-left: auto; color: var(--ink-soft); font-size: .8rem; }
.log-reason { flex: 1 1 100%; margin: 2px 0 0; color: var(--ink-soft); }

/* Reporting, from the reader's side. */
.report-line { margin: 4px 0 0; }
.report-link { font-size: .82rem; color: var(--ink-soft); text-decoration: underline; }
.report-link:hover { color: #a32020; }
.report-sub { font-size: .9rem; color: var(--ink-soft); }
.reason-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.reason-option {
  display: flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 8px 12px; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
}
.reason-option:hover { background: var(--paper); }
.reason-option input { width: 20px; height: 20px; accent-color: var(--blue); flex: none; }
.reason-option:has(input:checked) { border-color: var(--blue); background: rgba(26, 109, 255, .07); }

@media (min-width: 640px) {
  .admin-tiles { grid-template-columns: repeat(4, 1fr); }
  .admin-tiles-sm { grid-template-columns: repeat(6, 1fr); }
}

/* ---------- saved places ---------- */

.save-form { margin: 0; display: inline-flex; }
.save-btn .icon { width: 18px; height: 18px; }
/* Filled bookmark and the word "Saved": the state is never carried by colour alone. */
.save-btn.is-on { background: rgba(26, 109, 255, .1); border-color: var(--blue); color: var(--blue-dark); }
.link-button-quiet { color: var(--ink-soft); font-size: .82rem; }
.link-button-quiet:hover { color: #a32020; }

/* ---------- admin: photo queue ---------- */
/* The image is the row here, not a decoration on it, so the layout is image-then-caption at
   every width rather than a table that happens to contain a picture. */
.admin-photo {
  display: flex;
  gap: 1rem;
  padding: 1rem 0;
  border-top: 1px solid var(--line);
  align-items: flex-start;
}
.admin-photo.is-blocked { background: color-mix(in srgb, var(--amber) 12%, transparent); }
.admin-photo-image img {
  display: block;
  max-width: 220px;
  height: auto;
  border-radius: 6px;
  border: 1px solid var(--line);
}
.admin-photo-body { flex: 1 1 auto; min-width: 0; }
.admin-photo-body h2 { font-size: 1.05rem; margin: 0 0 .25rem; }
.admin-photo-meta, .admin-photo-scan { margin: .2rem 0; font-size: .9rem; }
.admin-photo-review {
  margin: .5rem 0; padding-left: .75rem;
  border-left: 3px solid var(--line); font-size: .9rem;
}
.admin-flag-blocked  { color: #b3261e; }
.admin-flag-review   { color: #8a5a00; }
.admin-flag-unscanned { color: #8a5a00; }
.admin-photo-actions {
  display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-top: .5rem;
}
.admin-photo-actions input[type="text"] { flex: 1 1 12rem; min-width: 0; }
.admin-photo-override, .admin-photo-danger {
  display: inline-flex; align-items: center; gap: .35rem; font-size: .85rem;
}
/* Set apart from the reversible controls: this is the one button on the page that destroys
   something, and it should not sit in the same visual run as "Hide". */
.admin-photo-danger { margin-left: auto; padding-left: .75rem; border-left: 1px solid var(--line); }
.btn-danger { background: #b3261e; color: #fff; border-color: #b3261e; }
.admin-photo-note { font-size: .8rem; color: var(--muted, #666); margin: .4rem 0 0; }
.admin-subnav { display: flex; gap: .75rem; margin: .5rem 0 1rem; font-size: .9rem; }
.admin-subnav .is-current { font-weight: 600; text-decoration: none; }

@media (max-width: 640px) {
  .admin-photo { flex-direction: column; }
  .admin-photo-image img { max-width: 100%; }
  .admin-photo-danger { margin-left: 0; padding-left: 0; border-left: 0; }
}

/* ---------- photo policy and upload ---------- */
/* The policy is read before choosing a file, so it comes before the input rather than under it
   as small print. A rule nobody reads until after they have been refused is not a rule. */
.photo-upload { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--line); }
.photo-policy { margin: 0 0 1rem; }
.photo-policy-title { font-size: 1rem; margin: 0 0 .25rem; }
.photo-policy-summary { margin: 0 0 .75rem; font-weight: 500; }
.photo-policy-list { list-style: none; padding: 0; margin: 0; }
.photo-policy-rule {
  display: grid;
  grid-template-columns: 1.25rem 1fr;
  gap: .1rem .5rem;
  margin-bottom: .6rem;
  font-size: .9rem;
  line-height: 1.4;
}
.photo-policy-rule strong { grid-column: 2; }
.photo-policy-detail { grid-column: 2; color: var(--muted, #555); }
/* Fill, not colour alone: the tick and cross must survive a colourblind reader, which is the
   same reasoning as the solid/hollow map pins. */
.photo-policy-mark { grid-row: 1 / span 2; font-weight: 700; }
.photo-policy-rule.is-yes .photo-policy-mark { color: var(--green); }
.photo-policy-rule.is-no  .photo-policy-mark { color: #b3261e; }
.photo-policy-note { font-size: .85rem; color: var(--muted, #555); margin: .5rem 0 0; }
.photo-policy-details summary { cursor: pointer; font-weight: 600; margin-bottom: .5rem; }
.photo-policy-moderation { margin-bottom: 1rem; }

.photo-upload-form { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; }
.photo-upload-form label { flex: 1 0 100%; font-weight: 500; }
.photo-mine { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 1rem; }
.photo-mine li { display: flex; flex-direction: column; gap: .25rem; max-width: 120px; }
.photo-mine img { border-radius: 6px; border: 1px solid var(--line); height: auto; }
.photo-mine-status { font-size: .75rem; color: var(--muted, #555); }

/* ---------- choosing photos ---------- */
.photo-field { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
.photo-field-title { font-size: 1.05rem; margin: 0 0 .5rem; }
.field-optional { font-weight: 400; color: var(--ink-soft); }

/* One row per chosen file: name, size, and the way out. The Remove button gets a full tap target
   because it is pressed by somebody who has just made a mistake. */
.photo-chosen-list { list-style: none; padding: 0; margin: .6rem 0 0; display: grid; gap: .4rem; }
.photo-chosen-list li {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  font-size: .9rem; min-width: 0;
}
.photo-chosen-list li > span:first-child {
  flex: 1 1 8rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.photo-chosen-size { color: var(--ink-soft); }
/* Marked on the row itself, not only in the message below, so with three files it is obvious
   which one is the problem. */
.photo-chosen-list li.is-too-big > span:first-child { color: #a32020; font-weight: 600; }
.photo-chosen-list .btn-small { min-height: 3rem; padding: .35rem .9rem; }

.photo-mine-title { font-size: .95rem; margin: 1rem 0 .4rem; }
.photo-mine { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 1rem; margin: 0; }
.photo-mine li { display: flex; flex-direction: column; gap: .25rem; max-width: 96px; }
.photo-mine img { border-radius: 6px; border: 1px solid var(--line); height: auto; }
.photo-mine-status { font-size: .75rem; color: var(--ink-soft); }
.photo-mine .btn-small { min-height: 2.5rem; padding: .2rem .5rem; font-size: .75rem; }

/* ---------- photos attached to a review ---------- */
/* Sized so several sit on one line on a phone without pushing the review text off screen. The
   review is what someone came to read; the photos support it rather than lead it. */
.review-photos {
  list-style: none; padding: 0; margin: .6rem 0 0;
  display: flex; flex-wrap: wrap; gap: .5rem;
}
.review-photos img {
  display: block; width: 120px; height: 90px; object-fit: cover;
  border-radius: 6px; border: 1px solid var(--line);
}

/* ---------- facts asked with a review ---------- */
/* Quieter than the ratings above: these are optional, and a block that looks as demanding as the
   required fields is what makes a form feel long. The controls inherit `.field`, so the select
   and the number input match every other input on the site rather than being raw browser
   defaults — 16px text so iOS does not zoom, 44px tall, same border and radius. */
.review-facts { border: 0; padding: 0; margin: 1.5rem 0 0; }
.review-facts legend { font-weight: 600; padding: 0; }
.fact-ask {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: .25rem .75rem;
  padding: .55rem 0;
  border-bottom: 1px solid var(--line);
}
.fact-ask-label { font-size: .95rem; font-weight: 500; }
.fact-ask-current { color: var(--ink-soft); font-size: .85rem; font-weight: 400; }
/* Said in words rather than by colour alone, for the same reason the map pins use fill. */
.fact-ask-state {
  font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .02em;
  color: var(--ink-soft); margin-left: .35rem;
}
.fact-ask-contested { color: #a32020; }
.fact-ask-choices { display: flex; gap: .6rem; justify-self: end; }
/* 48px rows: these get tapped on a pavement. */
.fact-ask-choices label {
  display: inline-flex; align-items: center; gap: .3rem;
  min-height: 3rem; font-size: .9rem; white-space: nowrap;
}
/* The select and number input are the two that looked wrong. Width is capped so they sit at the
   right of the row like the radio group, instead of stretching the full grid column. */
.fact-ask select, .fact-ask input[type="number"] {
  justify-self: end;
  width: auto; min-width: 9rem; max-width: 100%;
}
.fact-ask input[type="number"] { min-width: 6rem; }
.review-facts-more { margin-top: .75rem; }
.review-facts-more summary { cursor: pointer; font-size: .9rem; font-weight: 600; min-height: 2.75rem; display: flex; align-items: center; }
.fact-ask-count { font-weight: 400; color: var(--ink-soft); margin-left: .35rem; }

@media (max-width: 480px) {
  /* Stack, so a long label and a three-option radio group do not fight over one line. */
  .fact-ask { grid-template-columns: 1fr; }
  .fact-ask-choices, .fact-ask select, .fact-ask input[type="number"] { justify-self: start; }
  .fact-ask select { width: 100%; }
}

/* ---------- choosing a photo ---------- */
.photo-field { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
.photo-field-title { font-size: 1.05rem; margin: 0 0 .5rem; }
.field-optional { font-weight: 400; color: var(--muted, #666); }
/* The chosen file, its size, and the way out. 48px tap target on the Remove button, like every
   other control — this one gets pressed by someone who has just made a mistake. */
.photo-chosen {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  margin: .6rem 0 0; font-size: .9rem;
}
.photo-chosen-size { color: var(--muted, #666); }
/* Scoped to this control, NOT a redefinition of .btn-small — that class already exists at the
   top of the admin styles with min-height:0 and is used by the rename pencil and the report
   links. Redefining it here would have made every small button on the site 48px tall, which is
   the same shared-rule mistake .spot-actions made with .venue-actions. */
.photo-chosen [data-photo-clear] { min-height: 3rem; padding: .35rem .9rem; }
.photo-mine-wrap { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--line); }

/* ---------- photos attached to a review ---------- */
/* Sized so several sit on one line on a phone without pushing the review text off screen. The
   review is what someone came to read; the photos support it rather than lead it. */
.review-photos {
  list-style: none; padding: 0; margin: .6rem 0 0;
  display: flex; flex-wrap: wrap; gap: .5rem;
}
.review-photos img {
  display: block; width: 120px; height: 90px; object-fit: cover;
  border-radius: 6px; border: 1px solid var(--line);
}

/* ---------- facts asked with a review ---------- */
/* One row per fact, label left, choices right, wrapping to two lines on a phone. Kept visually
   quieter than the ratings above it: these are optional, and a block that looks as demanding as
   the required fields is what makes a form feel long. */
.review-facts { border: 0; padding: 0; margin: 1.5rem 0 0; }
.review-facts legend { font-weight: 600; padding: 0; }
.fact-ask {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: .25rem .75rem; padding: .5rem 0; border-bottom: 1px solid var(--line);
}
.fact-ask-label { font-size: .95rem; }
.fact-ask-current { color: var(--muted, #666); font-size: .85rem; }
.fact-ask-choices { display: flex; gap: .75rem; }
/* 48px rows, because these get tapped on a pavement. */
.fact-ask-choices label {
  display: inline-flex; align-items: center; gap: .3rem;
  min-height: 3rem; font-size: .9rem;
}
.fact-ask-number { width: 5rem; }
.photo-withdraw { margin: 0; }

/* "Posted from nearby" — a claim about what a device reported, not a verification. */
.review-onsite {
  display: inline-flex; align-items: center; gap: .25rem;
  font-size: .8rem; color: var(--muted, #555);
}
.photo-policy-more { margin-top: .5rem; }
.photo-policy-more summary { cursor: pointer; font-size: .9rem; }

/* ---------- admin: suppression table ---------- */
.admin-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.admin-table th, .admin-table td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); }
.admin-table tr.is-expired { color: var(--muted, #777); }
.admin-inline-form { display: flex; gap: .4rem; align-items: center; margin: 0; }
.admin-inline-form input[type="text"] { min-width: 0; flex: 1 1 8rem; }

/* ---------- /business: pricing ---------- */
.business { max-width: 62rem; }
.business-head { margin: 1.5rem 0 1rem; }
.business-sub { color: var(--ink-soft); margin: .25rem 0 0; }

/* The currency picker sits above the prices, because the number somebody reads has to already
   be the number they will be charged — Stripe needs a separate Price per currency, so display
   and checkout are the same question. */
.currency-picker { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 1rem 0; }
.currency-picker-label { font-size: .9rem; color: var(--ink-soft); }
.currency-options { display: flex; flex-wrap: wrap; gap: .35rem; }
/* **`color` and `appearance` are explicit, not inherited from the user agent.** This chip set a
   background and left the text colour to the UA, so the one chip with its own colour — the
   selected one — was the only one that stayed legible on a handset. `font: inherit` was already
   here for the same reason and did not go far enough. */
.currency-option {
  font: inherit; font-size: .85rem; min-height: 2.75rem; padding: .35rem .7rem;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: #fff; color: var(--ink); cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.currency-option.is-current { background: var(--navy); color: #fff; border-color: var(--navy); }
.business-market-notice { margin: 1rem 0 1.5rem; }

.business-grid { display: grid; gap: 1.25rem; grid-template-columns: 1fr; }
@media (min-width: 780px) { .business-grid { grid-template-columns: 1fr 1fr; align-items: start; } }

.product-card {
  border: 1px solid var(--line); border-radius: var(--radius); padding: 1.25rem; background: #fff;
}
/* `.product-card-coming` and `.product-status-coming` were the flag's styling — a greyed card
   and an amber "not open yet" label. They went out with it, rather than being left behind as
   rules nothing selects, which is how a removed feature comes back by accident. */
.product-card h2 { margin: 0 0 .2rem; font-size: 1.25rem; }
.product-tagline { margin: 0 0 .9rem; color: var(--ink-soft); }

/* What the product is for, above the mechanics. Full body size deliberately — it is the part
   that has to be read, so shrinking it the way a note is shrunk would be the wrong way round. */
.product-explainer { margin: 0 0 .9rem; font-size: .95rem; line-height: 1.55; }

/* Subordinate to the bold clause it follows: a qualification, not a second feature. Block so it
   starts its own line, since running on makes it read as part of the claim rather than a limit
   on it — which for the PIN note would be exactly the wrong impression. */
.feature-note {
  display: block; margin-top: .15rem;
  font-size: .88rem; color: var(--ink-soft); font-weight: 400; line-height: 1.5;
}
.product-price { margin: 0 0 .3rem; font-size: 1.6rem; line-height: 1.1; }
.product-period { font-size: .85rem; color: var(--ink-soft); font-weight: 400; }
.product-status { font-size: .8rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; margin: 0 0 1rem; }
.product-status-available { color: var(--green); }
.product-status-coming { color: #8a5a00; text-transform: none; letter-spacing: 0; font-weight: 600; font-size: .9rem; }
.product-features { margin: 0 0 1.1rem; padding-left: 1.1rem; display: grid; gap: .35rem; font-size: .92rem; }
.product-tiers { list-style: none; padding: 0; margin: 0 0 .8rem; display: grid; gap: .4rem; }
.product-tiers li { display: flex; justify-content: space-between; gap: 1rem; font-size: .95rem; }
.tier-label { color: var(--ink-soft); }
.product-cta { display: inline-block; min-height: 3rem; }
.product-note { font-size: .82rem; color: var(--ink-soft); margin: .6rem 0 0; }

/* The statement that makes a claim worth buying. Its own block, not a bullet: among features it
   reads as a limitation, alone it reads as the product. */
.business-honesty {
  margin: 2rem 0; padding: 1.25rem; border-radius: var(--radius);
  border: 2px solid var(--navy); background: #fff;
}
.business-honesty h2 { margin: 0 0 .5rem; font-size: 1.2rem; }
.business-honesty-lead { font-size: 1.05rem; font-weight: 600; margin: 0 0 .75rem; }
.business-honesty p { margin: 0 0 .7rem; }

.business-foot { margin: 2rem 0 3rem; }
.business-faq dt { font-weight: 600; margin-top: 1rem; }
.business-faq dd { margin: .25rem 0 0; color: var(--ink-soft); }

/* ---------- terms, claim, waitlist ---------- */
/* `.prose` already exists further up with its own max-width, headings, line-height and
   padding-top. Redefining those here was three duplicate rules quietly overriding a block
   somebody had already tuned, so only what is genuinely new is below. */
.prose-lead { font-size: 1.05rem; color: var(--ink-soft); margin-bottom: 1rem; }
.terms-refund { padding: .8rem 1rem; border-left: 3px solid var(--navy); background: var(--paper); }

/* The same top spacing every other page has. Without it the heading sat against the navbar,
   which `.prose` avoids with exactly this and `.business-head` with a top margin. */
.claim-page { max-width: 40rem; padding-top: 24px; }
.claim-page h1 { margin-top: 0; }
.claim-price { font-size: 1.2rem; margin: 0 0 1rem; }
/* The terms sit immediately above the pay button, which is the only place somebody reads them.
   Bordered so they are not skimmed as another field. */
.claim-terms {
  margin: 1.5rem 0; padding: 1rem 1.1rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper);
}
.claim-terms h2 { margin: 0 0 .5rem; font-size: 1rem; }
.claim-terms ul { margin: 0; padding-left: 1.1rem; display: grid; gap: .45rem; font-size: .92rem; }
.claim-terms-link { margin: .6rem 0 0; font-size: .85rem; }

/* ---------- the buying flow ---------- */

/* The quiet offer at the foot of a venue page. Deliberately not a button: this page belongs to
   somebody deciding whether to walk somewhere, and the owner is the rare visitor. */
.venue-claim-line {
  margin: 20px 0 0; padding-top: 14px; border-top: 1px solid var(--line);
  font-size: .9rem; color: var(--ink-soft);
}

/* The venue picker. One row per venue, name first, because the name is what somebody is
   scanning for and the town only disambiguates. */
.venue-picker-search { margin: 0 0 1rem; }
.venue-picker-search label { display: block; font-weight: 600; margin-bottom: .3rem; }
.venue-picker-row { display: flex; gap: 8px; }
.venue-picker-row input {
  flex: 1 1 auto; min-width: 0;
  padding: 11px 12px; font: inherit; font-size: 16px;   /* 16px stops iOS zooming on focus */
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.venue-picker-results { list-style: none; padding: 0; margin: 0 0 1rem; }
.venue-picker-results li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem;
  padding: .7rem 0; border-bottom: 1px solid var(--line);
}
.venue-picker-name { font-weight: 600; }
.venue-picker-where, .venue-picker-count { font-size: .88rem; color: var(--ink-soft); }
/* Pushed to the end of the row so the reason a venue cannot be bought reads as the answer to
   "why is there no link", rather than as another piece of its address. */
.venue-picker-taken {
  margin-left: auto; font-size: .8rem; font-weight: 600; color: var(--ink-soft);
  background: var(--paper); border: 1px solid var(--line);
  border-radius: 999px; padding: .15rem .6rem;
}
/* A venue the asker used to hold: claimable by anybody now, and a renewal for them. Green
   rather than the muted grey of `-taken`, because it is an invitation and not a refusal. */
.venue-picker-renew { color: var(--green); font-weight: 600; font-size: .85rem; }

/* Two short numeric fields side by side above 480px, stacked below — "how many sites" and "how
   many bathrooms" are one question asked twice and read better together. */
.field-pair { display: grid; gap: var(--gap); grid-template-columns: 1fr; }
@media (min-width: 480px) {
  .field-pair { grid-template-columns: 1fr 1fr; }
}

/* ---------- the cleaning audit ---------- */

/* The public reading, where a venue chose to publish it. Not a badge: it is a fact with an age,
   and dressing it up would make it look like a rating, which it is not. */
.audit-public { margin: 10px 0 0; font-size: .9rem; }
.audit-public-note { color: var(--ink-soft); }

/* Closed by default, and the scan opens it. A reader looking for a toilet gets one quiet line. */
.audit-staff { margin: 10px 0 0; font-size: .9rem; }
.audit-staff summary {
  cursor: pointer; color: var(--ink-soft); padding: 6px 0;
  min-height: 44px; display: flex; align-items: center;   /* a real tap target on a phone */
}
.audit-form { display: grid; gap: 8px; padding: 10px 0 4px; max-width: 22rem; }
.audit-form label { font-weight: 600; }
.audit-form input {
  padding: 11px 12px; font: inherit; font-size: 16px;   /* 16px stops iOS zooming on focus */
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.audit-form-buttons { display: flex; flex-wrap: wrap; gap: 8px; }

/* The holder's board. Worst first, so the room nobody has touched is the one you see. */
.audit-board-h { margin: 1.6rem 0 .6rem; }
.audit-board { list-style: none; padding: 0; margin: 0; }
.audit-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .6rem;
  padding: .7rem .8rem; border-bottom: 1px solid var(--line); border-left: 4px solid transparent;
}
.audit-room { font-weight: 600; }
.audit-when { color: var(--ink-soft); font-size: .9rem; }
/* Colour is the quick read and never the only one — the words say the same thing, because a
   colourblind reader and a printed page both lose the border. */
.audit-row-fresh { border-left-color: var(--green); }
.audit-row-stale { border-left-color: var(--amber); }
.audit-row-never { border-left-color: var(--line); background: var(--paper); }
.audit-issue { flex: 1 1 100%; font-size: .88rem; color: #8a5a00; }

.audit-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 1rem 0; }
.inline-form { display: inline; }

/* Shown once and never again, so it has to be unmissable and easy to copy by hand. */
.pin-reveal {
  margin: 1rem 0; padding: 1rem 1.2rem;
  border: 2px solid var(--navy); border-radius: var(--radius); background: #fff;
}
.pin-reveal h2 { margin: 0 0 .3rem; font-size: 1rem; }
.pin-value {
  margin: 0 0 .6rem; font-size: 2.4rem; font-weight: 700; letter-spacing: .18em;
  font-variant-numeric: tabular-nums;
}

/* ---------- door sheets ---------- */

.sheets-page { max-width: 46rem; padding-top: 24px; }
.sheets-intro h1 { margin-top: 0; }
.door-sheet {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.5rem; margin: 1.2rem 0; text-align: center; background: #fff;
}
.door-sheet-venue { margin: 0; color: var(--ink-soft); font-size: .9rem; }
.door-sheet-room { margin: .2rem 0 1rem; font-size: 1.8rem; }
.door-sheet-qr svg { width: 220px; height: 220px; max-width: 100%; }
.door-sheet-steps {
  display: inline-block; text-align: left; margin: 1rem auto 0; padding-left: 1.2rem;
  font-size: 1rem; line-height: 1.7;
}
/* **The foot of the sheet: where it belongs, and who it is from.** The building on the left so a
   stack can be sorted before it goes up; the mark and the domain on the right so somebody who
   finds one on a floor knows what it is. Replaced a typed-out URL nobody was going to key in off
   a wall. Pushed to the bottom in print, where the sheet is a whole page. */
.door-sheet-foot {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  margin: 1.2rem 0 0; padding-top: .8rem; border-top: 1px solid var(--line);
  font-size: .85rem; color: var(--ink-soft);
}
/* **Both ends on one baseline, hard against the edges.** The sheet is centred, so the site name
   sat wherever the flex box left it and read as misaligned against the right-hand mark. */
.door-sheet-where { text-align: left; flex: 0 1 auto; }
.door-sheet-brand { display: inline-flex; align-items: center; gap: .4rem; }
.sheet-mark { width: 18px; height: 18px; flex: none; }

/* **One sheet per sheet of paper.** Everything that belongs to the website is hidden: a header,
   a footer and a nav on a page taped inside a cubicle are noise, and they would push the QR onto
   a second sheet. `break-inside: avoid` keeps a code from being split across the fold. */
@media print {
  .site-header, .site-footer, .sheets-intro, .skip-link { display: none !important; }
  .sheets-page { max-width: none; padding: 0; }
  .door-sheet {
    border: none; margin: 0; padding: 0;
    break-inside: avoid; break-after: page;
    height: 100vh; display: flex; flex-direction: column; justify-content: center;
  }
  .door-sheet:last-child { break-after: auto; }
  .door-sheet-qr svg { width: 300px; height: 300px; }
  /* `margin-top: auto` inside the column flex puts the foot on the bottom edge of the page
     rather than under the code, which is where the same two things sit in the PDF. */
  .door-sheet-foot { margin-top: auto; border-top: none; }
}

/* The ordinal: what makes one sheet of four findable when all four are called "Bathroom", which
   most imported venues are. Same number on paper and on screen. */
.door-sheet-ordinal {
  margin: .2rem 0 0; font-size: 1.1rem; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.door-sheet-where { margin: 1.4rem auto 0; max-width: 22rem; text-align: left; }
.door-sheet-where-label { margin: 0; font-size: .85rem; font-weight: 700; color: var(--ink-soft); }
/* A real rule to write on. Kept in the printed page, unlike everything else that is chrome. */
.door-sheet-rule { border-bottom: 1px solid var(--line); height: 1.6rem; }
.door-sheet-where-hint { margin: .3rem 0 0; font-size: .72rem; color: var(--ink-soft); }

.audit-ordinal {
  flex: none; min-width: 1.9rem; text-align: center;
  font-variant-numeric: tabular-nums; font-weight: 700; font-size: .85rem;
  color: var(--ink-soft); background: var(--paper);
  border: 1px solid var(--line); border-radius: 999px; padding: .1rem .4rem;
}

@media print {
  /* The write-on line has to survive printing — it is the only part of the sheet a person adds
     to, and a form field with no line is a form field nobody fills in. */
  .door-sheet-rule { border-bottom: 1px solid #999; }
}

/* ---------- audit: naming, alerts, public reports ---------- */

.audit-alerts { margin: 1.2rem 0; padding: 1rem 1.1rem; border-radius: var(--radius);
                border: 2px solid var(--amber); background: #fff; }
.audit-alerts h2 { margin: 0 0 .6rem; font-size: 1rem; }
.audit-count { display: inline-block; margin-left: .3rem; padding: .05rem .45rem;
               border-radius: 999px; background: var(--amber); color: #3a2600;
               font-size: .8rem; font-weight: 700; }
.audit-alert-list { list-style: none; padding: 0; margin: 0; }
.audit-alert { padding: .6rem 0; border-top: 1px solid var(--line); }
.audit-alert:first-child { border-top: 0; }
.audit-alert-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; }
.audit-alert-note { margin: .3rem 0; }
/* **Source is a visible tag, not a footnote.** A cleaner who typed the PIN and a passer-by whose
   browser claimed a position are different kinds of evidence; showing them alike overstates one. */
.audit-tag { font-size: .72rem; font-weight: 700; padding: .1rem .5rem; border-radius: 999px;
             text-transform: uppercase; letter-spacing: .03em; }
.audit-tag-staff { background: var(--paper); color: var(--navy); border: 1px solid var(--line); }
.audit-tag-public { background: #fff3d6; color: #7a5200; border: 1px solid #e2c68a; }

.audit-names { margin: 1.4rem 0; }
.audit-name-list { list-style: none; padding: 0; margin: .8rem 0; display: grid; gap: .6rem; }
.audit-name-list li { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.audit-name-was { flex: none; min-width: 6rem; font-size: .85rem; color: var(--ink-soft); }
.audit-name-list input[type="text"] {
  flex: 1 1 12rem; min-width: 0;
  padding: 10px 12px; font: inherit; font-size: 16px;   /* 16px stops iOS zooming on focus */
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.audit-suggest { flex: none; display: flex; align-items: center; gap: .35rem;
                 font-size: .82rem; color: var(--ink-soft); min-height: 44px; }

.audit-report { margin: .6rem 0 0; }
.audit-report-details summary { cursor: pointer; color: var(--ink-soft); font-size: .9rem;
                                min-height: 44px; display: flex; align-items: center; }
.audit-geo-status { margin: .4rem 0 0; font-size: .85rem; color: var(--ink-soft); min-height: 1.2em; }
.audit-report-nojs { margin: .4rem 0 0; font-size: .85rem; color: var(--ink-soft); }

/* ---------- the claim queue ---------- */

/* **A stacked block, not `.queue-item`.** That class is `display: flex` with a checkbox column
   beside a body; a heading, a meta line, a fact grid and a form dropped into it laid out as four
   side-by-side columns, which is how the address came to run under the reason field. */
.grant-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.grant-card {
  display: grid; gap: 10px;
  background: #fff; border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
}
.grant-head { display: grid; gap: 2px; }
.grant-venue { margin: 0; font-size: 1.1rem; }
.grant-venue-plain { margin: 0; font-weight: 600; }
/* Its own line. Squeezed onto the heading row it wrapped mid-address at narrow widths. */
.grant-where { margin: 0; font-size: .9rem; color: var(--ink-soft); }
.grant-meta { margin: 0; font-size: .85rem; color: var(--ink-soft); }
.grant-paid { color: var(--green); font-weight: 600; }

/* The fact grid is `.queue-facts` used the way the pending queue uses it — one `.queue-fact` per
   pair. `min-width: 0` on the value so a long email wraps inside its cell instead of widening the
   column and pushing the next one off. */
/* Wider columns than the pending queue's, because these values are email addresses and domains
   rather than "3 stalls". At 11rem a work address broke mid-word across four lines; at 17rem it
   fits on one or two, and the grid still collapses to a single column on a phone. */
.grant-facts {
  padding: 10px 12px; background: var(--paper); border-radius: var(--radius-sm);
  /* 22rem, sized against the value these actually hold: a work email at a real organisation is
     ~45 characters, and at 17rem it still broke one character onto a second line. Two columns on
     a desktop card, one on a phone. */
  /* **`min(22rem, 100%)`, not a bare 22rem.** A bare minimum in `minmax` cannot shrink, so at
     375px the column stayed 352px wide, the card outgrew the viewport and the whole page scrolled
     sideways — which I introduced while fixing the wrapping and caught only by measuring. The
     width is sized against what these hold: a work email at a real organisation runs to about 45
     characters, and at 17rem it still broke one character onto a second line. */
  grid-template-columns: repeat(auto-fill, minmax(min(22rem, 100%), 1fr)); gap: 6px 20px;
}
/* `anywhere` only as the last resort: break at the @ and the dots first, so an address splits
   where a person would read the split, not mid-word. */
.grant-facts .queue-fact { align-items: baseline; }
.grant-facts .queue-fact dd { min-width: 0; overflow-wrap: break-word; word-break: normal; }

.grant-evidence {
  margin: 0; padding: 8px 10px; font-size: .92rem;
  border-left: 3px solid var(--line); color: var(--ink);
}

/* The form is the last row of the card and owns its full width, so the input can never sit under
   anything. Buttons wrap below it rather than beside it. */
.grant-actions { display: grid; gap: 6px; margin-top: 2px; }
.grant-reason-label { font-size: .82rem; color: var(--ink-soft); }
.grant-actions input {
  width: 100%; font: inherit; font-size: 16px;   /* 16px stops iOS zooming on focus */
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.grant-buttons { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.grant-buttons .btn { flex: 1 1 12rem; min-height: 44px; }

.grant-card-dormant { background: var(--paper); gap: 2px; }

/* ---------- the claimant's own controls ---------- */

/* **Rendered only for the holder, never hidden with CSS.** Same rule as the moderator pencil: a
   reader does not receive the markup, so the ids and action paths are not in the page for
   anybody else to find. These styles only ever apply to somebody who holds the claim. */
.owner-badge {
  display: inline-block; font-size: .7rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; padding: .12rem .5rem; border-radius: 999px;
  background: var(--navy); color: #fff;
}
.owner-strip {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
  margin: 20px 0 0; padding-top: 14px; border-top: 1px solid var(--line); font-size: .9rem;
}

/* The venue's own words. Set apart from the crowd's content rather than blended into it — a
   reader has to be able to tell at a glance whose sentence they are reading. */
.owner-description {
  margin: 14px 0; padding: 12px 14px;
  border-left: 3px solid var(--navy); background: var(--paper); border-radius: var(--radius-sm);
}
.owner-description-head { display: flex; align-items: center; gap: .5rem; margin: 0 0 .4rem; }
.owner-description-body { margin: 0; white-space: pre-line; }

/* A reply sits under its review, indented, so the thread reads as an answer rather than as a
   second review of equal weight. */
.owner-reply {
  margin: 10px 0 0 12px; padding: 10px 12px;
  border-left: 3px solid var(--navy); background: var(--paper); border-radius: var(--radius-sm);
}
.owner-reply-head { display: flex; align-items: center; gap: .5rem; margin: 0 0 .35rem; }
.owner-reply-body { margin: 0; white-space: pre-line; font-style: normal; }
.owner-reply-edit { margin: 8px 0 0 12px; font-size: .9rem; }
.owner-reply-edit summary {
  cursor: pointer; color: var(--ink-soft); min-height: 44px; display: flex; align-items: center;
}

.owner-form { display: grid; gap: 8px; max-width: 42rem; }
.owner-form textarea {
  width: 100%; font: inherit; font-size: 16px;   /* 16px stops iOS zooming on focus */
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  resize: vertical;
}
.owner-form-buttons { display: flex; flex-wrap: wrap; gap: 8px; }
.owner-form-buttons .btn { min-height: 44px; }
/* **A submit button is not a field.** `.owner-form` is a grid, so a button dropped straight into
   it stretches the full column — which on a white secondary button reads as another text input,
   and on the owner page there is one directly under every field. Sized to its own text instead,
   left-aligned with the labels above it. `.owner-form-buttons` above is untouched: it is already
   a flex row and sizes its children correctly. */
.owner-form > button, .owner-form > .btn { justify-self: start; min-height: 44px; }

.owner-section { margin: 1.6rem 0; }
.owner-section h2 { margin: 0 0 .4rem; font-size: 1.05rem; }
.owner-toggle { display: grid; gap: 8px; max-width: 42rem; }
.owner-check { display: flex; align-items: center; gap: .5rem; min-height: 44px; font-weight: 600; }
.owner-limits { padding: 1rem 1.2rem; border: 2px solid var(--navy); border-radius: var(--radius); }
.owner-limits h2 { margin-top: 0; }

/* The photo picker: thumbnails, because choosing between photos by id is not choosing. */
.owner-photo-grid {
  list-style: none; padding: 0; margin: 8px 0 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(9rem, 100%), 1fr)); gap: 10px;
}
.owner-photo-btn {
  display: grid; gap: 4px; width: 100%; padding: 6px; cursor: pointer;
  background: #fff; border: 2px solid var(--line); border-radius: var(--radius-sm);
  font: inherit; font-size: .8rem; color: var(--ink-soft);
}
.owner-photo-btn img { width: 100%; height: auto; border-radius: var(--radius-sm); display: block; }
.owner-photo-btn.is-current { border-color: var(--navy); color: var(--navy); font-weight: 700; }

/* The verified owner is among the dissenters. A label, not a weighting — the vote counts once,
   like anybody's, and the pricing page promises exactly that. */
.dispute-owner {
  display: inline-block; margin-left: .4rem; padding: .1rem .5rem; border-radius: 999px;
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  background: var(--navy); color: #fff;
}
/* `.owner-bar`, `.owner-bar-links`, `.owner-rename*` and `.owner-note` were the venue page's
   owner apparatus — a bar of anchors at the top (two of which pointed at ids nothing rendered),
   an inline rename form, and a paragraph pointing at the fact chips. All of it moved to
   /business/listing, so the rules went with it rather than being left to accumulate. */

/* Where a value came from. Quiet — it is context for a number, not a badge competing with it. */
.fact-owner {
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  color: var(--navy); background: #fff; border: 1px solid var(--navy);
  border-radius: 999px; padding: 0 .4rem; margin-left: .3rem; white-space: nowrap;
}

.owner-facts { margin: 10px 0 0; }
.owner-facts summary {
  cursor: pointer; color: var(--ink-soft); font-size: .9rem;
  min-height: 44px; display: flex; align-items: center; gap: .35rem; flex-wrap: wrap;
}
/* The verb, so the row reads as something to press rather than as a value. The marker is left
   alone deliberately — the browser's own triangle is the affordance every other disclosure on
   this site uses, and replacing it would make this one the odd one out. */
.owner-facts-action { color: var(--ink-soft); }
.owner-facts summary strong { color: var(--ink); }
/* `min()` on the minimum so the columns can shrink — a bare minmax minimum scrolls a phone
   sideways, which is a mistake this stylesheet has made once already. */
.owner-fact-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(13rem, 100%), 1fr));
  gap: 10px 14px; margin: 8px 0;
}
.owner-fact { display: grid; gap: 3px; }
.owner-fact label { font-size: .8rem; color: var(--ink-soft); }
.owner-fact select, .owner-fact input {
  font: inherit; font-size: 16px; padding: 9px 10px;
  border: 1px solid var(--line); border-radius: var(--radius-sm); min-height: 44px;
}

/* ---------------------------------------------------------------------------------------
   Claiming a whole property

   Behind a disclosure on the claim form, because the overwhelming case is one listing and the
   question must not push the ordinary path down the page.
   --------------------------------------------------------------------------------------- */
.property-block {
  margin: 0 0 1rem; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--paper);
}
.property-block > summary {
  cursor: pointer; padding: .8rem 1rem; min-height: 44px;
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
}
.property-body { padding: 0 1rem 1rem; }
.property-members { border: 0; margin: .8rem 0 0; padding: 0; }
.property-members legend { font-weight: 600; padding: 0; }
.property-member-list { list-style: none; margin: .5rem 0 0; padding: 0; }
/* Each row is a whole tap target, not just the box — this is ticked on a phone by somebody
   standing in the building they are claiming. */
.property-member-list label {
  display: flex; align-items: baseline; gap: .55rem; flex-wrap: wrap;
  padding: .5rem .2rem; min-height: 44px; cursor: pointer;
  border-bottom: 1px solid var(--line);
}
/* The seed is a sentence, not a control — it used to be a disabled checkbox, which reads as
   something switched off rather than as something necessarily included. */
.property-seed { margin: .5rem 0 .2rem; }
.property-select-all { margin: .4rem 0 0; font-size: .88rem; color: var(--ink-soft); }
.property-select-count { color: var(--ink-soft); }
/* The moderator's property card. Numbered pins so the map and the tick-list below it can be read
   against each other — "the fifth one is across the road" needs the fifth one to be findable. */
.property-card {
  margin: 0 0 1.4rem; padding: 1rem 1.1rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: #fff;
}
.property-head h2 { margin: 0; font-size: 1.15rem; }
.property-where { margin: .2rem 0 .6rem; color: var(--ink-soft); font-size: .9rem; }
.property-map {
  position: relative; display: block; overflow: hidden; max-width: 100%;
  border: 1px solid var(--line); border-radius: var(--radius-sm); margin: .6rem 0;
  background: var(--paper);
}
.property-map img { position: absolute; display: block; max-width: none; }
.property-map-pin {
  position: absolute; width: 20px; height: 20px; margin: -10px 0 0 -10px;
  border-radius: 999px; background: var(--blue); border: 2px solid #fff;
  color: #fff; font-size: .68rem; font-weight: 700; line-height: 16px; text-align: center;
  box-shadow: 0 1px 3px rgba(0,0,0,.4);
}
.property-map-note {
  position: absolute; right: 0; bottom: 0; padding: 2px 6px;
  background: rgba(255,255,255,.85); font-size: .75rem; color: var(--ink-soft);
}
.property-member-check label { border-bottom: 1px solid var(--line); }
.grant-unpaid { color: #a33; font-weight: 600; }
.grant-kind {
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-soft); margin-left: .4rem;
}
.property-member-note { color: var(--ink-soft); font-size: .85rem; }

/* ---------------------------------------------------------------------------------------
   The owner page

   One long administrative page rather than controls scattered down a public one. It is read
   top to bottom by somebody doing a job, so it wants headings that separate, a table of
   contents that jumps, and nothing competing for attention with the thing being edited.
   --------------------------------------------------------------------------------------- */
.owner-page-head { margin: 0 0 1rem; }
.owner-page-head h1 { margin: 0 0 .25rem; }
.owner-page-sub { margin: 0; font-size: .92rem; }

.owner-switch {
  display: flex; flex-wrap: wrap; gap: .3rem .9rem; margin: .6rem 0 0; font-size: .92rem;
}
.owner-switch a, .owner-switch-current {
  min-height: 44px; display: inline-flex; align-items: center;
}
.owner-switch-current { font-weight: 700; color: var(--navy); }
.owner-switch-label { color: var(--ink-soft); display: inline-flex; align-items: center; }
/* The listings on one claim. A list rather than a row of inline links: at eleven the row read as
   prose, and the one you were editing was a slightly bolder word in the middle of it. */
.owner-members {
  margin: .6rem 0 0; padding: .7rem .9rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper);
}
.owner-members-head { margin: 0 0 .4rem; font-size: .92rem; color: var(--ink-soft); }
.owner-members ul {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(17rem, 100%), 1fr));
  gap: 2px 14px;
}
.owner-members li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .4rem;
  padding: .35rem .4rem; border-radius: var(--radius-sm); min-height: 40px;
}
.owner-members li.is-current { background: #fff; box-shadow: inset 0 0 0 2px var(--navy); }
.owner-members li.is-current span:first-child { font-weight: 700; color: var(--navy); }
.owner-members-note { color: var(--ink-soft); font-size: .82rem; }

/* Which listing the controls below act on. Sits on the form, not at the top of the page, because
   the form is what a person is looking at when they need to know. */
.owner-editing {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .6rem;
  margin: 0 0 .5rem; padding-bottom: .5rem; border-bottom: 1px solid var(--line);
  font-size: .92rem; color: var(--ink-soft);
}
.owner-editing strong { color: var(--ink); }

/* ---------------------------------------------------------------------------------------
   The property index, and the chips that made eleven listings navigable
   --------------------------------------------------------------------------------------- */
.prop-card {
  margin: 0 0 1.2rem; padding: 1rem 1.1rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: #fff;
}
.prop-card-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .6rem; justify-content: space-between;
}
.prop-card-head h2 { margin: 0; font-size: 1.15rem; }
.prop-card-state {
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  padding: .12rem .5rem; border-radius: 999px; background: var(--green); color: #fff;
}
.prop-card-pending { background: var(--amber); color: #4a3000; }
.prop-card-meta { margin: .25rem 0 .6rem; color: var(--ink-soft); font-size: .9rem; }
.prop-card-warn {
  margin: .6rem 0; padding: .6rem .8rem; font-size: .9rem;
  background: rgba(245,166,35,.12); border: 1px solid rgba(245,166,35,.45);
  border-radius: var(--radius-sm);
}
.prop-card-links { margin: .7rem 0 0; display: flex; flex-wrap: wrap; gap: .8rem; align-items: baseline; }
.prop-card-note { color: var(--ink-soft); font-size: .85rem; }

/* **Chips rather than links in prose**, which is the whole difference between eleven listings
   you can scan and a paragraph you cannot. Sized so a gate label is a thumb target. */
.prop-chips { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
/* **The cap has to be on the flex item, not only on the link inside it.** A flex item's base size
   is its max-content width and it will not shrink below that without `min-width: 0`, so capping
   the anchor clipped the text and left the `li` 596px wide inside a 309px card — at 375px the
   whole page scrolled sideways. The anchor's own `max-width: …100%` then resolved against that
   596px, so it never bound either. */
.prop-chips li { min-width: 0; max-width: 100%; display: flex; }
/* Capped and clipped, because shortening is a best effort and sometimes it cannot shorten.
   A property whose members share no naming scheme falls back to full names, and four of those
   at forty characters each is the wall of text the chips replaced. The full name is on `title`
   and on the page it leads to, so the ellipsis loses nothing that cannot be recovered.
   `inline-block` rather than `inline-flex`: text-overflow does not apply inside a flex
   container, so the ellipsis silently did nothing. Line-height carries the 40px tap target. */
.prop-chips a, .prop-chips span {
  display: inline-block; max-width: min(18rem, 100%);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-height: 40px; line-height: 38px; padding: 0 .7rem; border-radius: var(--radius-sm);
  border: 1px solid var(--line); background: var(--paper);
  font-size: .9rem; font-weight: 600; text-decoration: none;
}
.prop-chips a:hover { border-color: var(--blue); }
.prop-chips li.is-current span {
  background: var(--navy); color: #fff; border-color: var(--navy);
}
.owner-sibling-chips { margin: .7rem 0 0; }

/* Where you are in what you hold. */
.owner-crumbs {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  margin: 0 0 .6rem; font-size: .9rem; color: var(--ink-soft);
}
.owner-crumbs-sep { color: var(--line-strong, #b9c0d0); }
.me-grant-covers { color: var(--ink-soft); font-size: .85rem; margin-left: .4rem; }

/* The dashboard pager. Prev/next and a position — a contributor walks these lists rather than
   jumping around them, and four numbered pagers on one page is more chrome than content. */
.pager {
  display: flex; flex-wrap: wrap; align-items: center; gap: .8rem;
  margin: .8rem 0 0; padding-top: .7rem; border-top: 1px solid var(--line);
  font-size: .9rem;
}
.pager-step {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 .3rem;
  font-weight: 600;
}
/* Shown rather than hidden at the ends, so the row does not shift under a thumb between pages. */
.pager-step.is-off { color: var(--line-strong, #b9c0d0); font-weight: 600; }
.pager-where { color: var(--ink-soft); }

/* Jump links. The page is long by design — six sections of real controls — and an owner who has
   come to change one thing should not scroll past the other five. */
.owner-toc {
  display: flex; flex-wrap: wrap; gap: .2rem 1rem;
  margin: 0 0 1.4rem; padding: .6rem .9rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper);
  font-size: .9rem;
}
.owner-toc a { min-height: 44px; display: inline-flex; align-items: center; }

/* Each section is a block a person can see the edges of. On a page this long, margins alone
   leave one continuous column of forms with no sense of where one job ends. */
.claim-page .owner-section {
  margin: 0 0 1.4rem; padding: 1rem 1.1rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: #fff;
}
.claim-page .owner-section h2 { font-size: 1.15rem; }
/* Two forms in one section (name, then description) need a rule between them or they read as
   one form with two submit buttons. */
.claim-page .owner-section .owner-form + .owner-form {
  margin-top: 1.1rem; padding-top: 1.1rem; border-top: 1px solid var(--line);
}

.owner-upload { margin: 1rem 0 0; }
.owner-upload summary {
  cursor: pointer; color: var(--ink-soft); min-height: 44px; display: flex; align-items: center;
}

.owner-reviews { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.owner-review { padding: .8rem .9rem; border: 1px solid var(--line); border-radius: var(--radius-sm); }
.owner-review-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 0 0 .4rem;
  font-size: .9rem;
}
.owner-review-spot { color: var(--ink-soft); }
.owner-review-body { margin: 0; white-space: pre-line; }
/* Inside the card the reply editor is already indented by the card; the venue page's extra
   left margin would double it. */
.owner-review .owner-reply-edit { margin-left: 0; }

/* Cancellation terms. Bordered rather than quiet: this is the paragraph somebody reads before
   deciding, and burying it would make the decision worse rather than less likely. */
.owner-cancel-terms {
  margin: 1.2rem 0 0; padding: .9rem 1rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper);
}
.owner-cancel-terms h3 { margin: 0 0 .5rem; font-size: 1rem; }
.owner-cancel-terms ul { margin: .5rem 0 0; padding-left: 1.1rem; }
.owner-cancel-terms li { margin: 0 0 .45rem; }

/* ---------------------------------------------------------------------------------------
   The verified badge, on the public listing

   For the reader, not the owner, so unlike everything above it this markup goes to everybody.
   Green rather than navy: navy is this site's structural colour and is on the owner badge, the
   dispute chip and the buttons, so a navy badge beside a heading reads as another control. It
   is a statement, and it carries a note saying what it means — a badge nobody can decode is
   decoration.
   --------------------------------------------------------------------------------------- */
.venue-verified {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  margin: .1rem 0 .5rem; font-size: .88rem;
}
.verified-badge {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .15rem .55rem .15rem .4rem; border-radius: 999px;
  background: var(--green); color: #fff;
  font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
}
.verified-badge .icon { width: 1.05em; height: 1.05em; flex: none; }
.verified-note { color: var(--ink-soft); }

/* The author's remove control, on their own photo. Quiet: it sits under a thumbnail and must not
   compete with the photo it removes. */
.photo-remove { margin: 2px 0 0; text-align: center; }

/* The moderator's lead-photo screen. Orientation is called out because it is what decides whether
   a photo survives the hero's 3.75:1 crop. */
.lead-photo-grid {
  list-style: none; padding: 0; margin: 12px 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(11rem, 100%), 1fr)); gap: 12px;
}
.lead-photo label {
  display: grid; gap: 6px; padding: 8px; cursor: pointer;
  border: 2px solid var(--line); border-radius: var(--radius-sm); background: #fff;
}
.lead-photo.is-current label { border-color: var(--navy); }
.lead-photo img { width: 100%; height: auto; border-radius: var(--radius-sm); display: block; }
.lead-photo-meta { font-size: .78rem; color: var(--ink-soft); display: grid; gap: 2px; }
.lead-photo-shape { font-weight: 700; }
.lead-photo-shape.is-landscape { color: var(--green); }
.lead-photo-shape.is-portrait { color: #8a5a00; }
.lead-photo-auto, .lead-photo-now { font-weight: 700; color: var(--navy); }

/* ---------- the cleaning audit ----------

   **The customer's own hierarchy, so the page is a list of lists.** A contract has sites, a site
   has bathrooms, and a bathroom has a name its owner wrote plus at most one link out to a public
   listing. Nothing here borrows the venue-card layout: a venue card is for a reader choosing
   where to walk, and this is somebody administering an operation. */

.audit-steps { margin: 0; padding-left: 1.2rem; line-height: 1.6; }
.audit-steps li { margin-bottom: .5rem; }

.audit-site {
  margin: 0 0 1.2rem; padding: 14px 16px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--paper);
}
.audit-site-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .6rem; }
.audit-site-head h3 { margin: 0; font-size: 1.05rem; }
.audit-site-meta { margin: 0; font-size: .85rem; color: var(--ink-soft); }

.audit-doors { list-style: none; margin: .8rem 0 0; padding: 0; }
.audit-door {
  padding: 10px 0; border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: .6rem 1rem; align-items: flex-start;
}
/* The name and the link are two jobs, and on a wide screen they sit side by side. `min-width: 0`
   on both so a long venue name shrinks rather than pushing the row sideways — the same flex-item
   rule the property chips needed. */
.audit-door-main { flex: 1 1 16rem; min-width: 0; }
.audit-door-link { flex: 1 1 18rem; min-width: 0; font-size: .9rem; }
.audit-door-name { display: flex; gap: .4rem; align-items: center; }
.audit-door-name input { flex: 1 1 auto; min-width: 0; }
.audit-door-meta { margin: .3rem 0 0; font-size: .85rem; color: var(--ink-soft); }
.audit-link-form { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.audit-link-form label { flex: 1 0 100%; font-size: .85rem; color: var(--ink-soft); }
.audit-link-form input { flex: 1 1 12rem; min-width: 0; }

.audit-add-door { margin-top: .8rem; display: flex; gap: .4rem; flex-wrap: wrap; }
.audit-add-door input { flex: 1 1 14rem; min-width: 0; }

.audit-pins { list-style: none; margin: 0; padding: 0; }
.audit-pin { padding: 12px 0; border-top: 1px solid var(--line); }
.audit-pin h3 { margin: 0; font-size: 1rem; }
.audit-pin-meta { margin: .2rem 0 .5rem; font-size: .85rem; color: var(--ink-soft); }

/* The door picker, grouped by site. Two columns where there is room, because a contract with
   twenty bathrooms is a long thin list otherwise. */
.door-picker { list-style: none; margin: .4rem 0; padding: 0; columns: 2; column-gap: 1.5rem; }
.door-picker li { break-inside: avoid; }
.door-picker-site {
  font-size: .8rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-soft); margin-top: .6rem;
}
@media (max-width: 560px) { .door-picker { columns: 1; } }

.audit-alerts, .audit-broken { list-style: none; margin: 0; padding: 0; }
.audit-alerts li, .audit-broken li {
  padding: 10px 0; border-top: 1px solid var(--line);
}
.audit-alert-where { margin: 0; font-size: .9rem; }
.audit-alert-note { margin: .3rem 0; }
.owner-section-warn { border-left: 3px solid var(--amber); padding-left: 12px; }

/* The log. Wide content scrolls inside its own box rather than making the page scroll. */
.audit-log { width: 100%; border-collapse: collapse; font-size: .9rem; }
.audit-log th, .audit-log td {
  text-align: left; padding: 8px 10px 8px 0; border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.audit-log thead th { font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-soft); }

/* ---------- the scan page ----------

   **Made for one thumb in a bathroom.** Whoever is looking at this is standing up, holding a
   phone, possibly with wet hands, and wants one of two buttons. Nothing else on it competes. */
.scan-page { max-width: 30rem; }
.scan-page h1 { margin-bottom: 1rem; }
.scan-form { margin-bottom: 1.5rem; }
.scan-actions { display: flex; flex-direction: column; gap: .6rem; margin: 1rem 0; }
.scan-actions .btn { min-height: 52px; font-size: 1.05rem; }
.scan-note { margin-bottom: .6rem; }
.scan-note textarea { width: 100%; }
.scan-public {
  margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--line);
  font-size: .9rem; color: var(--ink-soft);
}

.audit-link-list .is-reported { border-left: 3px solid var(--amber); padding-left: 12px; }

/* An explanation behind a control, for the thing somebody is about to do rather than the thing
   they are reading about. `details` because it needs no script, is keyboard-reachable for free,
   and keeps the text in the page for a screen reader — which a `title` attribute does not. */
.field-info { margin: .4rem 0 .8rem; font-size: .9rem; }
.field-info > summary {
  display: inline-flex; align-items: center; gap: .4rem;
  cursor: pointer; color: var(--ink-soft); list-style: none;
  min-height: 32px;                       /* a real target, not a 12px glyph */
}
.field-info > summary::-webkit-details-marker { display: none; }
.field-info > summary:hover { color: var(--navy); }
.field-info-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; flex: none;
  border: 1px solid currentColor; border-radius: 50%;
  font-size: .72rem; font-weight: 700; font-style: italic;
}
.field-info > p {
  margin: .4rem 0 0; padding-left: 26px;
  color: var(--ink-soft); max-width: 60ch;
}

/* The numbered setup order, inline above the sites it describes. */
.audit-steps-inline {
  margin: 0 0 1.2rem; padding-left: 1.2rem;
  font-size: .9rem; color: var(--ink-soft); line-height: 1.55;
}
.audit-steps-inline strong { color: var(--ink); }
/* "Add a site" sits below the sites it adds to, so it needs air rather than butting the last
   one — clicking it used to feel like leaving the page mid-thought. */
.audit-add-site-spaced { margin-top: 1.4rem; padding-top: 1.2rem; border-top: 1px solid var(--line); }
.audit-pin-actions { display: flex; flex-wrap: wrap; gap: .4rem 1.2rem; align-items: center; }
.audit-pin.is-off { opacity: .72; }
.audit-pin.is-off h3 { color: var(--ink-soft); }

/* Air above a question that starts a new thought rather than continuing the last one. */
.field-spaced { margin-top: 1.6rem; }

/* **The scan page's heading sat against the navbar.** It is the first thing a cleaner reads,
   standing up, and a title touching the bar above it reads as part of the chrome. */
.scan-page { padding-top: .5rem; }
.scan-page h1 { margin-top: 1.4rem; }

/* A PIN read back, behind a disclosure. Monospaced and large enough to read out over a phone
   without squinting — the whole reason somebody opens this is to tell it to another person. */
.pin-reveal .pin-value {
  display: inline-block; margin-top: .2rem; padding: .35rem .7rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.15rem; letter-spacing: .06em;
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius-sm);
  color: var(--ink); user-select: all;
}

/* The four steps at the top of the sheets page: what to do with the PDF, in order. */
.sheet-howto {
  margin: .6rem 0 1rem; padding-left: 1.2rem; line-height: 1.7; max-width: 46ch;
}

/* Who is signed in on this door sheet, above the two buttons. */
.scan-who { margin: 0 0 1rem; color: var(--ink-soft); }
.scan-public-report { margin-top: 1.2rem; }
.scan-public-report > summary { cursor: pointer; min-height: 40px; display: flex; align-items: center; }

/* A pencil that opens an edit form. `details` so it works with no JavaScript and is
   keyboard-reachable — the same reasoning as the moderator rename, which is a real link before a
   script upgrades it. */
.audit-rename > summary {
  display: flex; align-items: baseline; gap: .5rem; cursor: pointer; list-style: none;
}
.audit-rename > summary::-webkit-details-marker { display: none; }
.audit-rename > summary h3 { margin: 0; }
.rename-pencil { color: var(--ink-soft); font-size: .9em; }
.audit-rename > summary:hover .rename-pencil { color: var(--blue); }
.audit-rename-contract { margin: 0 0 .4rem; }
.audit-rename form { margin-top: .5rem; }

/* Where a site's pin actually is. A static tile, no script and no key — the same control the
   moderation queue uses, for the same reason: one image beats a WebGL canvas per row. */
.audit-site-map { margin: .6rem 0 .8rem; font-size: .9rem; }
.audit-site-map > summary { cursor: pointer; min-height: 36px; display: flex; align-items: center; }
.audit-site-thumb {
  display: block; margin: .6rem 0; max-width: 100%; height: auto;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.pin-delete { margin-top: .4rem; font-size: .9rem; }
.pin-delete > summary { cursor: pointer; color: var(--ink-soft); min-height: 32px; display: flex; align-items: center; }
.pin-delete > summary:hover { color: #a32020; }
.pin-delete form { margin-top: .5rem; }

/* The activity filters: a wrapping row of small fields above the table. */
.log-filters { display: flex; flex-wrap: wrap; gap: .6rem 1rem; align-items: flex-end; margin-bottom: 1rem; }
.log-filters .field { margin: 0; }
.log-filters label { display: block; font-size: .78rem; color: var(--ink-soft); margin-bottom: .2rem; }
.log-filters select, .log-filters input[type="date"] { min-height: 40px; }
.log-filter-actions { display: flex; align-items: center; gap: .8rem; }
.log-count { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem; margin: 0 0 .8rem; }
.audit-log thead a { text-decoration: none; }
.audit-log thead a:hover { text-decoration: underline; }
/* Wide content scrolls inside its own box rather than making the page scroll sideways. */
.table-scroll { overflow-x: auto; }

/* **A checkbox next to the words it governs.** `.owner-check` is `min-height: 44px` for a
   standalone tap target, which on a form row makes the box a large square sitting apart from its
   own label — so what it switched was a guess. Here the *label* carries the target and the box is
   the size a checkbox is. */
.owner-check-tight {
  min-height: 0; padding: .5rem 0; align-items: flex-start; gap: .55rem;
  font-weight: 600; line-height: 1.45;
}
.owner-check-tight input[type="checkbox"] {
  width: 18px; height: 18px; flex: none; margin: .15rem 0 0;
}
.owner-check-tight > span { flex: 1 1 auto; }

/* **Room between the sections on the manage page.** Every one of them is a distinct job — name
   the rooms, set the PINs, read the log — and they ran together tightly enough that the page read
   as one list. */
.page-audit .owner-section { margin-bottom: 2.4rem; }
.page-audit .owner-section + .owner-section { padding-top: 1.6rem; border-top: 1px solid var(--line); }
.page-audit .owner-section h2 { margin-bottom: .9rem; }
.page-audit .audit-site { margin-bottom: 1.6rem; }
.page-audit .stack > * + * { margin-top: 1rem; }

/* The contract name and the line under it were touching the box below them. */
.audit-rename-contract { margin-bottom: .8rem; }
.audit-rename-contract + .prose-lead { margin-top: 0; }
.audit-rename form { margin: .6rem 0 1rem; }

/* The radius badge doubles as the way into the control that changes it — a number somebody reads
   when a cleaner has just been turned away should not need finding. */
.pin-badge-link { text-decoration: none; }
.pin-badge-link:hover { border-color: var(--blue); color: var(--blue-dark); }
/* Choosing which bathroom at a venue a door is. */
.audit-choice-form fieldset { border: 0; padding: 0; margin: 0 0 .6rem; }
.audit-choice-form legend { padding: 0; font-size: .9rem; margin-bottom: .3rem; }

/* **The cleaner's row: everything on one line.** Two of the four controls are `details` elements
   carrying their own margins, so "Why disable" floated up and "Delete" sat low against the two
   plain forms beside them. The row owns the spacing; its children bring none. */
.audit-pin-actions { align-items: center; }
.audit-pin-actions > * { margin: 0; }
.audit-pin-actions > .field-info > summary,
.audit-pin-actions > .pin-delete > summary { min-height: 32px; }
.audit-pin-actions .inline-form { display: flex; align-items: center; gap: .4rem; }

/* Air between the tiles and the line under them. */
.admin-lead-spaced { margin-top: 1.1rem; }

/* What a person pays us, at the top of their moderation page. */
.admin-holdings { margin: 0 0 1.2rem; padding: .9rem 1.1rem;
  border: 1px solid var(--line); border-left: 3px solid var(--green); border-radius: var(--radius);
  background: var(--paper); }
.admin-holdings h2 { margin: 0 0 .4rem; font-size: .82rem; text-transform: uppercase;
  letter-spacing: .05em; color: var(--ink-soft); }
.admin-holdings ul { list-style: none; margin: 0; padding: 0; }
.admin-hold { padding: .2rem 0; font-size: .95rem; }
.admin-hold-state { color: var(--ink-soft); font-size: .85rem; }
.admin-hold-lapsed, .admin-hold-cancelled { opacity: .7; }

/* The unlink control on /admin/audit-links: a reason box that looks like the rest of the site,
   and a button sized like the other destructive actions rather than like a primary CTA. */
.audit-link-list .admin-item-actions {
  display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-top: .5rem;
}
.audit-link-list .admin-item-actions input[type="text"] {
  font: inherit; font-size: 15px; padding: 9px 11px; min-height: 40px;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: #fff;
  flex: 1 1 22rem; min-width: 0; max-width: 34rem;
}

/* The site name on a door sheet, hard against the left margin rather than wherever the centred
   column left it. */
.door-sheet-foot { text-align: left; }
.door-sheet-where { margin-right: auto; }
/* The second line of a cleaner's row: the explanation and the destructive action, apart from the
   two things done routinely. Deliberate rather than whatever the window width produces. */
.audit-pin-actions-secondary { margin-top: .35rem; gap: .4rem 1.2rem; }

/* **The contract header against the first section.** Raised twice: the rename block and the line
   under it sat 16px above the bordered box below, where every other gap on this page is 2.4rem,
   so it read as touching. The first fix set margins on the sections and left the header, which is
   the block that was actually too close. Measured rather than eyeballed. */
.page-audit .owner-head {
  margin-bottom: 2rem; padding-bottom: 1.2rem; border-bottom: 1px solid var(--line);
}
.page-audit .owner-head .prose-lead { margin-bottom: 0; }
.page-audit .owner-head .prop-chips { margin-top: .9rem; }

/* **Two reports, two jobs, and they were crammed together.** One is about this page being wrong
   and comes to us; one is about the room right now and goes to the cleaning contractor. Sitting
   flush against each other they read as the same question asked twice. */
.audit-report { margin-top: 1.6rem; padding-top: 1.2rem; border-top: 1px solid var(--line); }
.audit-report-details > summary { font-weight: 600; min-height: 40px; display: flex; align-items: center; }

/* What was just recorded, in place of the button that recorded it. */
.scan-done {
  margin: 0 0 1rem; padding: .8rem 1rem;
  background: #eaf7ee; border: 1px solid #bfe3c9; border-radius: var(--radius);
}
.scan-again, .scan-issue { margin-top: .8rem; }
.scan-again > summary, .scan-issue > summary {
  cursor: pointer; min-height: 44px; display: flex; align-items: center; font-weight: 600;
}
.scan-issue textarea { width: 100%; }

/* An unacknowledged-report count on the audit door, sized to sit on a 20px icon without changing
   the row's height — the header has been rebuilt twice over exactly that. */
.header-business { position: relative; }
.header-badge {
  position: absolute; top: 2px; right: -2px;
  min-width: 16px; height: 16px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--amber); color: #201400; border-radius: 8px;
  font-size: .68rem; font-weight: 700; line-height: 1;
}
