
  :root {
    --bg:#fbfbfd; --surface:#fff; --text:#1d1d1f; --muted:#6e6e73;
    --hair:rgba(0,0,0,.08); --hair2:rgba(0,0,0,.045);
    --accent:#FF002E; --accent-soft:rgba(255,0,46,.1);
    --good:#1a8f3c; --good-soft:rgba(26,143,60,.12);
    --warn:#a86500; --warn-soft:rgba(168,101,0,.13);
    --shadow:0 1px 2px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.06);
    --shadow-lg:0 12px 48px rgba(0,0,0,.18);
    --nav:rgba(251,251,253,.72);
  }
  /* Dark palette, applied either by the OS preference or by NYC clock time.
     The data-theme attribute is set before paint, so it wins over the media
     query in both directions and there's no flash. */
  @media (prefers-color-scheme:dark) {
    :root:not([data-theme="light"]) {
      --bg:#000; --surface:#1c1c1e; --text:#f5f5f7; --muted:#98989d;
      --hair:rgba(255,255,255,.13); --hair2:rgba(255,255,255,.07);
      --accent-soft:rgba(255,0,46,.2);
      --good:#32d74b; --good-soft:rgba(50,215,75,.16);
      --warn:#ffb340; --warn-soft:rgba(255,179,64,.16);
      --shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.5);
      --shadow-lg:0 12px 48px rgba(0,0,0,.7);
      --nav:rgba(0,0,0,.68);
    }
  }
  :root[data-theme="dark"] {
    --bg:#000; --surface:#1c1c1e; --text:#f5f5f7; --muted:#98989d;
    --hair:rgba(255,255,255,.13); --hair2:rgba(255,255,255,.07);
    --accent-soft:rgba(255,0,46,.2);
    --good:#32d74b; --good-soft:rgba(50,215,75,.16);
    --warn:#ffb340; --warn-soft:rgba(255,179,64,.16);
    --shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.5);
    --shadow-lg:0 12px 48px rgba(0,0,0,.7);
    --nav:rgba(0,0,0,.68);
  }
  *{box-sizing:border-box;}
  html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;}
  /* Load-in. Everything starts settled if JS never runs, so a failed script
     can't leave the page invisible. */
  .boot .brand-wrap{opacity:0;transform:scale(.94) translateY(8px);}
  .boot h1,.boot .hero-cap{opacity:0;transform:translateY(10px);}
  .boot .card{opacity:0;transform:translateY(14px);}
  .ready .brand-wrap{animation:riseIn .55s cubic-bezier(.2,.8,.25,1) both;}
  .ready h1{animation:riseIn .5s cubic-bezier(.2,.8,.25,1) .08s both;}
  .ready .hero-cap{animation:riseIn .5s cubic-bezier(.2,.8,.25,1) .16s both;}
  .ready .card{animation:riseIn .45s cubic-bezier(.2,.8,.25,1) both;
    animation-delay:calc(.22s + var(--i,0) * .035s);}
  /* Retired once it has played. Re-sorting moves cards in the DOM, which would
     otherwise restart this animation and rise the whole grid in again. */
  .anim-done .card{animation:none;}
  @keyframes riseIn{from{opacity:0;transform:translateY(12px) scale(.99);}
    to{opacity:1;transform:none;}}
  @media (prefers-reduced-motion:reduce){
    .boot .brand-wrap,.boot h1,.boot .hero-cap,.boot .card{opacity:1;transform:none;}
    .ready .brand-wrap,.ready h1,.ready .hero-cap,.ready .card{animation:none;}
    .card:hover .ph{animation:none;transform:scale(1.045);}
    .quip{transition:opacity .2s ease;transform:none;}
    .card:hover .quip{transform:none;}
  }
  body{margin:0;background:var(--bg);color:var(--text);line-height:1.47;
    font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",Roboto,sans-serif;
    -webkit-font-smoothing:antialiased;}
  .wrap{max-width:1120px;margin:0 auto;padding:0 24px;}

  /* ---------- nav ---------- */
  nav{position:sticky;top:0;z-index:50;background:var(--nav);
    backdrop-filter:saturate(180%) blur(20px);-webkit-backdrop-filter:saturate(180%) blur(20px);}
  .nav-in{position:relative;max-width:1120px;margin:0 auto;padding:0 24px;
    height:52px;display:flex;align-items:center;justify-content:space-between;
    gap:12px;}
  .nav-date{font-size:13px;font-weight:600;color:var(--text);white-space:nowrap;
    overflow:hidden;text-overflow:ellipsis;}
  .nav-date span{color:var(--muted);font-weight:500;}
  /* Every click into a dog is someone actually looking. Real number, live. */
  /* One centre slot holding both, so the swap reads as a transformation. */
  .nav-mid{position:absolute;left:50%;transform:translateX(-50%);
    height:52px;display:grid;place-items:center;}
  .nav-mid > *{grid-area:1/1;}
  .nav-logo{opacity:0;transform:scale(.55) translateY(-6px);pointer-events:none;
    transition:opacity .28s ease,transform .38s cubic-bezier(.34,1.5,.6,1);
    display:block;}
  .nav-logo img{width:112px;height:auto;display:block;}
  nav.shrunk .nav-logo{opacity:1;transform:none;pointer-events:auto;}
  nav.shrunk .nav-count{opacity:0;transform:scale(.85);pointer-events:none;}
  .nav-count{display:flex;align-items:center;gap:7px;font-size:12.5px;
    transition:opacity .24s ease,transform .28s ease;
    color:var(--muted);white-space:nowrap;background:var(--hair2);
    border-radius:980px;padding:6px 13px;}
  .nav-count[hidden]{visibility:hidden;}
  .nav-count b{color:var(--text);font-weight:700;
    font-variant-numeric:tabular-nums;}
  /* Room for seven figures without the bar reflowing. */
  .nav-count{min-width:0;}
  .nc-dot{width:6px;height:6px;border-radius:50%;background:var(--good);
    box-shadow:0 0 0 0 var(--good);animation:pulse 2.4s ease-out infinite;}
  @keyframes pulse{
    0%{box-shadow:0 0 0 0 rgba(50,215,75,.5);}
    70%{box-shadow:0 0 0 7px rgba(50,215,75,0);}
    100%{box-shadow:0 0 0 0 rgba(50,215,75,0);}
  }
  .nav-count b.bump{animation:bump .45s cubic-bezier(.34,1.6,.64,1);}
  @keyframes bump{
    0%{transform:scale(1);} 45%{transform:scale(1.35);color:var(--good);}
    100%{transform:scale(1);}
  }
  @media (max-width:860px){ .nc-l{display:none;} }
  @media (max-width:680px){
    /* No room for the date or a centred mark. The saved-hearts chip is the
       thing people need to reach, so it takes the left edge. */
    .nav-date{display:none;}
    .nav-mid{display:none;}

  }
  @media (prefers-reduced-motion:reduce){
    .nc-dot,.nav-count b.bump{animation:none;}
  }
  .logo{font-size:14px;font-weight:800;letter-spacing:.2em;color:var(--accent);
    text-transform:uppercase;text-decoration:none;}
  .nav-left{display:flex;align-items:center;gap:12px;min-width:0;}
  .nav-r{display:flex;align-items:center;gap:8px;}
  .nav-btn{all:unset;box-sizing:border-box;cursor:pointer;font-size:13.5px;
    font-weight:500;color:var(--text);
    padding:7px 13px;border-radius:980px;transition:background .2s,opacity .2s;}
  .nav-btn:hover{background:var(--hair2);}
  .nav-btn[hidden]{display:none;}
  .saved-chip{color:var(--accent);font-weight:700;display:inline-flex;
    align-items:center;gap:6px;padding:7px 13px;border-radius:980px;
    background:var(--accent-soft);}
  .saved-chip:hover{background:var(--accent-soft);opacity:.85;}
  .sc-hrt{width:15px;height:15px;stroke:var(--accent);stroke-width:2;fill:none;
    transition:fill .2s ease;}
  .saved-chip.has .sc-hrt{fill:var(--accent);}
  /* Nothing saved yet — present, but clearly not urgent. */
  .saved-chip.none{color:var(--muted);background:var(--hair2);}
  .saved-chip.none .sc-hrt{stroke:var(--muted);}
  /* Filtered state wins over both — must be unmistakable. */
  .saved-chip.active,.saved-chip.none.active{background:var(--accent);
    color:#fff;}
  .saved-chip.active .sc-hrt,.saved-chip.none.active .sc-hrt{
    stroke:#fff;fill:#fff;}
  .saved-chip.active:hover{background:var(--accent);opacity:.9;}
  .nav-cta{background:var(--accent);color:#fff;font-weight:600;}
  .nav-cta:hover{background:var(--accent);opacity:.88;}

  /* ---------- header ---------- */
  header{text-align:center;padding:34px 0 8px;position:relative;z-index:40;}
  h1{font-size:clamp(34px,5.5vw,56px);line-height:1.14;letter-spacing:-.025em;
    font-weight:700;margin:0;position:relative;z-index:5;}
  /* Two words in the headline become pickers. Everything stays real text, so
     the h1 still reads as one sentence to search engines and screen readers. */
  .pick{position:relative;display:inline-block;z-index:45;}
  .pick > button{all:unset;cursor:pointer;font:inherit;color:var(--text);
    border-bottom:3px solid var(--hair);padding:0 2px;
    transition:border-color .2s;}
  .pick > button:hover{border-bottom-color:var(--accent);}
  .pick > button:focus-visible{outline:3px solid var(--accent);
    outline-offset:3px;border-radius:4px;}
  /* Drawn, in the same idiom as the heart and the share icon: no fill, a
     currentColor stroke, round joins. See CHEVRON in page.py for why it stopped
     being a text character.
     Sized in em against whatever control holds it, so one declaration keeps the
     filter pills' chevrons in proportion across both breakpoints: 10.8px at the
     desktop 15px, 9.72px at the phone's 13.5px. Three hardcoded pixel sizes is
     what was here before, and they had already drifted: 9px on the pills
     against 13px on the sort. The two headline pickers and the sort each carry
     their own ratio against this one — see .pick .cv and .fpill.fsort .cv. */
  .cv{width:.72em;height:.48em;flex:none;fill:none;stroke:currentColor;
    stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;
    color:var(--muted);display:inline-block;transition:transform .2s;}
  /* The headline is up to 38px, where .72em would be a 27px chevron shouting at
     the word it belongs to. It takes its own ratio, which lands it at roughly
     the sort control's size — a chevron is a chevron whatever it hangs off.
     These two also sit inline in a sentence rather than in a flex button, so
     they need lifting off the baseline by hand. */
  .pick .cv{width:.24em;height:.16em;margin-left:.14em;vertical-align:.14em;}
  .pick.open .cv{transform:rotate(180deg);}
  .pick-menu{position:absolute;top:calc(100% + 12px);left:50%;
    transform:translateX(-50%) translateY(-6px);z-index:48;
    background:var(--surface);border:1px solid var(--hair);border-radius:16px;
    box-shadow:0 1px 0 rgba(255,255,255,.06) inset,
               0 4px 12px rgba(0,0,0,.3),0 22px 60px rgba(0,0,0,.55);
    padding:6px;min-width:210px;
    display:flex;flex-direction:column;gap:2px;opacity:0;
    transition:opacity .18s ease,transform .22s cubic-bezier(.2,.8,.25,1);}
  .pick-menu[hidden]{display:none;}
  .pick.open .pick-menu{opacity:1;transform:translateX(-50%);}
  /* A live city is an anchor, not a button: choosing one is an ordinary
     navigation to that city's own page, so it should behave like a link —
     middle-click, cmd-click and "copy link" all work, and it needs no
     JavaScript. The cities we don't cover yet stay buttons, because there is
     nowhere to send anyone and the click opens the waitlist instead. */
  .pick-menu button,.pick-menu a{all:unset;cursor:pointer;font-size:16px;
    font-weight:500;
    padding:11px 14px;border-radius:10px;text-align:left;color:var(--text);
    display:flex;justify-content:space-between;align-items:center;gap:14px;}
  .pick-menu button:hover,.pick-menu a:hover{background:var(--hair2);}
  .pick-menu button:disabled{cursor:default;opacity:.45;}
  .pick-menu button:disabled:hover{background:transparent;}
  .pick-menu [aria-current="page"]{background:var(--hair2);}
  .pick-menu button[data-ok]::after,
  .pick-menu a[data-ok]::after{content:'Live';font-size:11px;
    font-weight:700;color:var(--good);letter-spacing:.04em;}
  .pick-menu button:not([data-ok])::after{content:'Soon';font-size:11px;
    font-weight:700;color:var(--muted);letter-spacing:.04em;}

  /* A "coming soon" that only apologises is a dead end — capture the interest. */
  .soon{max-width:460px;margin:20px auto 0;}
  .soon[hidden]{display:none;}
  .soon p{font-size:15.5px;color:var(--text);margin:0 0 14px;line-height:1.5;}
  .soon p b{color:var(--accent);}
  /* Deliberately restrained: one row, no card, no heading of its own. The
     dogs stay the first real thing you see. */
  .hero-sub{display:flex;gap:8px;max-width:432px;margin:26px auto 0;}
  .hero-sub input{flex:1;min-width:0;padding:12px 15px;font-size:15px;
    border-radius:11px;border:1px solid var(--hair);background:var(--surface);
    color:var(--text);font-family:inherit;}
  .hero-sub input:focus{outline:2px solid var(--accent);outline-offset:-1px;
    border-color:transparent;}
  .hero-sub button{all:unset;box-sizing:border-box;cursor:pointer;
    background:var(--accent);color:#fff;font-weight:600;font-size:15px;
    padding:12px 18px;border-radius:11px;white-space:nowrap;
    transition:opacity .2s;text-align:center;}
  .hero-sub button:hover{opacity:.88;}
  .hero-note{font-size:12.5px;color:var(--muted);margin-top:9px;}
  /* The hero note rests empty; don't leave its margin behind holding space. */
  .hero-note:empty{margin-top:0;}
  .hero-note.ok{color:var(--accent);font-weight:700;font-size:14px;}
  .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
    overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}

  /* ---------- brand logo ---------- */
  .brand-wrap{display:flex;justify-content:center;margin-bottom:10px;
    transition:opacity .3s ease,transform .3s ease;}
  body.shrunk .brand-wrap{opacity:0;transform:scale(.9) translateY(-10px);}
  .brand-logo{width:clamp(190px,27vw,330px);height:auto;cursor:pointer;display:block;
    transition:transform .45s cubic-bezier(.34,1.56,.64,1),filter .35s ease;
    will-change:filter,transform;transform:translateZ(0);
    /* Two soft drop-shadows: a tight one for the edge, a wide diffuse one so
       the mark lifts off the background rather than sitting flat on it. */
    filter:drop-shadow(0 2px 3px rgba(0,0,0,.14))
           drop-shadow(0 12px 26px rgba(0,0,0,.16));}
  .brand-logo:hover{animation:beat .72s cubic-bezier(.4,0,.6,1) infinite;
    filter:drop-shadow(0 3px 5px rgba(0,0,0,.18))
           drop-shadow(0 18px 38px rgba(0,0,0,.24));}
  /* On black a dark shadow is invisible, so lift it with a faint glow instead. */
  :root[data-theme="dark"] .brand-logo{
    filter:drop-shadow(0 2px 4px rgba(0,0,0,.6))
           drop-shadow(0 14px 32px rgba(255,255,255,.09));}
  :root[data-theme="dark"] .brand-logo:hover{
    filter:drop-shadow(0 3px 6px rgba(0,0,0,.7))
           drop-shadow(0 20px 44px rgba(255,255,255,.14));}
  @keyframes beat{
    0%,100%{transform:scale(1) rotate(0deg);}
    16%{transform:scale(1.09) rotate(-1.6deg);}
    32%{transform:scale(1.01) rotate(0deg);}
    50%{transform:scale(1.06) rotate(1.2deg);}
  }
  .brand-logo.cheer,.nav-logo img.cheer{
    animation:cheer .9s cubic-bezier(.34,1.5,.5,1) 1;}
  @keyframes cheer{
    0%{transform:scale(1) rotate(0);}
    22%{transform:scale(1.22) rotate(-4deg);}
    44%{transform:scale(1.05) rotate(3deg);}
    66%{transform:scale(1.14) rotate(-2deg);}
    100%{transform:scale(1) rotate(0);}
  }
  @media (prefers-reduced-motion:reduce){
    .brand-logo:hover{animation:none;transform:scale(1.04);}
    .brand-logo.cheer,.nav-logo img.cheer{animation:none;}
  }
  /* Easter egg: paws drift up while you hover, and clicking sets off a burst. */
  .brand-wrap{position:relative;}
  .egg{position:absolute;pointer-events:none;font-size:20px;left:50%;top:50%;
    will-change:transform,opacity;z-index:2;}
  @keyframes drift{
    0%{transform:translate(-50%,-50%) scale(.4);opacity:0;}
    22%{opacity:.95;}
    100%{transform:translate(calc(-50% + var(--dx)),calc(-50% + var(--dy)))
          scale(var(--sc)) rotate(var(--rot));opacity:0;}
  }
  .woof{position:absolute;left:50%;top:-6px;transform:translateX(-50%);
    background:var(--accent);color:#fff;font-size:12.5px;font-weight:700;
    padding:5px 12px;border-radius:980px;white-space:nowrap;pointer-events:none;
    animation:woofpop 1.5s cubic-bezier(.34,1.56,.64,1) forwards;z-index:3;}
  @keyframes woofpop{
    0%{transform:translateX(-50%) scale(.3);opacity:0;}
    18%{transform:translateX(-50%) scale(1.12);opacity:1;}
    30%{transform:translateX(-50%) scale(1);}
    78%{opacity:1;}
    100%{transform:translateX(-50%) translateY(-14px) scale(.95);opacity:0;}
  }
  @media (prefers-reduced-motion:reduce){ .egg{display:none;} }

  /* Double-tap confirmation: a heart that punches in and fades. */
  .dbl-heart{position:absolute;inset:0;display:grid;place-items:center;
    pointer-events:none;z-index:6;}
  .dbl-heart svg{width:34%;height:34%;fill:#fff;stroke:none;
    filter:drop-shadow(0 4px 16px rgba(0,0,0,.45));
    animation:dblpop .9s cubic-bezier(.2,.9,.3,1) forwards;}
  @keyframes dblpop{
    0%{transform:scale(.3);opacity:0;}
    18%{transform:scale(1.15);opacity:.96;}
    36%{transform:scale(1);opacity:.96;}
    100%{transform:scale(1.08);opacity:0;}
  }
  @media (prefers-reduced-motion:reduce){ .dbl-heart{display:none;} }

  /* ---------- grid ---------- */
  .grid{display:grid;gap:28px;padding:22px 0 8px;
    grid-template-columns:repeat(auto-fill,minmax(248px,1fr));}
  .card{all:unset;cursor:pointer;display:block;border-radius:20px;background:var(--surface);
    box-shadow:var(--shadow);overflow:hidden;
    transition:transform .3s cubic-bezier(.2,.8,.2,1),box-shadow .3s;}
  .card:hover{transform:translateY(-6px);box-shadow:var(--shadow-lg);}
  .card:focus-visible{outline:3px solid var(--accent);outline-offset:3px;}
  .ph-wrap{aspect-ratio:1/1;overflow:hidden;background:var(--hair);position:relative;}
  .ph{width:100%;height:100%;object-fit:cover;display:block;
    transition:transform .5s cubic-bezier(.2,.8,.2,1);}
  /* A quick playful wiggle on hover, settling into a gentle zoom. The scale
     stays >1 throughout so the rotation never exposes the tile behind it. */
  .card:hover .ph{animation:cardWiggle .55s cubic-bezier(.36,.07,.19,.97) both;}
  @keyframes cardWiggle{
    0%{transform:scale(1) rotate(0);}
    22%{transform:scale(1.05) rotate(-2.4deg);}
    44%{transform:scale(1.05) rotate(1.9deg);}
    64%{transform:scale(1.05) rotate(-1.2deg);}
    82%{transform:scale(1.048) rotate(.6deg);}
    100%{transform:scale(1.045) rotate(0);}
  }
  /* The dog's own one-liner, in a little speech bubble that pops on hover.
     Hover-capable pointers only — on touch a stuck :hover would trap it open. */
  .quip{display:none;}
  @media (hover:hover) and (pointer:fine){
    .quip{display:block;position:absolute;left:12px;right:12px;bottom:12px;
      background:#fff;color:#1d1d1f;font-size:12.5px;font-weight:600;
      line-height:1.32;padding:9px 12px;border-radius:15px 15px 15px 4px;
      box-shadow:0 8px 22px rgba(0,0,0,.22);pointer-events:none;z-index:3;
      opacity:0;transform:translateY(8px) scale(.9);transform-origin:0 100%;
      transition:opacity .22s ease,transform .28s cubic-bezier(.34,1.56,.64,1);}
    .quip span{display:-webkit-box;-webkit-line-clamp:2;
      -webkit-box-orient:vertical;overflow:hidden;}
    /* The tail — a little nib on the lower-left, so it reads as speech. */
    .quip::after{content:"";position:absolute;left:14px;bottom:-6px;width:14px;
      height:14px;background:#fff;border-radius:0 0 0 3px;
      transform:rotate(45deg);box-shadow:-3px 3px 6px rgba(0,0,0,.06);}
    .card:hover .quip{opacity:1;transform:translateY(0) scale(1);}
    /* The bubble lands on top of the badge, and "NEW HERE" is wide enough that
       its red ends stick out past both sides of the white — which reads as a
       rendering fault rather than two things overlapping. So the badge leaves
       while the bubble is up.
       Hidden rather than nudged out of the way: quips are per-dog strings of
       different lengths and some wrap to two lines, so any geometry that clears
       today's longest quip stops clearing it the next time the bubble grows or
       the card narrows. Nothing is lost — the bubble is what you're reading
       while you hover, and the badge is back the moment you leave.
       Inside this media query on purpose: on a phone there is no bubble, so
       there is nothing to hide behind and the badge must stay put. It fades on
       the bubble's own opacity curve so the two read as one move; the badge
       never transforms, so the reduced-motion block below has nothing to
       undo here. */
    .new-chip{transition:opacity .22s ease;}
    .card:hover .new-chip{opacity:0;}
  }
  :root[data-theme="dark"] .quip{background:#2b2b2e;color:#f2f2f2;}
  :root[data-theme="dark"] .quip::after{background:#2b2b2e;}
  .noph{display:flex;flex-direction:column;align-items:center;
    justify-content:center;gap:10px;height:100%;
    background:linear-gradient(150deg,var(--accent-soft),transparent 72%);}
  .noph-i{font-size:58px;font-weight:800;letter-spacing:-.04em;
    color:var(--accent);opacity:.55;line-height:1;}
  .noph-t{font-size:11.5px;font-weight:600;letter-spacing:.03em;
    color:var(--muted);}
  /* Real click counts, fetched live. Hidden until a dog has genuine interest —
     never a fabricated number. */
  .views{position:absolute;left:11px;top:11px;display:flex;align-items:center;gap:5px;
    font-size:13px;font-weight:700;padding:6px 11px;border-radius:980px;color:#fff;
    background:rgba(0,0,0,.55);backdrop-filter:blur(10px);
    -webkit-backdrop-filter:blur(10px);animation:pop .35s cubic-bezier(.34,1.56,.64,1);}
  /* display:flex above would otherwise beat the [hidden] attribute. */
  .views[hidden]{display:none;}
  /* Eyes, not a flame. The number is how many people have opened this dog's
     card — a plain count of who looked, which is useful to an adopter deciding
     whether to hurry. A flame reads as "trending", which turns the same number
     into a popularity contest and quietly tells you the quiet dogs matter less.
     Every dog here is waiting; the badge should say "looked at", not "hot". */
  .views .eyes{font-size:13px;line-height:1;}
  @keyframes pop{from{transform:scale(.6);opacity:0;}to{transform:scale(1);opacity:1;}}
  .card{text-decoration:none;color:inherit;}
  .meta{padding:15px 16px 17px;}
  /* Rescue moves off the photo and sits beside the name, where it reads as
     attribution rather than a sticker. */


  /* Long-stay marker. Exception-based: most dogs never show it. */
  .waiting{position:absolute;right:11px;bottom:11px;font-size:10.5px;
    font-weight:700;padding:5px 10px;border-radius:980px;color:#fff;
    background:rgba(168,101,0,.85);backdrop-filter:blur(10px);
    -webkit-backdrop-filter:blur(10px);}

  /* Save — sits on the photo, only fully opaque once saved. */
  .save{position:absolute;right:11px;top:11px;width:34px;height:34px;
    border:none;border-radius:50%;cursor:pointer;padding:0;
    background:rgba(0,0,0,.5);backdrop-filter:blur(10px);
    -webkit-backdrop-filter:blur(10px);display:grid;place-items:center;
    transition:transform .2s cubic-bezier(.34,1.6,.64,1),background .2s;}
  .save:hover{transform:scale(1.1);background:rgba(0,0,0,.68);}
  .save:active{transform:scale(.9);}
  .hrt{width:18px;height:18px;fill:none;stroke:rgba(255,255,255,.85);
    stroke-width:1.9;stroke-linejoin:round;
    transition:fill .18s ease,stroke .18s ease,transform .3s
      cubic-bezier(.34,1.8,.5,1);}
  .save:hover .hrt{stroke:#fff;}
  .save.on .hrt{fill:var(--accent);stroke:var(--accent);}
  /* Overshoot on save, nothing on unsave — the reward belongs to one direction. */
  .save.pop .hrt{animation:hrtpop .48s cubic-bezier(.34,1.7,.5,1);}
  @keyframes hrtpop{
    0%{transform:scale(1);} 30%{transform:scale(1.45);}
    55%{transform:scale(.92);} 100%{transform:scale(1);}
  }
  /* A ring that expands and fades out from under the heart. */
  .burst{position:absolute;inset:0;border-radius:50%;pointer-events:none;
    border:2px solid var(--accent);opacity:0;}
  .save.pop .burst{animation:ring .55s cubic-bezier(.2,.7,.3,1);}
  @keyframes ring{
    0%{transform:scale(.7);opacity:.9;}
    100%{transform:scale(2.1);opacity:0;}
  }
  .fh{position:fixed;pointer-events:none;z-index:200;color:var(--accent);
    transform:translate(-50%,-50%);will-change:transform,opacity;opacity:0;
    line-height:1;}
  @keyframes floatheart{
    0%{transform:translate(-50%,-50%) scale(.4);opacity:0;}
    25%{opacity:1;}
    100%{transform:translate(calc(-50% + var(--dx)),calc(-50% + var(--dy)))
      scale(1.1) rotate(var(--rot));opacity:0;}
  }
  @media (prefers-reduced-motion:reduce){
    .save.pop .hrt,.save.pop .burst{animation:none;}
    .fh{display:none;}
  }
  .nm-row{display:flex;align-items:center;gap:7px;margin:0 0 10px;}
  .nm{font-size:23px;font-weight:700;letter-spacing:-.02em;margin:0;min-width:0;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
  /* Arrived today. On a busy morning most cards carry this, so it's sized like
     the wait badge rather than the old day heading's chip — a marker you can
     scan past, not a sticker on every dog. */
  /* On the photo, bottom-right, the corner the wait badge used to hold. Solid
     accent rather than the old tinted pill: it has to hold up over a photo
     instead of a white card. The hover quip covers it on desktop, which is
     fine — that hover is a deliberate takeover of the tile. */
  .new-chip{position:absolute;right:11px;bottom:11px;z-index:2;
    font-size:9.5px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;
    color:#fff;background:var(--accent);padding:4px 9px;border-radius:980px;
    box-shadow:0 2px 10px rgba(0,0,0,.22);}
  /* nowrap keeps every card the same height so the grid stays aligned */
  .pills{display:flex;flex-wrap:nowrap;gap:5px;overflow:hidden;}
  .pill{font-size:11.5px;font-weight:500;padding:4px 9px;border-radius:980px;
    background:var(--hair2);color:var(--muted);white-space:nowrap;}
  /* Breed leads and is tinted — the one fact most adopters scan for. */
  .pill.breed{background:var(--accent-soft);color:var(--accent);font-weight:600;}
  /* A different route to the same dog, so it borrows the warning tint rather
     than the brand one: read this before you get attached, it isn't a feature. */
  .pill.program{background:var(--warn-soft);color:var(--warn);font-weight:600;}


  .empty{text-align:center;padding:90px 20px;}
  .empty-emoji{font-size:52px;margin-bottom:12px;}
  .empty h2{font-size:24px;margin:0 0 8px;letter-spacing:-.02em;}
  .empty p{color:var(--muted);margin:0;}

  /* ---------- subscribe ---------- */
  .sub-sec{background:var(--surface);border-radius:28px;padding:56px 32px;
    margin:56px 0 0;text-align:center;box-shadow:var(--shadow);}
  .sub-sec h2{font-size:clamp(26px,3.6vw,36px);letter-spacing:-.022em;margin:0 0 10px;}
  /* Wide enough for the promise to land in two lines, balanced so it never
     drops a one-word orphan onto a third. */
  .sub-sec p{color:var(--muted);font-size:16.5px;margin:0 auto 26px;max-width:470px;
    text-wrap:balance;}
  .sub-form{display:flex;gap:9px;max-width:430px;margin:0 auto;flex-wrap:wrap;}
  .sub-form input{flex:1;min-width:200px;padding:14px 17px;font-size:16px;border-radius:13px;
    border:1px solid var(--hair);background:var(--bg);color:var(--text);font-family:inherit;}
  .sub-form input:focus{outline:2px solid var(--accent);outline-offset:-1px;
    border-color:transparent;}
  .sub-form button{all:unset;box-sizing:border-box;cursor:pointer;
    background:var(--accent);color:#fff;font-weight:600;font-size:16px;
    padding:14px 26px;border-radius:13px;transition:opacity .2s;
    text-align:center;}
  .sub-form button:hover{opacity:.88;}
  .sub-note{font-size:13px;color:var(--muted);margin-top:14px;}
  .sub-note a{color:var(--accent);}
  .sub-ok{color:var(--accent);font-weight:600;margin-top:14px;font-size:15px;}

  /* ---------- saved filter bar ---------- */
  .filter-bar{display:flex;align-items:center;justify-content:space-between;
    gap:14px;background:var(--accent-soft);border:1px solid var(--accent);
    border-radius:14px;padding:12px 16px;margin:26px 0 0;}
  .filter-bar[hidden]{display:none;}
  .fb-label{display:flex;align-items:center;gap:9px;font-size:14.5px;
    font-weight:600;color:var(--accent);}
  .fb-hrt{width:16px;height:16px;fill:var(--accent);stroke:var(--accent);
    stroke-width:1.8;}
  .fb-clear{all:unset;box-sizing:border-box;cursor:pointer;font-size:13.5px;
    font-weight:600;color:var(--accent);padding:6px 12px;border-radius:980px;
    background:var(--surface);white-space:nowrap;}
  .fb-clear:hover{opacity:.8;}
  /* The right-hand group: the URL, then the button. min-width:0 so the code
     element can actually shrink — a flex child will not ellipsis without it. */
  .fb-right{display:flex;align-items:center;gap:12px;min-width:0;
    margin-left:auto;}
  /* The link, shown rather than described. Monospace and muted: it is evidence,
     not a control, and it should not compete with the button beside it.
     Truncates from the right, host first. `direction:rtl` was tried to keep the
     tail visible and is wrong for a URL: it reorders the string at the bidi
     level rather than scrolling it, so "luvd.com/?saved=a,b" rendered as
     "…,b/luvd.com" — a URL nobody could verify, which defeats showing it. */
  .fb-url{font:500 12.5px/1.35 ui-monospace,SFMono-Regular,Menlo,monospace;
    color:var(--accent);opacity:.72;background:rgba(255,255,255,.55);
    border-radius:8px;padding:5px 9px;max-width:340px;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .fb-url[hidden]{display:none;}
  /* White, matching the bar's other pill, with a drawn icon rather than 🔗 —
     that emoji renders as a different object on every platform and read as a
     smudge at this size. It is the only button in the bar now, so it does not
     need to shout to be found. */
  .fb-copy{padding:7px 14px;display:inline-flex;align-items:center;gap:6px;
    flex:0 0 auto;box-shadow:0 1px 2px rgba(0,0,0,.07);}
  .fb-copy:hover{opacity:.85;}
  .fb-copy svg{width:14px;height:14px;flex:0 0 auto;fill:none;
    stroke:var(--accent);stroke-width:2;stroke-linecap:round;
    stroke-linejoin:round;}
  .fb-copy[hidden]{display:none;}
  @media (max-width:680px){
    /* No room for a URL beside a button on a phone, and the button is the part
       that does something. The link is still what gets shared — it is just not
       recited first. */
    .fb-url{display:none;}
    /* 44px, because this is now the only thing to press in this bar and it was
       coming out at 31. The label stays 13.5px; the height comes from padding,
       so the pill grows without the type shouting. */
    .fb-copy{min-height:44px;padding:0 16px;}
    .filter-bar{padding:10px 12px;}
  }

  /* ---------- filter pills ---------- */
  /* One quiet row above the dogs, centred under the centred hero. No card, no
     heading, no sidebar: at ~220 dogs these are for narrowing a scroll, not for
     running a search. */
  .fbar{margin:30px 0 -6px;}
  /* These set display (via all:unset or flex), which would otherwise beat the
     UA rule behind the [hidden] attribute. */
  .fpill-t[hidden],.fb-clear[hidden]{display:none;}
  /* The pills own this row alone. Sort used to share it behind a hairline
     divider; it now sits on the results header below, where being a row apart
     separates it more plainly than the divider ever did. */
  .fbar-pills{display:flex;align-items:center;justify-content:center;gap:8px;
    flex-wrap:wrap;}
  /* An open menu is only above the grid for as long as nothing between it and
     the page root is a stacking context — and this row has now twice become one
     by accident. First the scroll fade's mask (paintPillFade stands it down for
     exactly this reason), then -webkit-overflow-scrolling:touch on iOS, which
     put every filter menu behind the dogs in mobile Safari. A menu's z-index:48
     is measured inside whichever context it lands in, so each time the row grew
     one, 48 stopped meaning "above the cards" and started meaning "above the
     other things in this row".
     So the row states its own order instead of relying on not having one: while
     a menu is open it *is* a stacking context, deliberately, and one that sits
     above the grid. 49 is one below nav's 50 — a menu should cover the dogs, not
     the bar you close it from. Toggled by paintPillFade(), which already knows
     when a menu is open. Anything added here that draws outside the row's box
     now stays correct without a third special case. */
  /* The layer that holds whichever menu is open. z-index 49 keeps it over the
     grid and under the sticky nav at 50 — a menu should cover the dogs, not the
     bar you close it from. inset:0 makes it exactly the viewport, so a menu's
     own left/right/top resolve as they did when it was fixed to the viewport
     directly. pointer-events pass through everywhere except the menu itself, so
     an empty layer cannot swallow a tap on the page beneath it. */
  .fmenu-layer{position:fixed;inset:0;z-index:49;pointer-events:none;}
  .fmenu-layer > .fmenu{pointer-events:auto;}
  /* The results header: count left, sort right, sitting on top of the grid.
     The count is always on the page, never toggled. It used to appear only once
     a filter was applied, which pushed the whole grid down at the exact moment
     you clicked — so the count arrived by shoving the thing you were looking at.
     Permanent means filtering just changes the number in place. Left, flush with
     the first card, so it reads as a heading for the grid, which is what it is
     now that the day headings are gone. */
  /* No reserved height here any more. This row used to carry a Clear link that
     came and went with the filter, so it needed a floor to stop the grid
     dropping 23px the moment you clicked a pill. Clear is gone and both things
     left — the count and the sort — are permanent and fixed-width, so the row
     is a constant height by construction. Its height is the sort pill's 44px
     touch target; see .fpill.fsort > button. */
  /* The negative bottom margin is doing optical work, not layout work. The row
     is a fixed 44px and the count is centred in it, so the type change from
     23px to 19px left ~3px of extra air between the count's ink and the first
     card without changing a single box. These margins put the ink back where it
     was: 24px below the pills, 15px above the cards. Measure from the numeral,
     not from the row, if you retune this. */
  .fbar-meta{display:flex;align-items:center;justify-content:flex-start;
    gap:8px;margin:14px 0 -19px;}
  /* 19px on desktop, 17px on phones — the phone step-down and its reason are in
     the mobile block. Desktop 19px is a step down from the retired day heading's
     23px: the count has that heading's job now the grid is flat, but at 23px it
     and the sort at the other end of the row read as two unrelated things rather
     than one header. Matching the filter pills' 15px control scale was tried and
     rejected: this is the page's only statement of how many dogs there are, and
     at control size it stops reading as a heading and becomes metadata. Numeral
     at full contrast, unit greyed: the same pairing the date and its "N dogs"
     had. */
  .fbar-n{font-size:19px;font-weight:700;letter-spacing:-.022em;
    color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums;}
  .fbar-n b{font-weight:700;color:var(--text);}
  /* Hard right, opposite the count: the heading states what you're looking at,
     the control at the other end changes its order. */
  .fbar-meta .fpill.fsort{margin-left:auto;}
  .fpill{position:relative;}
  /* 15px is the control scale, shared with the sort at the other end of the
     header below. One size for everything you can click up here, so the two
     rows read as one set of controls rather than two unrelated toolbars. The
     sort stays the quieter of the two on weight, colour and chrome instead of
     on size — see .fpill.fsort. */
  .fpill > button,.fpill-t{all:unset;box-sizing:border-box;cursor:pointer;
    display:inline-flex;align-items:center;gap:5px;font-size:15px;
    font-weight:600;color:var(--text);background:var(--surface);
    border:1px solid var(--hair);border-radius:980px;padding:8px 14px;
    white-space:nowrap;transition:border-color .18s,background .18s,color .18s;}
  .fpill > button:hover,.fpill-t:hover{border-color:var(--muted);}
  /* Drawn inside the pill, not around it. An offset outline was being sheared
     off by .fbar-pills' old overflow-x scroll container on phones, and on a pill
     already filled solid accent a red ring one gap away read as a broken double
     border. Inset can't be clipped; white reads as focus on a filled pill. */
  .fpill > button:focus-visible,.fpill-t:focus-visible{
    outline:2px solid var(--accent);outline-offset:-2px;}
  .fpill.on > button:focus-visible,.fpill-t.on:focus-visible{outline-color:#fff;}
  /* Active reads as filled brand, so the row tells you at a glance how narrow
     the list you're looking at is. */
  .fpill.on > button,.fpill-t.on{background:var(--accent);border-color:var(--accent);
    color:#fff;}
  .fpill.on .cv{color:rgba(255,255,255,.75);}
  .fpill-t b{font-weight:700;opacity:.6;font-variant-numeric:tabular-nums;}
  .fpill-t.on b{opacity:.8;}
  /* No size of its own: the em ratio on .cv already scales it with each pill,
     and the flex button centres it without a vertical-align nudge. */
  .fpill.open .cv{transform:rotate(180deg);}
  /* Sort is not a filter, so it deliberately looks unlike one: no pill, and a
     "Sort by" label that names the control instead of mirroring its value the
     way the filter pills do. It never fills either — something is always sorting, so a
     filled control here would claim you'd narrowed the list, and the pill row's
     filled state is how the page reports exactly that. The hairline divider it
     used to sit behind is gone with the move to its own row: a row apart is
     already a stronger separation than a 1px rule. */
  /* 17px, one value at every width — the only thing in the filter bar that
     doesn't step across the breakpoint, because it is a control and not type in
     a ladder. It spent a while at the count's 19px, on the theory that the two
     ends of the row should agree on size, and that made the least-used control
     on the page the loudest thing in its own row and louder than the filter
     pills above it, which is backwards. 17px still sits above the pills'
     control scale, so it reads as belonging to the heading row rather than to
     them, without shouting; it keeps the count's tracking for the same reason.
     Lighter than the count on everything else: weight 600 against 700, --muted
     against --text, no fill and no border. It reorders the list, it never
     narrows it, so it must never look like something you've switched on.
     min-height, not padding: this is the row's only touch target and the thing
     that sets the results header's height, so the row is 44px because this
     button is. The line-height is holding that up. At the body's 1.47 a 19px
     label measured 27.9px, which with padding and border came to 45.9 and took
     the height off min-height and onto the type — the row grew 2px and the
     floor stopped meaning anything. 1.2 keeps the floor in charge, with more
     room to spare at 17px than there ever was at 19. Nothing is clipped: the
     button centres its label in the full 44. */
  .fpill.fsort > button{background:none;border-color:transparent;padding:8px;
    min-height:44px;font-size:17px;line-height:1.2;font-weight:600;
    letter-spacing:-.022em;color:var(--muted);}
  .fpill.fsort > button:hover{border-color:transparent;color:var(--text);}
  /* Its own ratio, not the shared .72em, and deliberately not in proportion to
     its label. The arrow is the least informative mark in the row — it says
     "this opens", which you already know from everything else about the
     control — so it is the first thing that should give up size, and it should
     not swing every time the sort's type is retuned. .66em of 17px is 11.2px,
     which is a step down from the 13.7px it drew at the old 19px and only just
     above the pills' 10.8px: a hair larger, because the sort's label is larger,
     without the two reading as different icons. Height is two thirds of width,
     the viewBox's own ratio. */
  .fpill.fsort .cv{width:.66em;height:.44em;}
  /* Right-anchored: it's the last control in the row, so a left-anchored menu
     would hang off the edge on narrow screens. */
  .fpill.fsort .fmenu{left:auto;right:0;}
  .fmenu{position:absolute;top:calc(100% + 8px);left:0;z-index:48;
    background:var(--surface);border:1px solid var(--hair);border-radius:14px;
    box-shadow:0 4px 12px rgba(0,0,0,.14),0 18px 48px rgba(0,0,0,.18);
    padding:6px;min-width:200px;max-height:min(60vh,420px);overflow-y:auto;
    display:flex;flex-direction:column;gap:1px;opacity:0;
    transform:translateY(-6px);
    transition:opacity .16s ease,transform .2s cubic-bezier(.2,.8,.25,1);}
  :root[data-theme="dark"] .fmenu{
    box-shadow:0 4px 12px rgba(0,0,0,.5),0 22px 60px rgba(0,0,0,.6);}
  .fmenu[hidden]{display:none;}
  /* On the menu, not on `.fpill.open .fmenu`: an open menu is moved out of its
     pill into #fmenu-layer, so a descendant selector stops matching exactly when
     it is needed. */
  .fmenu.open{opacity:1;transform:none;}
  .fmenu button{all:unset;cursor:pointer;font-size:14.5px;font-weight:500;
    padding:9px 12px;border-radius:9px;text-align:left;color:var(--text);
    display:flex;justify-content:space-between;align-items:center;gap:16px;}
  /* One line per option, always. A wrapped row makes the menu look broken and
     the count float away from the label it belongs to. */
  .fmenu button > span{white-space:nowrap;}
  /* The label never wraps: a two-line row next to a right-aligned count reads
     as two options. The menu widens to the longest label instead — "Bulldog &
     mastiff" and "Mixed / unknown" are the widest we have. */
  .fmenu button > span{white-space:nowrap;}
  /* The pound range beside a size name. An <i> for brevity in the markup, so
     font-style has to be reset — it is a quieter register, not an emphasis.
     --muted against the row's --text is the same pairing the card facts and the
     dog count use for "the number under the thing it describes". */
  .fmenu .opt-d{font-style:normal;font-weight:500;color:var(--muted);
    margin-left:7px;}
  /* On the selected row the name turns accent; the range follows it rather than
     staying grey, or it reads as disabled next to a chosen label. */
  .fmenu button[aria-selected="true"] .opt-d{color:var(--accent);opacity:.75;}
  .fmenu button:hover{background:var(--hair2);}
  .fmenu button[aria-selected="true"]{color:var(--accent);font-weight:700;}
  /* Counted and dimmed rather than removed: seeing "Husky 0" is information,
     and it stops the menu reshuffling under the cursor as filters change. */
  .fmenu button:disabled{cursor:default;opacity:.34;}
  .fmenu button:disabled:hover{background:transparent;}
  .fmenu button b{font-weight:600;color:var(--muted);font-size:13px;
    font-variant-numeric:tabular-nums;}
  .fmenu button[aria-selected="true"] b{color:var(--accent);}

  /* ---------- saved empty state ---------- */
  .saved-empty{text-align:center;padding:70px 20px 40px;max-width:420px;
    margin:0 auto;}
  .saved-empty[hidden]{display:none;}
  .se-art svg{width:60px;height:60px;fill:none;stroke:var(--accent);
    stroke-width:1.6;opacity:.5;animation:sePulse 2.6s ease-in-out infinite;}
  @keyframes sePulse{
    0%,100%{transform:scale(1);opacity:.45;}
    50%{transform:scale(1.08);opacity:.75;}
  }
  .saved-empty h2{font-size:24px;letter-spacing:-.02em;margin:16px 0 8px;}
  .saved-empty p{color:var(--muted);font-size:15px;margin:0 0 22px;
    line-height:1.55;}
  .saved-empty .cta{display:inline-block;width:auto;padding:12px 22px;
    margin-top:0;cursor:pointer;}
  @media (prefers-reduced-motion:reduce){ .se-art svg{animation:none;} }

  /* ---------- faq ---------- */
  .faq{max-width:720px;margin:56px auto 0;padding-top:40px;
    border-top:1px solid var(--hair2);}
  .faq h2{font-size:24px;letter-spacing:-.022em;margin:0 0 18px;}
  .faq details{border-bottom:1px solid var(--hair2);padding:14px 0;}
  .faq summary{cursor:pointer;font-size:16px;font-weight:600;list-style:none;
    display:flex;justify-content:space-between;align-items:center;gap:12px;}
  .faq summary::-webkit-details-marker{display:none;}
  .faq summary::after{content:'+';color:var(--muted);font-weight:400;
    font-size:20px;line-height:1;}
  .faq details[open] summary::after{content:'–';}
  .faq p{font-size:15px;line-height:1.6;color:var(--muted);margin:10px 0 0;}
  /* The rescue names in the coverage answer link to their pages; without this
     they'd fall back to the UA's default blue. */
  .faq p a{color:var(--text);text-decoration:underline;
    text-decoration-color:var(--hair);text-underline-offset:2px;
    white-space:nowrap;}
  .faq p a:hover{color:var(--accent);text-decoration-color:var(--accent);}

  /* ---------- shared list bar ---------- */
  /* Sits above the grid rather than in the nav: it is about this visit, not a
     permanent control, and it has to be able to say two lines without pushing
     the nav around. Surface card on the page background, so it reads as a note
     laid on the page rather than a piece of chrome. */
  .shared-bar{margin:22px 0 -4px;}
  .shared-bar[hidden]{display:none;}
  .sb-in{display:flex;align-items:center;gap:16px;background:var(--surface);
    border:1px solid var(--hair);border-radius:18px;padding:16px 18px;
    box-shadow:var(--shadow);}
  .sb-text{min-width:0;flex:1 1 auto;}
  .sb-h{font-size:17px;font-weight:700;letter-spacing:-.015em;color:var(--text);}
  /* The honest line: how many of the shared dogs are still listed, and how many
     are not. A link outlives the dogs on it, so this is the difference between a
     stale link degrading into information and degrading into a blank page. */
  .sb-sub{font-size:14px;color:var(--muted);margin-top:3px;}
  /* .cta is the page's full-width block button — display:block, width:100%,
     margin-top:24px — which inside a flex row crushes the text beside it into a
     one-word column. Overridden rather than a new class so the colour, radius,
     focus ring and hover stay in one place. */
  /* `.sb-in .sb-save`, not `.sb-save`: .cta is declared further down the sheet,
     so at equal specificity it wins on source order and the button stays
     display:block/width:100%, crushing the text beside it into a one-word
     column. Two classes beats one regardless of order. */
  .sb-in .sb-save{flex:0 0 auto;width:auto;display:inline-block;margin-top:0;
    padding:12px 20px;font-size:15.5px;white-space:nowrap;}
  @media (max-width:680px){
    .shared-bar{margin:18px 0 -2px;}
    /* Stacked on a phone: side by side, the button either wrapped to two lines
       or squeezed the count into an ellipsis. */
    .sb-in{flex-direction:column;align-items:stretch;gap:13px;padding:15px 16px;}
    /* min-height, not more padding: 12px top and bottom against a 19px line box
       came out at 43px, one short of the touch floor, and nudging the padding to
       fix a single pixel is the kind of number nobody can explain later. */
    .sb-in .sb-save{width:100%;min-height:44px;}
  }

  /* ---------- footer ---------- */
  footer{text-align:center;padding:52px 0 72px;color:var(--muted);font-size:13.5px;}
  footer .date{font-weight:600;color:var(--text);font-size:15px;}
  footer a{color:var(--muted);}
  /* Terms/Privacy open modals, so they're buttons dressed as the Contact link. */
  .foot-link{background:none;border:0;padding:0;margin:0;font:inherit;
    color:var(--muted);cursor:pointer;text-decoration:underline;
    text-underline-offset:2px;}
  .foot-link:hover{color:var(--text);}
  .foot-rescues{margin:20px auto 18px;max-width:600px;line-height:1.95;}
  .foot-hd{display:block;font-size:11px;font-weight:700;letter-spacing:.09em;
    text-transform:uppercase;opacity:.7;margin-bottom:1px;}
  /* A rescue's name is one unit — don't break "Sean Casey Animal Rescue"
     across two lines. */
  .foot-rescues a{white-space:nowrap;}
  .foot-rescues a:hover{color:var(--text);}
  .foot-all{font-weight:600;}

  /* ---------- modal shell ---------- */
  .scrim{position:fixed;inset:0;background:rgba(0,0,0,.42);
    backdrop-filter:saturate(180%) blur(24px);-webkit-backdrop-filter:saturate(180%) blur(24px);
    display:none;align-items:center;justify-content:center;padding:24px;z-index:100;
    opacity:0;transition:opacity .28s ease;}
  .scrim.on{display:flex;} .scrim.vis{opacity:1;}
  .modal{position:relative;background:var(--surface);border-radius:26px;width:min(880px,100%);
    max-height:min(88vh,900px);overflow:visible;box-shadow:var(--shadow-lg);
    display:flex;flex-direction:column;
    transform:scale(.96) translateY(12px);
    transition:transform .34s cubic-bezier(.2,.9,.25,1);}
  /* The contact button is the whole point of the page, so it never scrolls
     out of reach — the body scrolls beneath a pinned action bar. */
  .m-scroll{overflow-y:auto;-webkit-overflow-scrolling:touch;flex:1 1 auto;
    border-radius:26px 26px 0 0;
    /* The default desktop scrollbar is a chunky grey slab against a rounded
       modal. Slim, translucent, and only as present as it needs to be. */
    scrollbar-width:thin;
    scrollbar-color:var(--hair) transparent;
    overscroll-behavior:contain;}
  .m-scroll::-webkit-scrollbar{width:7px;}
  .m-scroll::-webkit-scrollbar-track{background:transparent;}
  .m-scroll::-webkit-scrollbar-thumb{background:var(--hair);
    border-radius:980px;border:2px solid transparent;background-clip:content-box;}
  .m-scroll::-webkit-scrollbar-thumb:hover{background:var(--muted);
    border:2px solid transparent;background-clip:content-box;}
  /* No footer (about, share, contact) means the scroller owns all four. */
  .modal:not(:has(.m-foot)) .m-scroll{border-radius:26px;}
  .m-foot{flex:0 0 auto;padding:14px 28px 18px;border-top:1px solid var(--hair);
    background:var(--surface);border-radius:0 0 26px 26px;}
  .m-foot .cta{margin-top:0;}
  .m-foot .cta-sub{margin-top:9px;}
  .foot-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
  .act-note{font-size:12.5px;color:var(--muted);text-align:center;
    margin-bottom:10px;}
  /* Program terms are several sentences of real commitment, so they get a box
     and a left edge to read down — centred prose that long is a wall. */
  .act-note.prog{text-align:left;line-height:1.55;background:var(--hair2);
    border-radius:12px;padding:10px 12px;}
  .act-note.prog b{color:var(--warn);}
  /* .cta is declared later in the sheet, so match specificity to win. */
  .cta.cta2{background:var(--hair2);color:var(--text);
    border:1px solid var(--hair);cursor:pointer;}
  .cta.cta2:hover{opacity:1;background:var(--hair);}
  .cta.cta2:disabled{opacity:.5;cursor:default;}
  /* iOS-style share glyph: tray with an arrow lifting out of it. */
  .shr-ic{width:17px;height:17px;margin-right:8px;vertical-align:-3px;
    fill:none;stroke:currentColor;stroke-width:1.9;
    stroke-linecap:round;stroke-linejoin:round;}
  .cta.cta2{display:inline-flex;align-items:center;justify-content:center;}

  /* ---------- contact sheet ---------- */
  .contact{padding:26px 24px 24px;}
  .ct-lead{font-size:14.5px;color:var(--muted);margin:10px 0 16px;
    line-height:1.5;}
  .ct-addr{font-size:14px;background:var(--hair2);border-radius:10px;
    padding:11px 13px;text-align:center;margin-bottom:14px;user-select:all;
    font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}
  .ct-btns{display:grid;grid-template-columns:1fr 1fr;gap:10px;
    margin-bottom:10px;}
  .ct-btns .cta{margin-top:0;font-size:15px;padding:13px;}
  .ct-prev{margin-top:8px;}
  .ct-prev summary{cursor:pointer;font-size:13px;color:var(--muted);}
  .ct-prev pre{white-space:pre-wrap;font-size:12.5px;color:var(--muted);
    background:var(--hair2);border-radius:10px;padding:12px;margin-top:9px;
    max-height:190px;overflow:auto;font-family:inherit;line-height:1.5;}

  /* ---------- share sheet ---------- */
  .share{padding:26px 24px 24px;}
  .story-wrap{display:flex;justify-content:center;margin:14px 0 16px;}
  .story-img,.story-ph{width:min(232px,58vw);aspect-ratio:9/16;border-radius:16px;
    background:var(--hair2);box-shadow:var(--shadow);display:grid;place-items:center;
    color:var(--muted);font-size:13px;object-fit:cover;}
  .share-url{font-size:12px;color:var(--muted);background:var(--hair2);
    border-radius:10px;padding:10px 12px;word-break:break-all;text-align:center;
    margin-bottom:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}
  .share-btns{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
  .share-btns .cta{margin-top:0;}
  .scrim.vis .modal{transform:none;}
  /* Photo column spans both right-hand panels and stretches to match their
     combined height, so the photo is tall enough to actually see the dog. */
  /* The RIGHT column sets the height; the photo fills whatever is left after
     the thumbnails. So hero + thumbs always equals the panels beside it, and
     a dog with no thumbnails simply gets a taller photo — the block never
     changes size. Height comes from the grid, never from the image, so a
     portrait photo can't stretch the modal. */
  .m-media{grid-row:1 / span 2;display:flex;flex-direction:column;gap:9px;
    min-width:0;min-height:0;}
  .m-hero{position:relative;flex:1 1 auto;min-height:0;
    background:var(--hair);border-radius:16px;overflow:hidden;}
  .m-hero img{width:100%;height:100%;object-fit:cover;object-position:center;
    display:block;}
  .m-close{position:absolute;top:-12px;right:-12px;width:36px;height:36px;border:none;
    border-radius:50%;cursor:pointer;font-size:17px;line-height:1;
    background:rgba(28,28,30,.72);color:#fff;
    backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
    display:grid;place-items:center;z-index:20;
    box-shadow:0 3px 12px rgba(0,0,0,.3);
    transition:background .2s,transform .2s;}
  .m-close:active{transform:scale(.92);}
  .m-close:hover{background:rgba(0,0,0,.75);}
  .thumbs{display:flex;gap:8px;overflow-x:auto;flex:0 0 auto;
    scrollbar-width:none;}
  .m-media:has(.thumbs) .m-hero{min-height:0;}
  .thumbs::-webkit-scrollbar{height:0;}
  .thumbs img{width:60px;height:60px;object-fit:cover;object-position:center;
    border-radius:10px;cursor:pointer;
    flex:0 0 auto;opacity:.5;transition:opacity .2s;border:2px solid transparent;}
  .thumbs img:hover,.thumbs img.sel{opacity:1;border-color:var(--accent);}
  .m-body{padding:18px 22px 28px;}
  /* No photo: content starts at the top, with room for the close button. */
  .m-scroll.no-hero .m-body{padding-top:30px;}
  .m-scroll.no-hero .m-close{background:var(--hair);color:var(--text);}
  .m-name-row{display:flex;align-items:center;justify-content:space-between;
    gap:14px;margin-bottom:4px;}
  .m-name{font-size:clamp(40px,4.4vw,58px);font-weight:800;letter-spacing:-.035em;
    margin:0;line-height:1;min-width:0;overflow-wrap:anywhere;}
  /* The rescue reads as attribution under the name, not as another pill.
     When we know the rescue's site it's a link; otherwise plain text. */
  .m-rescue{font-size:15px;font-weight:600;color:var(--muted);
    margin:0 0 15px;overflow-wrap:anywhere;align-self:flex-start;
    text-decoration:none;transition:color .15s ease;}
  a.m-rescue:hover{color:var(--accent);text-decoration:underline;}
  /* Same control as the cards, sized for the detail view and on a surface
     rather than over a photo. */
  .m-save{position:static;right:auto;top:auto;flex:0 0 auto;width:44px;height:44px;
    background:var(--hair);}
  .m-save:hover{background:var(--hair2);}
  .m-save .hrt{width:23px;height:23px;stroke:var(--muted);}
  .m-save:hover .hrt{stroke:var(--text);}
  .m-save.on .hrt{fill:var(--accent);stroke:var(--accent);}
  /* Identity on the left, ratings on the right. The left column holds the name
     and chips — content every dog has — so this never collapses to an empty
     box the way a traits-only column did. The bars never needed full width. */
  .topgrid{display:grid;grid-template-columns:1fr 1fr;gap:14px;
    align-items:stretch;margin-bottom:14px;}
  /* Both columns share the same grey card so the top of the modal reads as one
     balanced band instead of a floating name next to a panel. */
  .idcol{min-width:0;background:var(--hair2);border-radius:18px;
    padding:20px 20px 18px;display:flex;flex-direction:column;
    justify-content:center;}
  .topgrid .scores{margin:0;}
  .chips{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:0;}
  .chip{font-size:12.5px;padding:6px 12px;border-radius:980px;background:var(--hair2);
    color:var(--text);font-weight:500;}
  .chip.wait{background:var(--warn-soft);color:var(--warn);font-weight:600;}
  .chip.program{background:var(--warn-soft);color:var(--warn);font-weight:600;}
  .m-views{left:12px;top:12px;}
  .chip[hidden]{display:none;}

  /* Benefits and challenges, spelled out rather than colour-coded confetti. */

  .tl-hd{font-size:11px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
    color:var(--muted);margin-bottom:13px;}
  .tlists ul{list-style:none;margin:0;padding:0;display:grid;
    grid-template-columns:1fr;gap:9px;}
  .tlists li{display:flex;align-items:flex-start;gap:9px;font-size:14.5px;
    line-height:1.4;color:var(--text);min-width:0;}
  .tl-ic{flex:0 0 auto;display:grid;place-items:center;width:17px;height:17px;
    border-radius:50%;font-size:10.5px;font-weight:800;font-style:normal;
    color:#fff;line-height:1;margin-top:1px;}
  .tlists li.good .tl-ic{background:var(--good);}
  .tlists li.warn .tl-ic{background:var(--warn);}

  /* ---------- score bars ---------- */
  .scores{background:var(--hair2);border-radius:18px;padding:20px 20px 18px;
    margin-bottom:22px;display:flex;flex-direction:column;}
  .sc-hd{font-size:11px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
    color:var(--muted);margin-bottom:18px;}
  /* Spread the three bars through whatever height the panel ends up with,
     so they never bunch at the top of a stretched card. */
  .sc{margin-bottom:24px;}
  .sc:last-of-type{margin-bottom:0;}
  .sc-top{display:flex;align-items:center;gap:8px;margin-bottom:9px;}
  .sc-ic{font-size:15px;width:20px;text-align:center;}
  .sc-lb{font-size:14px;font-weight:600;flex:1;}
  /* A marker on a named axis, not a fill. The fill was unreadable, and not for
     want of a label: its direction meant three different things down the four
     rows. Energy is neutral, a full Apartment fit is good, a full Experience
     needed is *harder*, a full Home alone is good again. So a long bar was
     "better" on two rows and "more demanding" on a third, with no rule to learn
     — and with both ends unlabelled there was nothing to say what 20% was 20%
     of. A pin has no direction to misread: it says this dog is here, between
     these two things, and the two things are now written underneath.
     It also fixes the middle. At `v*20` a score of 3 filled 60%, which reads as
     "quite a lot of"; centred, 3 sits dead centre and looks like the middle. */
  .sc-track{position:relative;height:6px;border-radius:980px;
    background:var(--hair);margin:0 11px;}
  /* The heart from the save button. Same mark doing a second job, and it makes
     the row feel like part of the page rather than a chart dropped into it. */
  .sc-pin{position:absolute;top:50%;width:22px;height:22px;
    transform:translate(-50%,-50%) scale(.2);opacity:0;
    transition:transform .5s cubic-bezier(.34,1.4,.64,1),opacity .28s ease;
    filter:drop-shadow(0 1px 2px rgba(0,0,0,.22));}
  /* The wordmark's heart, outline and all: accent fill inside a white edge.
     paint-order:stroke puts the stroke behind the fill so it reads as an outline
     around the shape rather than a line drawn through it — without that, a 3px
     stroke eats 1.5px of the heart from every side and the shape thins out. */
  .sc-pin svg{width:100%;height:100%;display:block;fill:var(--accent);
    stroke:#fff;stroke-width:3.8;paint-order:stroke;
    stroke-linejoin:round;overflow:visible;}
  /* Grows in when the modal opens, the same beat the fill used to animate on. */
  .sc-pin.in{transform:translate(-50%,-50%) scale(1);opacity:1;}
  .sc-ends{display:flex;justify-content:space-between;gap:10px;
    margin:9px 11px 0;font-size:11.5px;color:var(--muted);line-height:1.3;}
  /* The end the dog is actually at. Only ever one of the two, and never both:
     a score of 3 leaves them both quiet, which is the honest answer. */
  .sc-ends .on{color:var(--text);font-weight:600;}
  .sc-ends span{max-width:47%;}

  /* ---------- size + cost ---------- */
  .sizecost{margin-bottom:22px;background:var(--hair2);border-radius:18px;}
  .sc-inner{display:grid;grid-template-columns:1.1fr 1fr;}
  .sc-inner > .sc-right{border-left:1px solid var(--hair);
    display:flex;flex-direction:column;}
  /* Two stacked blocks on the right, divided rather than gapped. */
  .sc-right > * + *{border-top:1px solid var(--hair);}
  .sc-right > *{flex:1;}
  .sc-block{padding:18px 20px 18px;display:flex;flex-direction:column;}
  .sc-block p{font-size:14.5px;line-height:1.5;color:var(--text);margin:0;}
  .gw{height:8px;border-radius:980px;background:var(--hair);overflow:hidden;
    margin-top:14px;}
  .gw span{display:block;height:100%;border-radius:980px;
    background:var(--accent);}
  .gw-l{display:flex;justify-content:space-between;font-size:11.5px;
    color:var(--muted);margin-top:6px;font-weight:600;}
  /* Where this dog sits between a chihuahua and a mastiff. */
  .szscale{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;
    margin-top:18px;}
  .szb{display:flex;flex-direction:column;gap:6px;}
  .szb i{height:7px;border-radius:980px;background:var(--hair);display:block;}
  .szb em{font-style:normal;font-size:10.5px;font-weight:600;
    color:var(--muted);letter-spacing:.01em;}
  .szb.on i{background:var(--accent);}
  .szb.on em{color:var(--accent);}
  .szb.from i{background:var(--accent);opacity:.35;}
  .szcap{font-size:12.5px;color:var(--muted);margin-top:11px;line-height:1.45;}
  .cost-big{font-size:30px;font-weight:800;letter-spacing:-.03em;
    margin-bottom:12px;}
  .cost-big span{font-size:14px;font-weight:600;color:var(--muted);
    letter-spacing:0;}
  .cost-list{list-style:none;margin:0 0 12px;padding:0;}
  .cost-list li{display:flex;justify-content:space-between;font-size:13.5px;
    padding:5px 0;border-bottom:1px solid var(--hair2);color:var(--muted);}
  .cost-list li:last-child{border-bottom:0;}
  .cost-list b{color:var(--text);font-weight:600;}
  .cost-note{font-size:11.5px !important;color:var(--muted) !important;
    line-height:1.45 !important;}
  @media (max-width:680px){
    .sc-inner{grid-template-columns:1fr;}
    .sc-inner > .sc-right{border-left:0;border-top:1px solid var(--hair);}
  }

  /* ---------- tabs ---------- */
  .tabs{display:flex;gap:4px;background:var(--hair2);padding:3px;border-radius:11px;
    margin-bottom:16px;}
  .tab{all:unset;cursor:pointer;flex:1;text-align:center;font-size:13px;font-weight:600;
    padding:8px 6px;border-radius:9px;color:var(--muted);transition:all .2s;}
  .tab.on{background:var(--surface);color:var(--text);box-shadow:0 1px 3px rgba(0,0,0,.08);}
  .pane{display:none;font-size:15px;line-height:1.62;}
  .pane.on{display:block;}
  .pane h4{font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
    color:var(--muted);margin:18px 0 6px;}
  .pane h4:first-of-type{margin-top:0;}
  .pane p{margin:0;}
  .pane .bio{white-space:pre-wrap;}
  .breed-tag{display:block;font-size:23px;font-weight:700;letter-spacing:-.02em;
    color:var(--text);margin:2px 0 14px;line-height:1.2;}
  /* One scannable line per topic: bold = this dog, plain = breed context. */
  .fact{padding:13px 0;border-bottom:1px solid var(--hair2);}
  .fact:last-child{border-bottom:0;padding-bottom:2px;}
  .fact h4{margin:0 0 5px;}
  .fact p{font-size:14.5px;line-height:1.58;color:var(--text);margin:0;}
  .fact p b{font-weight:650;}

  .cta{display:block;width:100%;text-align:center;background:var(--accent);color:#fff;
    text-decoration:none;padding:15px;border-radius:14px;font-weight:600;font-size:16.5px;
    transition:opacity .2s;margin-top:24px;
    /* Also used on <button>, which brings a default border, its own font and
       an iOS appearance. Reset all three. */
    border:none;appearance:none;-webkit-appearance:none;
    font-family:inherit;line-height:normal;cursor:pointer;}
  .cta:focus-visible{outline:3px solid var(--accent);outline-offset:3px;}
  .cta:hover{opacity:.88;}
  .cta-sub{text-align:center;font-size:13px;color:var(--muted);margin-top:11px;}
  .cta-sub a{color:var(--muted);}

  /* ---------- similar dogs ---------- */
  .sim{margin-top:28px;padding-top:22px;border-top:1px solid var(--hair);}
  .sim-hd{font-size:23px;font-weight:700;letter-spacing:-.02em;
    color:var(--text);margin-bottom:6px;text-transform:none;}
  .sim-note{font-size:13px;color:var(--muted);margin:0 0 14px;line-height:1.5;}
  .sim-row{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;}
  .sim-card{all:unset;cursor:pointer;min-width:0;
    transition:transform .25s cubic-bezier(.2,.8,.2,1);}
  .sim-card:hover{transform:translateY(-4px);}
  .sim-ph{width:100%;aspect-ratio:1/1;border-radius:14px;object-fit:cover;display:block;
    background:var(--hair);}
  @media (max-width:520px){ .sim-row{grid-template-columns:repeat(2,1fr);} }
  .sim-ph.noph{display:grid;place-items:center;font-size:34px;opacity:.35;}
  .sim-nm{font-size:13.5px;font-weight:600;margin-top:7px;white-space:nowrap;
    overflow:hidden;text-overflow:ellipsis;}
  .sim-rs{font-size:11.5px;color:var(--muted);white-space:nowrap;overflow:hidden;
    text-overflow:ellipsis;}
  .sim-why{font-size:10.5px;color:var(--accent);font-weight:600;margin-top:3px;
    line-height:1.35;}

  /* ---------- about modal ---------- */
  .about-hero{background:
      radial-gradient(120% 130% at 50% 0%, var(--accent-soft) 0%, transparent 68%);
    padding:52px 32px 34px;display:flex;justify-content:center;}
  .about-logo{width:min(66%,300px);height:auto;display:block;
    animation:heroIn .6s cubic-bezier(.34,1.4,.64,1) both;}
  @keyframes heroIn{from{transform:scale(.86) translateY(10px);opacity:0;}
    to{transform:none;opacity:1;}}
  .about-body{padding:4px 34px 34px;}
  .about h2{font-size:clamp(23px,3.4vw,30px);letter-spacing:-.025em;line-height:1.22;
    margin:0 0 26px;text-align:center;font-weight:700;}
  .creed{list-style:none;margin:0 0 28px;padding:0;}
  .creed li{display:flex;gap:14px;align-items:flex-start;padding:14px 0;
    border-top:1px solid var(--hair2);font-size:15.5px;line-height:1.55;
    color:var(--muted);}
  .creed li:last-child{border-bottom:1px solid var(--hair2);}
  .creed .num{flex:0 0 auto;font-size:11.5px;font-weight:800;letter-spacing:.06em;
    color:var(--accent);padding-top:4px;font-variant-numeric:tabular-nums;}
  .creed b{color:var(--text);font-weight:650;}
  /* Get in touch + Share, side by side under the creed. */
  .about-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:24px;}
  .about-actions .cta{margin-top:0;}
  /* ---------- terms / privacy ---------- */
  .legal{padding:34px 34px 38px;}
  .legal-h{font-size:26px;font-weight:800;letter-spacing:-.02em;margin:0 0 4px;}
  .legal .upd{font-size:12.5px;color:var(--muted);margin:0 0 18px;}
  .legal h3{font-size:15px;font-weight:700;color:var(--text);margin:22px 0 6px;}
  .legal p{font-size:14.5px;line-height:1.62;color:var(--muted);margin:0 0 12px;}
  .legal ul{margin:0 0 12px;padding-left:20px;}
  .legal li{font-size:14.5px;line-height:1.62;color:var(--muted);margin:0 0 8px;}
  .legal b{color:var(--text);font-weight:650;}
  .legal a{color:var(--accent);}
  @media (prefers-reduced-motion:reduce){ .about-logo{animation:none;} }

  /* ---------- subscribe modal ---------- */
  /* Narrower than the dog modal — one field / one message shouldn't sprawl. */
  .modal.narrow{width:min(460px,100%);}
  .modal.mid{width:min(560px,100%);}
  .sub-modal{padding:42px 30px 34px;text-align:center;background:
      radial-gradient(120% 110% at 50% 0%, var(--accent-soft) 0%, transparent 62%);}
  .sub-logo{width:min(52%,200px);height:auto;display:block;margin:0 auto 22px;}
  .sub-modal h2{font-size:26px;letter-spacing:-.022em;margin:0 0 9px;}
  .sub-modal p{color:var(--muted);font-size:15px;margin:0 auto 22px;max-width:400px;
    text-wrap:balance;}
  body.locked{overflow:hidden;}

  /* ---------- mobile ---------- */
  @media (max-width:720px){
    .wrap{padding:0 16px;}
    header{padding:22px 0 4px;}
    .brand-wrap{margin-bottom:8px;}
    /* Two across beats one giant column — more dogs per scroll. */
    .grid{grid-template-columns:repeat(2,1fr);gap:13px;padding:16px 0 4px;}
    .card{border-radius:15px;}
    .meta{padding:11px 12px 13px;}
    .nm-row{margin:0 0 8px;}
    .nm{font-size:19px;}
    .new-chip{right:8px;bottom:8px;font-size:8.5px;padding:3px 7px;
      letter-spacing:.07em;}
    .save{width:38px;height:38px;right:7px;top:7px;}
    .hrt{width:19px;height:19px;}
    .waiting{right:8px;bottom:8px;font-size:9.5px;}
    .pill{font-size:10.5px;padding:3px 7px;}
    /* Two pills fit a half-width card; a third gets sliced. */
    .pills .pill:nth-child(3){display:none;}
    .views{font-size:11.5px;padding:5px 9px;left:8px;top:8px;}
    .sub-sec{padding:38px 20px;border-radius:22px;margin-top:38px;}
    .sub-form{flex-direction:column;}
    .hero-sub{flex-direction:column;gap:8px;}
    .hero-sub button{text-align:center;padding:13px;}
    .sub-form input,.sub-form button{width:100%;}

    /* Full-screen on phones — a dog's detail view is the whole task, and a
       floating sheet just wastes the smallest screen we have. */
    .scrim{padding:0;align-items:stretch;}
    .modal{width:100%;max-width:100%;height:100%;max-height:100%;border-radius:0;
      transform:translateY(18px) scale(1);}
    .m-scroll,.m-foot,.modal:not(:has(.m-foot)) .m-scroll{border-radius:0;}
    .scrim.compact .modal{border-radius:22px;}
    .scrim.compact .m-scroll,
    .scrim.compact .modal:not(:has(.m-foot)) .m-scroll{border-radius:22px;}
    .scrim.vis .modal{transform:none;}
    /* The close button rides above the hero image, always top-right. */
    .m-close{position:fixed;top:max(14px,env(safe-area-inset-top));right:14px;
      width:38px;height:38px;font-size:18px;z-index:20;
      background:rgba(0,0,0,.62);color:#fff;}
    .m-foot{padding:12px 18px max(14px,env(safe-area-inset-bottom));}
    .foot-row{grid-template-columns:1fr;gap:8px;}
    .share{padding:22px 18px 20px;}
    .modal.narrow{width:100%;}
    /* The subscribe and contact sheets stay centred cards — one field each. */
    .scrim.compact{align-items:center;padding:18px;}
    .scrim.compact .modal{height:auto;max-height:92vh;border-radius:22px;}
    .scrim.compact .m-close{position:absolute;}
    /* About is a page of reading, so it goes full screen like a dog does. */
    .scrim.compact.sheet{align-items:stretch;padding:0;}
    .scrim.compact.sheet .modal{width:100%;max-width:100%;height:100%;
      max-height:100%;border-radius:0;}
    .scrim.compact.sheet .m-close{position:fixed;
      top:max(14px,env(safe-area-inset-top));right:14px;}
    /* Centring the content leaves space above the hero, which showed the
       modal surface as a white band. Move the gradient onto the container so
       it covers the whole sheet.
       Centred with auto margins rather than justify-content, and scrollable.
       About is the one sheet with no .m-scroll inside it — Terms and Privacy
       both have one — so the sheet itself has to be the scroller. With
       `justify-content:center` and no scroller it fit a 812px phone by three
       pixels and lost 51px off the bottom on a 667px one, with no way to reach
       it: centring overflow pushes it past BOTH ends of the box, and a flex
       container will not let you scroll back to what it pushed off the start.
       Auto margins centre it while it fits and collapse to zero when it does
       not, which is the behaviour centring was reached for in the first
       place. */
    .scrim.compact.sheet .about{display:flex;flex-direction:column;
      justify-content:flex-start;min-height:100%;overflow-y:auto;
      background:radial-gradient(120% 60% at 50% 0%,
        var(--accent-soft) 0%, transparent 62%);}
    .scrim.compact.sheet .about-hero{margin-top:auto;}
    .scrim.compact.sheet .about-body{margin-bottom:auto;}
    .scrim.compact.sheet .about-hero{background:none;
      padding-top:max(58px,calc(env(safe-area-inset-top) + 46px));}
    .scrim.compact.sheet .about-body{padding-bottom:max(28px,
      env(safe-area-inset-bottom));}
    .topgrid.with-photo{grid-template-rows:none;min-height:0;}
    .m-media{grid-row:auto;}
    .m-hero{aspect-ratio:1/1;flex:0 0 auto;}
    .m-name{font-size:40px;}
    .m-body{padding:18px 18px 26px;}
    .thumbs{padding-left:18px;padding-right:18px;}
    .about-hero{padding:36px 20px 22px;}
    .about-body{padding:0 20px 26px;}
    .legal{padding:26px 20px 28px;}
    .about h2{font-size:22px;}
    .creed li{font-size:14.5px;gap:11px;}
    .sub-modal{padding:34px 20px 28px;}
    .sim-row{grid-template-columns:repeat(2,1fr);}
    .scores{padding:16px 16px 13px;}
    .tlists ul{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px 14px;}
    /* Stack on phones — 288px of bars beside chips is unreadable. */
    .topgrid{grid-template-columns:1fr;gap:16px;margin-bottom:16px;}
  }
  /* Only the very narrowest phones drop to one column; 375px still fits two. */
  @media (max-width:680px){
    .filter-bar{padding:10px 12px;margin-top:20px;}
    .fb-label{font-size:14px;}
    .fb-clear{font-size:13px;padding:6px 11px;}
    /* The pills scroll sideways as one row rather than wrapping five of them
       onto three lines and pushing the dogs off the screen. `safe center` keeps
       them centred until they overflow, then falls back to flex-start — plain
       `center` in a scroll container puts the first pill out of reach. */
    .fbar{margin:22px 0 -4px;}
    /* The row scrolls sideways again, and that is only safe because an open menu
       no longer lives inside it — see openMenuInLayer(). Read this before
       touching either.
       The menus are position:fixed. WebKit clips a fixed descendant to any
       scrolling ancestor's box, and makes a containing block out of any ancestor
       carrying transform, filter, backdrop-filter, perspective, contain or
       will-change. While the menus were children of this row, that meant each
       one was cut down to the row's own 87px — which on a phone is
       indistinguishable from the menu opening behind the dogs.
       That shipped twice: first from -webkit-overflow-scrolling:touch, then
       straight back from the plain overflow-x:auto left behind, because z-index
       fixes paint ORDER and clipping is not an ordering problem. The row can
       scroll now because the menu is moved to a body-level layer when it opens,
       where nothing above it clips.
       So: scroll and mask this row as much as you like. What must not change is
       that an open menu is NOT a descendant of it.
       tests/test_multicity.py::test_filter_menus_cannot_be_clipped holds the
       pair together — if this row scrolls, the layer and the move must exist. */
    .fbar-pills{flex-wrap:nowrap;justify-content:safe center;overflow-x:auto;
      padding-bottom:2px;scrollbar-width:none;}
    .fbar-pills::-webkit-scrollbar{display:none;}
    /* The fade that says the row keeps going. A mask, not an overlaid gradient,
       for two reasons: a mask cannot intercept a touch, so the pills under it
       stay tappable and the row stays scrollable; and it fades to *transparent*
       rather than to a colour, so it is correct on whatever background the
       current theme paints without knowing anything about it. The classes are
       set by paintPillFade() — the edge only fades when there is something
       behind it, so the cue disappears at the end of the scroll and never
       appears at all if the pills happen to fit. */
    .fbar-pills.fade-r{-webkit-mask-image:linear-gradient(to right,#000
      calc(100% - 34px),transparent);mask-image:linear-gradient(to right,#000
      calc(100% - 34px),transparent);}
    .fbar-meta{margin:12px 0 -10px;}
    /* 17px here, against 19px on desktop. 19px is exactly the phone size of the
       card dog names (.nm, 19px/700), so the count sat at the size and weight of
       the very thing it is a heading for: measured at 390px it stopped reading as
       a heading over the grid and competed with the names instead. Desktop keeps
       19px, where .nm is 23px and the count is already a step below it. 19 to 17
       is the same step the rest of the page's type takes across this breakpoint
       — card names 23 to 19, pills 15 to 13.5 — so the count moves with the
       ladder rather than against it. Size is the only thing that moves: the bold,
       the tracking and the left flush all stay, and the <b> carries no size of
       its own so the numeral comes down with the line. */
    .fbar-n{font-size:17px;}
    /* No sort override here on purpose: it is 17px at every width from its base
       declaration, so this breakpoint is where the count comes down to meet it
       rather than where the sort moves. */
    /* 14.5px, and desktop stays at 15px. This was 13.5px, chosen because the
       full 15px cost 22px of overflow for no gain anyone could see. Half of that
       still holds and half didn't survive being read on a phone: 13.5px is a
       step below the 14.5px the *menu* rows these pills open are set at, so the
       control read as smaller than its own contents. 14.5px puts the trigger and
       its options on one size and stops the row looking like fine print.
       It is not free. Measured at 375px the row goes from 6px of overflow to
       19px, so a little more of the last pill starts off-screen — but the row has
       been a scroller with a fade cue at this width for a while, so this makes an
       existing scroll slightly longer rather than introducing one. The full 15px
       is still declined: 26px at the same width, and it would flatten the step
       the rest of the page's type takes across this breakpoint (card names 23 to
       19, count 19 to 17) for the sake of matching desktop, which has room this
       breakpoint doesn't. */
    /* 12px of side padding, not the desktop 14. The drawn chevrons are ~5px
       wider each than the text glyph they replaced, which put 15.7px back onto
       a row that had 8.7px of room — enough to reintroduce the overflow the
       "Foster" rename had just removed. Padding is the cheapest place to find
       it back: 2px a side across four pills returns 16px, the pills still look
       generously spaced at this size, and neither the chevron nor the label had
       to shrink to pay for it. */
    .fpill > button,.fpill-t{font-size:14.5px;padding:8px 12px;}
    /* The sort deliberately does NOT come down to 14.5px with them: it is 17px
       at every width, and it is now short enough that the width that costs is
       no longer the problem it was when it read "Sort by: Recently added". */
    /* Anchored to the row, not the pill, so a menu on the last pill can't open
       off the edge of a phone. */
    .fmenu{position:fixed;left:14px;right:14px;min-width:0;
      top:auto;transform:translateY(-6px);}
    /* Re-asserted: the desktop right-anchor for the sort menu is more specific
       than the rule above and would otherwise defeat this full-width sheet. */
    .fpill.fsort .fmenu{left:14px;right:14px;}
    .pick-menu{position:fixed;left:50%;transform:translateX(-50%) translateY(-6px);
      width:calc(100vw - 28px);max-width:330px;min-width:0;}
    .pick.open .pick-menu{transform:translateX(-50%);}
    .pick-menu button,.pick-menu a{padding:13px 15px;font-size:16.5px;}
  }
  @media (max-width:400px){
    .tlists ul{grid-template-columns:1fr;}
  }
  /* This used to also strip the sort back to its value alone, with a border, on
     the grounds that "Sort by: Recently added" ran the results row out of line
     below about 340px. The trigger is now "Sort by" and that whole problem is
     gone: measured at 320px, the narrowest phone worth serving, the count and
     the sort leave 123px of slack between them. There is nothing left here to
     fall back to, so the fallback went rather than sitting in the file being
     read as still-needed. One column is all this breakpoint still does. */
  @media (max-width:339px){
    .grid{grid-template-columns:1fr;}
  }
