
:root{--bg:#f7f8fa;--card:#fff;--ink:#1a1d24;--muted:#5b6472;--line:#e6e9ee;
--brand:#0b6b3a;--brand-ink:#0b6b3a;--star:#e8a400;--accent:#eef6f1}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
a{color:var(--brand-ink);text-decoration:none}a:hover{text-decoration:underline}
.wrap{max-width:820px;margin:0 auto;padding:0 18px}
.site-header{background:var(--card);border-bottom:1px solid var(--line)}
.site-header .wrap{display:flex;align-items:center;justify-content:space-between;padding:14px 18px}
/* Brand lockup: dog mark + wordmark. Shared by the header and the footer, so
   the two stay in step. The mark reads as a brand mark, not a favicon. */
/* Brand lockup: dog mark + wordmark. Shared by the header and the footer, so
   the two stay in step. Sized to read as a brand mark rather than a favicon. */
.brand{display:inline-flex;align-items:center;gap:14px;font-weight:800;font-size:26px;
color:var(--brand);min-width:0}
.brand:hover{text-decoration:none}.brand:hover .brand-name{text-decoration:underline}
.brand-mark{display:block;flex:0 0 auto;width:66px;height:66px;border-radius:16px}
.brand-name{white-space:nowrap}
.nav{display:flex;align-items:center}
.nav a{margin-left:16px;color:var(--muted);font-size:14px;white-space:nowrap}
/* "Advertise" is the header's one call to action: a modest filled pill in the
   brand green, matching a primary button in the advertiser portal so the two
   surfaces read as one product. Everything else stays a quiet text link. */
.nav a.cta{color:#fff;background:var(--brand);border-radius:8px;padding:7px 13px;
font-weight:600}
.nav a.cta:hover{text-decoration:none;opacity:.92}
/* Sign out is a button because it changes something. It has to sit in a row of
   text links without announcing that it is a different kind of element. */
.nav-signout{margin-left:16px;color:var(--muted);font-size:14px;white-space:nowrap;
background:none;border:0;padding:0;font-family:inherit;cursor:pointer}
.nav-signout:hover{text-decoration:underline}
.nav-signout[disabled]{opacity:.6;cursor:default}
.nav #ts-account{display:contents}
/* Step the lockup down so it never crowds the nav on phones. */
@media(max-width:640px){
.site-header .wrap{padding:10px 18px}
.brand{font-size:21px;gap:11px}.brand-mark{width:52px;height:52px;border-radius:13px}
.nav a{margin-left:12px;font-size:13px}
}
/* Below ~560px four nav items no longer fit beside the lockup, so the nav drops
   to its own full-width row and spreads across it. A second row, not a menu:
   every destination stays one tap away and nothing is hidden behind an icon. */
@media(max-width:560px){
.site-header .wrap{flex-wrap:wrap;row-gap:10px}
.nav{width:100%;justify-content:space-between}
.nav a{margin-left:0}
}
@media(max-width:400px){
.brand{font-size:18px;gap:9px}.brand-mark{width:44px;height:44px;border-radius:11px}
.nav a{font-size:12.5px}.nav a.cta{padding:6px 10px}
}
.crumbs{color:var(--muted);font-size:13px;margin:18px 0}
.crumbs a{color:var(--muted)}
main{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:24px 22px;margin-bottom:20px}
h1{font-size:26px;line-height:1.25;margin:.1em 0 .2em}
h2{font-size:18px;margin:1.4em 0 .5em;padding-bottom:.3em;border-bottom:1px solid var(--line)}
.subtitle{color:var(--muted);margin:.2em 0 1.2em}
.disclosure{background:#fff7e6;border:1px solid #f0d9a0;color:#6b4e00;
padding:10px 14px;border-radius:10px;margin:0 0 16px;font-size:14px}
/* "Coming soon" note. Deliberately calm and on-brand rather than a warning:
   nothing is wrong, the city simply has no rankings yet. */
.soon{background:var(--accent);border:1px solid #cfe3d6;color:var(--brand);
padding:12px 16px;border-radius:10px;margin:0 0 16px;font-size:14px}
.planned-head{margin-top:1.6em}
ol.leaderboard{list-style:none;margin:0;padding:0}
.rank-item{display:flex;align-items:center;gap:14px;padding:12px 6px;border-bottom:1px solid var(--line)}
.rank-item:last-child{border-bottom:none}
.rank{flex:0 0 34px;height:34px;display:flex;align-items:center;justify-content:center;
background:var(--accent);color:var(--brand);border-radius:8px;font-weight:700;font-size:14px}
.biz{min-width:0}
.biz-name{font-weight:600;font-size:17px}
.biz-meta{color:var(--muted);font-size:14px;margin-top:2px}
.biz-meta .stars{color:var(--star);font-weight:600}
.biz-meta .reviews{margin-left:10px}
.method-note{color:var(--muted);font-size:14px;margin-top:18px}
.cat-list,.city-list{list-style:none;margin:0;padding:0}
.cat-list li,.city-list li{padding:8px 0;border-bottom:1px solid var(--line)}
.cat-list.cols,.city-list{columns:2;column-gap:28px}
@media(max-width:560px){.cat-list.cols,.city-list{columns:1}}
.tag{font-size:11px;color:var(--muted);background:var(--bg);border:1px solid var(--line);
border-radius:6px;padding:1px 6px;margin-left:6px}
.group h2{margin-top:1.2em}
/* Sponsor slot. Desktop targets the proportions of a 728x90 leaderboard
   (~8:1) at the site's own content width rather than a literal 728px box, so
   it stays aligned with the rankings card. Below 640px it becomes a stacked
   card: the creative is a single responsive block, never a shrunken banner. */
.ad{display:flex;align-items:center;gap:16px;min-height:96px;
border:1px dashed var(--line);background:var(--card);border-radius:14px;
padding:14px 20px;margin:0 0 18px}
.ad-live{border-style:solid;border-color:#cfe3d6;background:var(--accent)}
.ad-label{flex:0 0 auto;font-size:10px;letter-spacing:.08em;
text-transform:uppercase;color:var(--muted);
border:1px solid var(--line);border-radius:5px;padding:2px 6px}
.ad-body{display:flex;flex-direction:column;gap:3px;flex:1 1 auto;min-width:0;
overflow-wrap:break-word}
.ad-name{font-weight:700;font-size:18px;line-height:1.3}
/* The call to action inside the house ad. A span, because the whole block is
   already the link — see render_ad. It is a flex item, so the link's own
   text-decoration does not draw through it, and align-self keeps it the width
   of its words instead of the width of the card. */
.ad-cta{align-self:flex-start;margin-top:9px;color:#fff;background:var(--brand);
border-radius:8px;padding:8px 14px;font-weight:600;font-size:14.5px}
.ad-house .ad-body:hover .ad-cta{opacity:.92}
.ad-tagline{color:var(--muted);font-size:14px;line-height:1.4}
/* Sponsor image creative. Every selector here is scoped to .ad-has-image or
   .ad-image, and both only exist when a city has an approved image, so a
   text-only sponsor slot renders byte-identically to before.
   The slot is 6:1, not the classic 8:1 leaderboard. At 8:1 the banner is only
   38px tall on a 375px phone — under the 50px floor every mobile ad format
   uses. 6:1 gives 51px there and 110px on desktop. min-height:96px on .ad is a
   floor, never a cap, so an image ad simply computes taller and that rule never
   fires; it is left exactly as it was. */
.ad-image{display:block;width:100%;max-width:100%;height:auto;
aspect-ratio:6/1;object-fit:contain;border-radius:8px}
/* In the <=640px column layout .ad is align-items:flex-start, which shrink-wraps
   .ad-body to its content. Without this the image's intrinsic 1500px would size
   the flex item and overflow the card. */
.ad-has-image .ad-body{align-self:stretch;width:100%}
/* The disclosure must not be paid for out of the artwork.
   .ad-label was a flex SIBLING of .ad-body, so above 640px it took its own
   width plus the 16px gap — 80px — out of the banner. That is a bad deal on its
   own, and it also made the page render two different ways: a privacy tool that
   hides the chip (its class and its container are exactly what cosmetic filter
   lists look for) left the row with one child, and the banner silently grew
   from 662px to 742px. Same bytes, same CSS, two layouts, and the one that
   looked right was the one with the disclosure missing.
   So image ads stack at every width, which is what they already did below
   640px: the label sits above the banner, the artwork spans the full column,
   and whether anything hides the label changes nothing about its geometry. A
   display:none flex item takes its gap with it, so there is no stray space
   either. Scoped to .ad-has-image — the text-only house slot keeps its chip
   beside the copy, where it costs nothing. */
.ad-has-image{flex-direction:column;align-items:flex-start;gap:8px}
/* The image IS the advertisement. The name and tagline stay in the DOM as the
   link's accessible name and as the fallback if the image never loads, but they
   are not drawn on top of artwork that already says them. */
.ad-has-image .ad-name,.ad-has-image .ad-tagline{position:absolute;width:1px;
height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);
white-space:nowrap;border:0}
@media(max-width:640px){
.ad{flex-direction:column;align-items:flex-start;gap:10px;min-height:0;
padding:14px 16px;border-radius:12px}
.ad-name{font-size:16px}.ad-tagline{font-size:13.5px}
}
.site-footer{color:var(--muted);font-size:12.5px;padding:4px 2px 26px}
.attribution{margin:0 0 8px}.foot-links a{color:var(--muted)}
/* Branded sign-off under the legal footer. Deliberately quiet: the mark, the
   wordmark, nothing else. The Google attribution above it is untouched. */
.foot-brand{display:flex;margin-top:18px;padding-top:18px;font-size:22px;gap:12px;
border-top:1px solid var(--line)}
.foot-brand .brand-mark{width:56px;height:56px;border-radius:14px}
@media(max-width:640px){
.foot-brand{font-size:19px;gap:10px}
.foot-brand .brand-mark{width:48px;height:48px;border-radius:12px}
}
pre{background:#0f1720;color:#e6edf3;padding:14px;border-radius:10px;overflow:auto;font-size:13px}
hr{border:none;border-top:1px solid var(--line);margin:1.6em 0}
