/* Elk Grove House Watch
 *
 * Sharp, decisive, honest. The object this borrows from is a printed
 * surveyor's report or a bank statement -- institutional, dense, unshowy.
 * Zilla Slab carries the headings; Public Sans (designed for public data)
 * carries the table.
 *
 * Every colour is a token with an explicit value per theme. The previous
 * revision hard-coded three hex badges and each one failed WCAG AA in one
 * theme or the other -- light and dark cannot share a single mid-tone.
 */

:root {
  color-scheme: light dark;

  /* Neutrals, tinted toward the warm hue (h=70) so surfaces and accents feel
     related. Never pure black or white. */
  --bg:      light-dark(oklch(98.5% 0.004 70), oklch(17%   0.008 70));
  --surface: light-dark(oklch(96%   0.007 70), oklch(21.5% 0.010 70));
  --text:    light-dark(oklch(22%   0.012 70), oklch(94%   0.006 70));
  --muted:   light-dark(oklch(44%   0.014 70), oklch(72%   0.012 70));
  /* --border outlines interactive components (inputs, the assumptions panel),
     so WCAG 1.4.11 demands 3:1 against the background. The first pass sat at
     1.68:1 in dark. --rule is only row separation -- decorative, exempt, and
     deliberately quieter so a 142-row table does not turn into a grid. */
  --border:  light-dark(oklch(62%   0.020 70), oklch(52%   0.020 70));
  --rule:    light-dark(oklch(90%   0.008 70), oklch(27%   0.012 70));

  /* Semantic accents. Lightness flips per theme so both clear 4.5:1;
     chroma drops as lightness rises, or high-L colours look garish. */
  --go:    light-dark(oklch(46% 0.13 150), oklch(80% 0.15 150));  /* new */
  --move:  light-dark(oklch(48% 0.14 55),  oklch(82% 0.14 70));   /* price cut */
  --warn:  light-dark(oklch(47% 0.17 25),  oklch(78% 0.14 25));   /* Mello-Roos */
  --focus: light-dark(oklch(45% 0.18 250), oklch(80% 0.16 250));

  /* 4pt scale, semantic names. */
  --space-2xs: 4px;  --space-xs: 8px;  --space-sm: 12px;
  --space-md: 16px;  --space-lg: 24px; --space-xl: 32px;

  --display: "Zilla Slab", Georgia, serif;
  --body: "Public Sans", ui-sans-serif, sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  padding: var(--space-md) var(--space-md) var(--space-xl);
  background: var(--bg);
  color: var(--text);
  font: 400 1rem/1.5 var(--body);
  font-variant-numeric: tabular-nums;
  -webkit-text-size-adjust: 100%;
}

/* Type scale: 5 steps, 1.3 ratio. Fixed rem -- this is product UI, not a
   marketing page, and fluid type in a dense table just makes rows jump. */
h1 {
  font: 700 1.6rem/1.15 var(--display);
  letter-spacing: -0.015em;
  margin: 0 0 var(--space-2xs);
}
.sub { font-size: 0.8rem; color: var(--muted); margin-bottom: var(--space-lg); }

.summary {
  font: 400 1.05rem/1.4 var(--body);
  margin: 0 0 var(--space-md);
  max-width: 60ch;
}
.summary b { font-family: var(--display); font-weight: 700; font-size: 1.15em; }

/* --- Assumptions panel ------------------------------------------------- */
/* A <details> so the phone shows data first. Opened by script on wide
   viewports, where there is room for it to sit inline. */
.assumptions {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  margin: 0 0 var(--space-lg);
}
.assumptions > summary {
  cursor: pointer;
  padding: var(--space-sm) var(--space-md);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  min-height: 44px;
  display: flex;
  align-items: center;
}
.assumptions[open] > summary { border-bottom: 1px solid var(--rule); }
.controls {
  display: flex;
  gap: var(--space-sm) var(--space-lg);
  flex-wrap: wrap;
  padding: var(--space-md);
}
.controls label {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.controls input {
  font: 400 1rem var(--body);
  font-variant-numeric: tabular-nums;
  padding: var(--space-sm);
  min-height: 44px;
  width: 8rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
}

/* --- Table ------------------------------------------------------------- */
.wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; }
caption {
  text-align: left;
  font-size: 0.75rem;
  color: var(--muted);
  padding-bottom: var(--space-xs);
  /* The caption box inherits the TABLE's width, which is ~1780px with all
     twelve columns -- so this sentence set as one 130-character line the eye
     has to track across the whole screen. Every other block on the page is
     capped; this one was missed because its width comes from the table, not
     the page. */
  max-width: 75ch;
}

th, td { padding: var(--space-sm) var(--space-md) var(--space-sm) 0; text-align: left; }
tbody td { white-space: nowrap; }
td.num, th.num { text-align: right; }

/* --- Grouping ----------------------------------------------------------
   A listing is TWO rows: its data and its photo strip. Drawing a rule after
   every row put the separator BETWEEN a listing and its own photos, so each
   strip read as belonging to the listing below it -- which made tapping a
   photo feel like it opened the wrong house. The line goes after the GROUP,
   never inside it. */
tbody tr:not(.shots):not(.divider):not(:has(+ tr.shots)) td {
  border-bottom: 1px solid var(--rule);
}
tr.shots td { border-bottom: 1px solid var(--rule); }
/* Extra room above a group, desktop only. On mobile every td is a flex item,
   so a padding here multiplies into a dozen vertical gaps and blows the card
   apart -- the mobile block sets its own spacing on the row instead. */
@media (min-width: 701px) {
  tbody tr:has(+ tr.shots) td { padding-top: var(--space-lg); }
}

thead th {
  position: sticky;
  top: 0;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  padding: 0 var(--space-md) 0 0;
  white-space: nowrap;
}
/* Real buttons, not click handlers on <th>. Keyboard reachable, announced. */
thead button {
  all: unset;
  cursor: pointer;
  display: block;
  width: 100%;
  padding: var(--space-sm) 0;
  min-height: 44px;
  font: 600 0.7rem/1 var(--body);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  box-sizing: border-box;
}
th.num button { text-align: right; }
thead button:hover { color: var(--text); }
th[aria-sort] button { color: var(--text); }
th[aria-sort="ascending"] button::after  { content: " \2191"; }
th[aria-sort="descending"] button::after { content: " \2193"; }

tbody tr:hover { background: var(--surface); }

/* The address is the tap target. Full-height block so it clears 44px.
   Scoped off .viewbtn: that anchor shares this cell but is a pill button. */
td.c-addr a:not(.viewbtn) {
  display: flex;
  align-items: center;
  min-height: 44px;
  color: var(--text);
  text-decoration-color: var(--border);
  text-underline-offset: 3px;
}
td.c-addr a:not(.viewbtn):hover { text-decoration-color: currentColor; }

/* The hero. Previously 16.8px against a 16px price -- a 5% edge is not a
   hierarchy. Display face at 1.25rem gives it the ratio the scale asks for. */
td.c-cash { font: 700 1.25rem/1.2 var(--display); }
td.c-price { color: var(--muted); }

/* Out of reach: muted colour, NOT opacity. Opacity dropped these 132 rows to
   2.85:1, below AA -- they still carry meaning she may want. */
tr.far td { color: var(--muted); }
tr.far td.c-cash { font-weight: 400; }

/* --- Flags ------------------------------------------------------------- */
.badge {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 2px var(--space-xs);
  border-radius: 3px;
  border: 1px solid currentColor;
  white-space: nowrap;
}
.badge + .badge { margin-left: var(--space-2xs); }
.new { color: var(--go); }
.cut { color: var(--move); }
.cfd { color: var(--warn); }
.guess { color: var(--muted); }

/* --- Photo strip ------------------------------------------------------- */
/* Horizontal scroller, keyboard reachable (tabindex on .strip) because a
   scroll region that only responds to a trackpad is unusable without one. */
tr.shots td { padding: var(--space-2xs) 0 var(--space-lg); }
.strip {
  display: flex;
  gap: var(--space-xs);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--space-2xs);
  scrollbar-width: thin;
}
.strip a { flex: 0 0 auto; display: block; line-height: 0; border-radius: 4px; }
.strip a:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.strip img {
  height: 128px;
  /* aspect-ratio reserves the box BEFORE the image loads. Without it every
     one of 100 photos reflowed the strip on arrival -- measured CLS 0.38,
     which is "poor" (0.1 is the threshold). Portrait shots rendered 96px wide
     against 171px for landscape, so the filmstrip visibly jerked. A uniform
     4:3 crop also gives the strip a steady rhythm to scan. */
  aspect-ratio: 4 / 3;
  width: auto;
  object-fit: cover;
  border-radius: 4px;
  background: var(--surface);
  scroll-snap-align: start;
}
.strip:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

/* --- Divider between reachable and out-of-reach ------------------------ */
tr.divider td {
  padding: var(--space-lg) 0 var(--space-sm);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  border-bottom: 1px solid var(--border);
  white-space: normal;
}

/* --- Stale-data and fallback warnings ---------------------------------- */
.stale {
  margin: 0 0 var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--warn);
  border-radius: 4px;
  color: var(--warn);
  font-size: 0.9rem;
  max-width: 60ch;
}
.warn-inline { color: var(--warn); font-size: 0.85rem; }

#reset {
  all: unset;
  cursor: pointer;
  align-self: flex-end;
  min-height: 44px;
  display: flex;
  align-items: center;
  padding: 0 var(--space-sm);
  font: 600 0.72rem/1 var(--body);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 3px;
}
#reset:hover { color: var(--text); }

/* --- Notes ------------------------------------------------------------- */
.notes { margin-top: var(--space-xl); max-width: 68ch; }
.notes p { font-size: 0.85rem; line-height: 1.6; color: var(--muted); margin: 0 0 var(--space-sm); }
.notes b { color: var(--text); font-weight: 600; }
.notes a { color: inherit; }

/* --- Focus and press --------------------------------------------------- */
/* Previous revision had zero focus rules. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Tactile confirmation on press. Transform only, so it stays on the compositor
   and cannot cause layout shift. */
thead button, #reset, .assumptions > summary, td.c-addr a:not(.viewbtn), .strip a, .strip img,
.viewbtn {
  transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1),
              color 160ms cubic-bezier(0.22, 1, 0.36, 1),
              border-color 160ms cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
thead button:active, #reset:active, .assumptions > summary:active, .viewbtn:active {
  transform: translateY(1px);
}
.strip a:active { transform: scale(0.985); }

/* Hover affordance must not move the box: the strip is overflow-x:auto, which
   forces overflow-y:auto, and the images sit flush against its top edge -- a
   lift would be clipped and could spawn a vertical scrollbar. An inset ring
   stays inside the painted area. */
@media (hover: hover) {
  .strip a:hover img { box-shadow: inset 0 0 0 2px var(--focus); }
}

/* --- Phone ------------------------------------------------------------- */
/* Below 700px the table becomes cards. The old layout put "cash needed" --
   the one number the tool exists to produce -- as the third column of an
   832px table, so on a phone she saw "$8," and had to scroll sideways. */
@media (max-width: 700px) {
  body { padding: var(--space-md) var(--space-sm) var(--space-xl); }
  .wrap { overflow-x: visible; }
  thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  table, tbody, tr, td { display: block; }

  /* A table-caption inside a display:block table shrink-to-fits and wraps to
     one word per line. Converting the table to cards means converting this
     too -- it stays in the accessibility tree either way. */
  caption {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  tbody tr {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2xs) var(--space-sm);
    padding: var(--space-lg) 0 var(--space-sm);
    border-bottom: 1px solid var(--rule);
  }
  /* A listing and its photo strip are one card: no rule between them, one
     rule after. Otherwise the strip reads as the next listing's photos. */
  tbody tr:has(+ tr.shots) { border-bottom: 0; padding-bottom: var(--space-sm); }
  tbody td { border: 0; padding: 0; }

  /* display:contents dissolves the cell so the address and the View button
     become flex items of the card and can be ordered independently. Without
     it the button inherits the address cell's order:1 and lands ABOVE the
     cash figure -- the one number the whole page exists to deliver. The card
     already dropped table semantics above (table,tr,td -> block), so this
     costs no structure that was still standing. */
  td.c-addr { display: contents; }
  td.c-addr a:not(.viewbtn) {
    flex: 1 0 100%;
    order: 1;
    font-size: 1.05rem;
    min-height: 48px;
    font-weight: 600;
  }

  td.c-cash {
    flex: 0 0 auto;
    font: 700 1.5rem/1.1 var(--display);
    text-align: left;
    order: 2;
  }
  td.c-cash::before {
    content: "Cash needed";
    display: block;
    font: 600 0.65rem/1 var(--body);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
    margin-bottom: var(--space-2xs);
  }
  /* flex-grow eats the rest of the cash line, so the secondary facts are
     forced to wrap to their own row instead of riding up beside the hero. */
  /* Two things that do NOT force a wrap here, both tried:
     - margin-left:auto absorbs all free space before flex-grow sees any
     - flex-grow itself, because flexbox settles line breaks BEFORE
       distributing free space, so growing an item cannot evict its neighbours
     The only reliable break is a real item with flex-basis:100%. c-seen is
     hidden on mobile anyway, so it is reused as that break (see below). */
  td.c-mo {
    flex: 1 1 auto;
    text-align: right;
    font-size: 1rem;
    order: 3;
  }
  td.c-mo::before {
    content: "payment then";
    display: block;
    font: 600 0.65rem/1 var(--body);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
    margin-bottom: var(--space-2xs);
  }

  /* Secondary facts collapse into one wrapped line with their units. */
  td.c-price, td.c-bd, td.c-ba, td.c-sqft, td.c-year, td.c-yard, td.c-days {
    flex: 0 0 auto;
    font-size: 0.8rem;
    color: var(--muted);
    text-align: left;
    order: 5;
  }
  td.c-price { font-weight: 600; color: var(--text); }
  td.c-bd:not(:empty)::after   { content: " bd"; }
  td.c-ba:not(:empty)::after   { content: " ba"; }
  td.c-sqft:not(:empty)::after { content: " sqft"; }
  td.c-year:not(:empty)::before { content: "built "; }
  td.c-yard:not(:empty)::before { content: "yard "; }
  td.c-days:not(:empty)::after  { content: "d listed"; }
  td.c-bd:empty, td.c-ba:empty, td.c-sqft:empty,
  td.c-year:empty, td.c-yard:empty, td.c-days:empty { display: none; }
  td.c-yard[data-yard="0"] { display: none; }

  /* Zero-height full-width flex item = a forced line break. Ordered between
     the hero pair and the secondary facts. Its date text is clipped, not
     removed, so screen readers on the desktop table still get the column. */
  tr.shots { display: block; border-bottom: 1px solid var(--rule); }
  tr.shots td { display: block; padding: 0 0 var(--space-lg); border: 0; }
  /* Bleed to the screen edges so a swipe feels like the whole width. */
  .strip { margin-inline: calc(-1 * var(--space-sm)); padding-inline: var(--space-sm); }
  .strip a { flex: 0 0 auto; display: block; line-height: 0; border-radius: 4px; }
.strip a:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.strip img { height: 156px; }

  tr.divider { display: block; padding: var(--space-lg) 0 0; }
  tr.divider td { display: block; padding: 0 0 var(--space-sm); }

  td.c-seen {
    flex: 0 0 100%;
    height: 0;
    padding: 0;
    margin: 0;
    overflow: hidden;
    order: 4;
  }
  td.c-flags { flex: 1 0 100%; margin-top: var(--space-2xs); order: 6; }
  td.c-flags:empty { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* --- Map ---------------------------------------------------------------- */
/* Hidden in markup and unhidden by script only when Leaflet actually loads,
   so a phone with no route to the CDN gets the table and no empty grey box. */
.mapwrap { margin: 0 0 var(--space-lg); }
.mapwrap h2 {
  font: 700 0.75rem/1 var(--body);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin: 0 0 var(--space-2xs);
}
.maphint {
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0 0 var(--space-xs);
  max-width: 70ch;
}
#map {
  height: 420px;
  border: 1px solid var(--border);
  border-radius: 6px;
  /* Leaflet panes stack their own z-index; without this the sticky table
     header scrolls underneath the tiles instead of over them. */
  z-index: 0;
}
@media (max-width: 700px) { #map { height: 300px; } }

/* Leaflet's popup chrome is hardcoded white. Repaint it for the dark theme. */
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
  background: var(--surface);
  color: var(--text);
}
.leaflet-popup-content { font: 400 0.85rem/1.5 var(--body); }
.leaflet-popup-content a { color: inherit; }
.leaflet-container { background: var(--surface); font: inherit; }

/* Leaflet ships white controls. Repaint so they read as part of the page,
   keeping the >=3:1 non-text contrast the rest of the UI holds to. */
/* Leaflet ships `.leaflet-container .leaflet-control-attribution` (0,2,0), so a
   bare class here lost and the bar stayed rgba(255,255,255,.8) -- muted text on
   near-white, 2.49:1 in dark mode. Match its specificity to win. */
.leaflet-bar a,
.leaflet-container .leaflet-control-attribution {
  background: var(--surface);
  color: var(--muted);
  border-color: var(--border);
}
.leaflet-bar a { color: var(--text); }
.leaflet-bar a:hover { background: var(--bg); color: var(--text); }
.leaflet-control-attribution { font-size: 0.65rem; }
.leaflet-container .leaflet-control-attribution a { color: var(--text); }

/* --- Must-have filters -------------------------------------------------- */
/* These HIDE rows, unlike the assumptions above which only re-rank them, so
   they sit in their own box rather than inside the assumptions panel. */
.filters {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  margin: 0 0 var(--space-lg);
  padding: var(--space-sm) var(--space-md) var(--space-md);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  align-items: center;
}
.filters legend {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  padding: 0 var(--space-2xs);
}
.filters label {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: 0.9rem;
  min-height: 44px;          /* touch target, same floor as the summary rows */
  cursor: pointer;
}
.filters input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--focus);
}
.filternote {
  flex: 1 0 100%;
  margin: 0;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--muted);
  max-width: 78ch;
}
.filternote b { color: var(--text); font-weight: 600; }

.pool  { color: var(--focus); }
.gated { color: var(--muted); }

/* Dues floor: a sub-control of the HOA checkbox, so it reads as subordinate
   and greys out when the checkbox it qualifies is off. */
.filters .sublabel {
  font-size: 0.78rem;
  color: var(--muted);
  gap: var(--space-2xs);
}
.filters .sublabel input {
  width: 6.5em;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: var(--space-2xs) var(--space-xs);
  font: inherit;
}
.filters .sublabel:has(input:disabled) { opacity: 0.45; cursor: not-allowed; }

/* --- View listing button ------------------------------------------------ */
/* The address has always been a link, but it did not read as the way OUT to
   the real posting. This is a real target: bordered, its own column, and
   labelled per-row for screen readers since every one says "View listing". */
.viewbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  white-space: nowrap;
  padding: var(--space-xs) var(--space-sm);
  min-height: 36px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font: 600 0.78rem/1 var(--body);
  color: var(--text);
  text-decoration: none;
}
.viewbtn:hover,
.viewbtn:focus-visible {
  border-color: var(--focus);
  color: var(--focus);
}
/* Out-of-reach rows mute their text; the button would inherit that and drop
   under 3:1. Hold the border and let only the label dim. */
tr.far .viewbtn { color: var(--muted); }
tr.far .viewbtn:hover { color: var(--focus); }

/* Sits inline after the address, which is column one and therefore the only
   cell guaranteed to be on screen -- the table is wider than most viewports. */
td.c-addr .viewbtn { margin-left: var(--space-sm); vertical-align: middle; }

/* Popup version sits under the numbers, so give it room to breathe. */
.leaflet-popup-content .viewbtn { margin-top: var(--space-xs); }

@media (max-width: 700px) {
  /* Cards: the button is the card's call to action, so it takes its own line
     and a full 44px touch target instead of sitting beside the address. */
  td.c-addr .viewbtn {
    display: flex;
    order: 7;                /* after c-flags (6): the card's closing action */
    flex: 1 0 100%;
    margin: var(--space-xs) 0 0;
    min-height: 44px;
    width: 100%;
    font-size: 0.85rem;
  }

  /* Sunlight legibility beats density for the flags specifically: they are
     the glanceable layer, and 10.9px does not survive a bright sidewalk.
     Bigger badges need somewhere to go: a listing carrying new + Mello-Roos +
     HOA + gated is four nowrap pills on a 390px card, so the row must wrap or
     it pushes the whole page sideways. */
  .badge { font-size: 0.8rem; padding: 3px var(--space-xs); }
  td.c-flags { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
  td.c-flags .badge + .badge { margin-left: 0; }   /* gap owns the spacing now */

  /* Dues floor is a real field she taps and types into; hold the 44px floor
     the checkboxes beside it already meet. */
  .filters .sublabel input { min-height: 44px; }
}

/* Map pin hover preview. Photo plus address, so the map can be swept without
   clicking every pin. The popup still owns the full detail and the outbound
   link -- this is a glance, not a replacement. Bound only on hover-capable
   pointers (see CAN_HOVER in render.py), so touch behaviour is unchanged. */
.pintip.leaflet-tooltip {
  width: 210px;
  padding: 0;
  overflow: hidden;
  white-space: normal;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 20px oklch(0% 0 0 / 0.28);
  font: 400 0.8rem/1.4 var(--body);
}
/* Reserve the height before the photo loads, or the card grows under the
   cursor on every hover and the map appears to twitch. */
.pintip .tipshot {
  display: block;
  width: 100%;
  height: 118px;
  object-fit: cover;
  background: var(--rule);
}
.pintip .tipaddr { display: block; padding: 0.45rem 0.55rem 0; }
.pintip .tipmeta {
  display: block;
  padding: 0.1rem 0.55rem 0.5rem;
  color: var(--muted);
}
/* Leaflet's callout arrow is hardcoded white like the popup chrome. */
.pintip.leaflet-tooltip-top::before { border-top-color: var(--border); }

/* Same photo in the click popup. Hover cards never appear on a touch screen,
   so without this the iPad has no way to see a picture from the map at all. */
.leaflet-popup-content .popshot {
  display: block;
  width: calc(100% + 2px);
  margin: -2px -1px 0.5rem;
  height: 130px;
  object-fit: cover;
  border-radius: 6px;
  background: var(--rule);
}
