:root {
  --cream: #f6ecd9;
  --brown: #5c3d2e;
  --ink: #2b2320;
  --terracotta: #d97a5c;
  --sage: #8ca47c;
  --pink: #f2b6c6;
  --berry: #c1495c;
  --white: #fffaf0;
  --sky: #8fb8d4;
  --mustard: #eab649;
  --mustard-light: #f3dfae;
  --lavender: #b6a6ca;
  --mint: #a9cdae;
  --sky-bg: #52beff;
  --panel-beige: #f3eae1;
  --header-lavender: #f0c9fb;
  /* Periwinkle — derived from --sky-bg (203°, 100%, 66%): hue shifted
     toward violet (225°, periwinkle territory, distinct from
     --header-lavender's 287° and --pink's 344°), lightened to 82% so
     black --ink text/icons on top stay high-contrast (8.2:1, well past
     WCAG AAA), saturation eased to 90% to match the brightness tier of
     the app's other pastel accents rather than staying fully saturated. */
  --periwinkle: #a8bcfa;
}

* {
  box-sizing: border-box;
}

html, body {
  height: 100vh;
  height: 100dvh; /* avoids mobile browsers' address-bar resize cutting off content */
  margin: 0;
  overflow: hidden;
  background: var(--sky-bg);
  color: var(--ink);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.title, .panel-title, h2, h3 {
  font-family: "Press Start 2P", monospace;
}

/* ---------- shared world background ---------- */

.world-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: var(--sky-bg);
  overflow: hidden;
}

.cloud {
  position: absolute;
  left: 0;
  image-rendering: pixelated;
  pointer-events: none;
  animation: cloud-drift linear infinite;
}

.cloud--1 { top: 4%; width: 460px; animation-duration: 70s; animation-delay: -7s; }
.cloud--2 { top: 9%; width: 380px; animation-duration: 60s; animation-delay: -24s; }
.cloud--3 { top: 56%; width: 340px; animation-duration: 65s; animation-delay: -42s; }
.cloud--4 { top: 44%; width: 300px; animation-duration: 55s; animation-delay: -47s; }

@keyframes cloud-drift {
  from { transform: translateX(-100%); }
  to { transform: translateX(100vw); }
}

.grass {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  aspect-ratio: 1920 / 297;
  background-image: url("../assets/p1-grass.png");
  background-size: 100% 100%;
  image-rendering: pixelated;
}

.ambient-cat {
  position: absolute;
  bottom: 11%;
  image-rendering: pixelated;
  animation: cat-idle ease-in-out infinite;
}

.ambient-cat--1 { left: 5%; height: 150px; animation-duration: 4.5s; }
.ambient-cat--2 { left: 24%; height: 190px; animation-duration: 3.8s; animation-delay: -1.2s; }
.ambient-cat--3 { right: 6%; height: 230px; animation-duration: 4.2s; animation-delay: -2.4s; }

@keyframes cat-idle {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50% { transform: translateY(-5px) rotate(-1deg); }
}

.ambient-cat {
  display: none;
}

.pixel-title {
  font-family: "Press Start 2P", monospace;
  color: #fff;
  text-shadow: 3px 3px 0 rgba(20, 60, 100, 0.4), 6px 6px 0 rgba(20, 60, 100, 0.2);
  line-height: 1.5;
  margin: 0 0 2rem;
}

/* ---------- main app screen ---------- */

#screen-app {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding: 1.25rem 2rem 2rem;
}

.info-btn {
  position: absolute;
  /* Constant top:15px / 40px size at every width, matching the account
     badge's own values exactly (its top is hardcoded in mode:'fixed' and
     isn't exposed via init() at all; size we set to 40 via init() in
     app.js) — same size + same top makes them automatically same-center
     aligned too, rather than needing a separate alignment calculation
     per breakpoint. Previously this varied by breakpoint (44px/38px,
     different top values), which is exactly what caused the size
     mismatch and vertical misalignment found in real testing. */
  top: 15px;
  right: 1.5rem;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 3px solid var(--ink);
  background: var(--header-lavender);
  color: var(--ink);
  font-family: "Press Start 2P", monospace;
  font-size: 0.9rem;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
  cursor: pointer;
  /* Above .edge-cat's z-index:5 — pushing this button left (to clear the
     account badge, see the 480px-split rules further down) moved it under
     the feed panel's decorative cat art. .edge-cat is pointer-events:none
     so the button stayed clickable, but was visually painted over. */
  z-index: 10;
}

.info-list {
  margin: 0;
  padding-left: 1.2rem;
  font-size: 0.85rem;
  line-height: 1.7;
}

.info-list li {
  margin-bottom: 0.9rem;
}

.info-list li:last-child {
  margin-bottom: 0;
}

.app-title {
  font-size: 1.6rem;
  margin: 0 0 1.25rem;
  flex-shrink: 0;
}

.app {
  /* align-items:stretch is what makes the map panel and feed panel
     always match heights exactly — both are sized to whichever is
     taller (in practice, #screen-app's own fixed viewport height, since
     both panels fill it via .panel-inner{height:100%}), not to their
     own content. That's a deliberate matched pair, not incidental. */
  display: flex;
  align-items: stretch;
  gap: 2rem;
  flex: 1;
  min-height: 0;
  width: 100%;
  max-width: 1400px;
}

.map-frame,
.feed-panel {
  position: relative;
}

.map-frame {
  flex: 1;
}

.feed-panel {
  width: 300px;
  flex-shrink: 0;
}

.panel-inner {
  height: 100%;
  display: flex;
  flex-direction: column;
  border: 4px solid var(--ink);
  border-radius: 28px;
  overflow: hidden;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.2);
  isolation: isolate;
}

.panel-header {
  font-family: "Press Start 2P", monospace;
  font-size: 0.9rem;
  line-height: 1.6;
  text-align: center;
  padding: 1.1rem;
  background: #fff;
  flex-shrink: 0;
}

.panel-body {
  flex: 1;
  min-height: 0;
  background: var(--panel-beige);
  position: relative;
}

#map {
  width: 100%;
  height: 100%;
}

.edge-cat {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  image-rendering: pixelated;
}

/* height:50px, not 68px — at 68px this rendered 159x68 (10,838px^2), nearly
   2x the area of the orange (83x80, 6,645px^2) and black (64x84, 5,352px^2)
   cats despite a similar CSS height, because its wide lying pose has a much
   larger width than their portrait/square poses. 50px area-matches it to
   the average of the other two (measured via getBoundingClientRect, not
   eyeballed) since matching by height alone was misleading given how
   differently these three poses distribute their pixels. */
/* Each is now a wrapper div (see index.html) instead of a bare <img>, so
   the browser can no longer derive width from the image's own intrinsic
   aspect ratio automatically — width is spelled out here to match what
   auto-width previously computed (height * each PNG's own w/h ratio). */
.edge-cat--bl { bottom: -13px; left: 0px; height: 50px; width: 117.2px; }
.edge-cat--br2 { bottom: 4px; right: 4px; height: 80px; width: 83.1px; }
.edge-cat--br3 { bottom: -10px; right: -10px; height: 84px; width: 63.7px; }

.edge-cat-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  image-rendering: pixelated;
}

/* ---------- gray cat: tail wag ---------- */
/* Both layers show the same full image; each clip-path reveals only its
   own half of a single rectangular cut (0%,0%)-(16.43%,56.4%) — the
   tail's own bounding box, found by scanning the source PNG for where
   the curled tail silhouette separates from the body. Matching cuts
   mean the two layers tile back into the whole cat with no seam at
   rest (rotate(0)); only the tail layer rotates, pivoting at
   transform-origin, which sits at the tail's own base (where the curl
   visually joins the body) rather than the image's own corner. */
.edge-cat-layer--body {
  clip-path: polygon(16.43% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 56.4%, 16.43% 56.4%);
}

.edge-cat-layer--tail {
  clip-path: polygon(0% 0%, 16.43% 0%, 16.43% 56.4%, 0% 56.4%);
  transform-origin: 14% 56%;
  animation: cs-tail-wag 3.5s ease-in-out infinite;
}

/* Was -3deg/5deg (8deg total swing) over 5s — too subtle to notice at a
   glance. Widened to -6deg/10deg (16deg swing, 2x) and sped up to 3.5s
   (30% faster) so it reads as a clear, deliberate wag without crossing
   into a fast/frantic wiggle. */
@keyframes cs-tail-wag {
  0%, 100% { transform: rotate(-6deg); }
  50% { transform: rotate(10deg); }
}

/* ---------- orange cat: blink ---------- */
/* Same technique as the black cat's blink below (fur-colored overlay,
   opacity toggled via the same cs-blink keyframes) — a separate class
   with its own position/size/color rather than sharing .cat-blink-overlay,
   so the black cat's existing rule is untouched either way. */
.cat-blink-overlay--orange {
  position: absolute;
  left: 46%;
  top: 25.37%;
  width: 36.55%;
  height: 8.53%;
  /* Sampled directly from p4-cat-orange-sitting.png at the fur
     immediately surrounding both eyes (#ef9645, consistent above, below,
     and between them) — not guessed. */
  background: #ef9645;
  opacity: 0;
  animation: cs-blink 4s ease-in-out infinite;
  /* Same 4s cycle as the black cat's blink — without an offset the two
     blink in perfect unison, which reads as robotic. A negative delay
     starts this one already partway into its cycle (rather than a
     positive delay, which would just leave both cats' eyes open a
     beat longer before anything happens on the first load) so the two
     stay permanently out of phase with each other. */
  animation-delay: -1.7s;
}

/* ---------- black cat: blink ---------- */
/* Overlay sized/positioned to the eye region only (both eyes plus the
   fur gap between them, all sampled as #444 at that row — see the
   sourcing note below), not the whole face, so nothing else visibly
   changes during the blink. */
.cat-blink-overlay {
  position: absolute;
  left: 13.21%;
  top: 20.45%;
  width: 45.01%;
  height: 10.02%;
  /* Sampled directly from p4-cat-black-standing.png at the fur
     immediately surrounding the eyes (#444444, consistent above, below,
     and between both eyes at that row) — not guessed, so the "closed"
     state blends into the surrounding fur exactly. */
  background: #444444;
  opacity: 0;
  animation: cs-blink 4s ease-in-out infinite;
}

@keyframes cs-blink {
  0%, 91%, 100% { opacity: 0; }
  92.5%, 96% { opacity: 1; }
  97.5% { opacity: 0; }
}

/* ---------- sightings feed sidebar ---------- */

.feed-header {
  font-family: "Press Start 2P", monospace;
  font-size: 0.9rem;
  line-height: 1.5;
  text-align: center;
  padding: 1.1rem 0.5rem;
  background: var(--header-lavender);
  flex-shrink: 0;
}

.feed-list {
  /* height:100% resolves correctly here because .panel-body now has a
     genuinely definite height all the way up the chain: .feed-panel is
     flex:1 with min-height:0 (mobile: inside a flex:3/flex:1 split;
     desktop: stretched to match .map-frame) -> .panel-inner{height:100%}
     -> .panel-body{flex:1; min-height:0}. The min-height:0 at each flex
     level is what makes this work — without it, a flex child defaults to
     a min-height equal to its content, so it never actually shrinks
     below however many cards exist, and this height:100%/overflow-y:auto
     never gets anything to act on. */
  height: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* Now that the mobile page itself can also scroll (feed-panel taller than
     one screen), a swipe that starts inside this list and runs past its own
     scroll limit would otherwise chain up and scroll the page too. Contain
     it so this panel's scroll always stays local to itself. */
  overscroll-behavior-y: contain;
  list-style: none;
  margin: 0;
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.feed-empty {
  font-size: 0.8rem;
  text-align: center;
  opacity: 0.6;
  margin-top: 1rem;
}

.feed-item {
  display: flex;
  align-items: center;
  text-align: left;
  gap: 0.6rem;
  background: var(--white);
  border: 3px solid var(--ink);
  box-shadow: 3px 3px 0 var(--ink);
  padding: 0.5rem;
  cursor: pointer;
  transform: rotate(var(--tilt, -1deg));
}

.feed-item:nth-child(even) {
  --tilt: 1deg;
}

.feed-item:hover {
  animation: purr-wiggle 0.28s ease-in-out infinite;
}

@keyframes purr-wiggle {
  0%, 100% { transform: rotate(var(--tilt, -1deg)); }
  25% { transform: rotate(calc(var(--tilt, -1deg) - 2deg)) translateX(-1px); }
  75% { transform: rotate(calc(var(--tilt, -1deg) + 2deg)) translateX(1px); }
}

.feed-thumb {
  width: 40px;
  height: 40px;
  object-fit: cover;
  border: 2px solid var(--ink);
  flex-shrink: 0;
  background: var(--cream);
}

.feed-thumb-fallback {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border: 2px solid var(--ink);
  background: var(--pink);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
  opacity: 0.5;
}

.feed-text {
  font-size: 0.72rem;
  line-height: 1.3;
}

.feed-text strong {
  display: block;
  font-size: 0.8rem;
}

/* ---------- pixel panel look ---------- */

.panel,
.pixel-btn {
  border: 4px solid var(--ink);
  box-shadow: 6px 6px 0 var(--ink);
  clip-path: polygon(
    0 8px, 8px 8px, 8px 0,
    calc(100% - 8px) 0, calc(100% - 8px) 8px, 100% 8px,
    100% calc(100% - 8px), calc(100% - 8px) calc(100% - 8px), calc(100% - 8px) 100%,
    8px 100%, 8px calc(100% - 8px), 0 calc(100% - 8px)
  );
}

/* ---------- modal ---------- */

.modal {
  position: fixed;
  inset: 0;
  background: rgba(58, 42, 32, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 1rem;
}

.modal.hidden {
  display: none;
}

/* .panel is the card FRAME only — it doesn't scroll itself, so its
   close button (positioned relative to it) can never be scrolled out of
   view. .panel-scroll is the inner content area that actually scrolls
   when a form/profile is taller than the card's capped height. Splitting
   these is what lets the close button stay both reachable AND visually
   anchored inside the card, instead of needing position:fixed-to-viewport
   (which read as floating disconnected from the card) or position:absolute
   inside a scrolling box (which scrolled away with the content). */
.panel {
  position: relative;
  display: flex;
  flex-direction: column;
  background-color: var(--white);
  background-image: radial-gradient(rgba(52, 40, 30, 0.06) 1px, transparent 1px);
  background-size: 14px 14px;
  max-width: 420px;
  width: 100%;
  max-height: 85vh;
  max-height: 85dvh;
  overflow: hidden;
}

.panel-scroll {
  flex: 1 1 auto;
  min-height: 0;
  padding: 1.75rem;
  overflow-y: auto;
}

.panel-title {
  font-size: 1rem;
  margin: 0 0 1.25rem;
  color: var(--terracotta);
}

.close-btn {
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--white);
  border: 3px solid var(--ink);
  border-radius: 50%;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
  font-size: 1.1rem;
  cursor: pointer;
  color: var(--ink);
  z-index: 2;
}

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

form label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  margin-bottom: 1rem;
}

form input,
form select {
  display: block;
  width: 100%;
  min-height: 44px;
  margin-top: 0.4rem;
  padding: 0.6rem;
  font-size: 0.95rem;
  border: 3px solid var(--ink);
  background: var(--cream);
  font-family: inherit;
}

/* "Your name" fields, locked to the signed-in account's display name
   (see applyAccountNameField in app.js) — greyed and blocked from
   editing/focus-ring so it reads as "this is fixed," not just pre-filled.
   #e6dfd2 is --cream (#f6ecd9) darkened/desaturated a touch, not a new
   arbitrary color, so it still reads as the same warm palette. */
form input[readonly] {
  background: #e6dfd2;
  color: var(--brown);
  cursor: not-allowed;
}

.vibe-other-input.hidden {
  display: none;
}

.pixel-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  background: var(--sage);
  color: var(--ink);
  font-family: "Press Start 2P", monospace;
  font-size: 0.75rem;
  padding: 0.9rem 1rem;
  cursor: pointer;
  margin-top: 0.5rem;
}

.pixel-btn:active {
  box-shadow: 2px 2px 0 var(--ink);
  transform: translate(4px, 4px);
}

.pixel-btn.small {
  min-height: 44px;
  font-size: 0.65rem;
  padding: 0.6rem 0.8rem;
  background: var(--header-lavender);
  color: var(--ink);
}

.add-name-section input,
.add-sighting-section input {
  margin-bottom: 0.6rem;
}

hr {
  border: none;
  border-top: 3px dashed var(--brown);
  margin: 1.5rem 0;
}

/* ---------- cat profile content ---------- */

.cat-photo-frame {
  position: relative;
  margin-bottom: 1rem;
}

.cat-photo {
  width: 100%;
  max-height: 220px;
  object-fit: cover;
  border: 4px solid var(--ink);
  display: block;
}


.cat-names {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  margin: 1rem 0;
  text-align: center;
}

.cat-name-heading {
  font-family: "Press Start 2P", monospace;
  font-size: 1rem;
  line-height: 1.4;
}

.cat-name-heading--primary {
  font-size: 1.3rem;
}

.cat-name-credit {
  font-size: 0.75rem;
  margin-top: 0.25rem;
  opacity: 0.75;
}

.credit-underline {
  text-decoration: underline;
}

.discovered-by {
  display: block;
  text-align: center;
  font-size: 0.8rem;
  font-style: italic;
  color: var(--ink);
  opacity: 0.75;
  margin: 0.4rem 0 0.75rem;
}

.vibe-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: var(--periwinkle);
  color: var(--ink);
  font-size: 0.8rem;
  padding: 0.25rem 0.8rem;
  border-radius: 999px;
  border: 2px solid var(--ink);
  margin-bottom: 0.75rem;
}

.vibe-icon {
  flex-shrink: 0;
}

.sighting-entry {
  text-align: center;
  font-size: 0.85rem;
  padding: 0.6rem 0;
  border-bottom: 2px dotted var(--brown);
}

.sighting-entry:last-child {
  border-bottom: none;
}

.sighting-quote {
  margin: 0 0 0.3rem;
  font-style: italic;
}

.sighting-credit {
  margin: 0;
  font-size: 0.75rem;
  opacity: 0.7;
}

.sighting-empty {
  font-style: normal;
}

/* ---------- custom leaflet pin ---------- */

.pixel-pin {
  width: 30px;
  height: 30px;
  background: var(--terracotta);
  border: 3px solid var(--ink);
  border-radius: 50%;
  box-shadow: 2px 2px 0 var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--white);
}

/* ---------- cluster badges ---------- */

.pixel-cluster {
  width: 36px;
  height: 36px;
  background: var(--berry);
  border: 3px solid var(--ink);
  border-radius: 50%;
  box-shadow: 2px 2px 0 var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
}

.pixel-cluster span {
  font-family: "Press Start 2P", monospace;
  font-size: 0.7rem;
  color: var(--white);
}

/* ---------- leaflet control tap targets ---------- */
/* Leaflet's default zoom/popup-close buttons are ~26-30px, under the
   44px minimum touch target — overridden here for all pointer types. */

.leaflet-control-zoom a {
  width: 44px !important;
  height: 44px !important;
  line-height: 44px !important;
  font-size: 20px !important;
}

.leaflet-popup-close-button {
  width: 36px !important;
  height: 36px !important;
  font-size: 20px !important;
  display: flex !important;
  align-items: center;
  justify-content: center;
}

/* ---------- map pin popup (quick preview) ---------- */

.leaflet-popup-content-wrapper {
  background: var(--white);
  border: 3px solid var(--ink);
  border-radius: 14px;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.2);
  max-width: min(220px, calc(100vw - 3rem));
}

.leaflet-popup-tip {
  background: var(--white);
  border: 3px solid var(--ink);
}

.leaflet-popup-content {
  margin: 10px 12px;
  text-align: center;
}

.pin-popup-name {
  display: block;
  padding: 0.4rem 0;
  font-family: "Press Start 2P", monospace;
  font-size: 0.78rem;
  color: var(--ink);
  text-decoration: underline;
  cursor: pointer;
}

.pin-popup-photo {
  width: 100%;
  aspect-ratio: 4 / 3;
  background: var(--panel-beige);
  border: 2px solid var(--ink);
  border-radius: 10px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.pin-popup-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pin-popup-photo-fallback {
  color: var(--ink);
  opacity: 0.35;
}

/* ==================================================================
   Mobile layout — stack map/sidebar, shrink chrome, keep everything
   reachable within the viewport instead of side-by-side desktop panels.
   ================================================================== */

@media (max-width: 768px) {
  /* Mobile only — desktop stays exactly as it is. The page can now
     scroll again: .map-frame is a fixed 75% of .app's own height
     (unchanged from before — see .map-frame below) and .feed-panel is
     tied to exactly half of THAT SAME value via --map-height, not to a
     flex-grow share of whatever's left. Those are two different things:
     flex:3/flex:1 on siblings means each one's size depends on the
     other (change either and both shift, which is exactly what went
     wrong the last time this was adjusted) — flex:0 0 auto with an
     explicit height on each means neither can affect the other at all,
     because neither is "growing" into shared leftover space anymore.
     map(75%) + feed(37.5%) = 112.5% of .app's own bounded height, so
     the combined content now genuinely overflows one screen — html/body
     allows scrolling below to reveal the rest, same as .app's own
     overflow:visible default (unset, not overridden here). */
  /* html and body are handled separately here (not "html, body {}" together)
     because the actual page-scroll behavior depends on which one holds the
     overflow. Per the CSS overflow-propagation rule, when html's own
     overflow is visible (its default — left alone below) and body's is
     something else, the UA applies BODY's overflow to the viewport itself.
     Setting overflow-y: auto on BOTH (as before) defeats this: each becomes
     its own independent scroll box, body scrolls internally but nothing
     tells the actual viewport/window to scroll, so window.scrollTo /
     touch-scrolling the page did nothing even though body.scrollHeight
     correctly showed the extra content was there. */
  html {
    /* height: auto overrides the base rule's height: 100vh/100dvh — needed
       so html isn't clamped and can size to whatever the propagated
       overflow requires; min-height is the floor for shorter content. */
    height: auto;
    min-height: 100vh;
    min-height: 100dvh;
    overflow: visible;
  }

  body {
    height: auto;
    min-height: 100vh;
    min-height: 100dvh;
    overflow-x: hidden;
    overflow-y: auto;
  }

  #screen-app {
    /* Fixed (not min-height): .app's flex:1 share must be computed against
       a value that can't itself change size in response to its own
       children overflowing. min-height here would let #screen-app grow to
       fit .map-frame + .feed-panel once they exceed one screen (now that
       they're sized independently instead of sharing a flex ratio), which
       in turn changes what "100% of #screen-app" means for .app, which in
       turn nudges .map-frame's own height — a circular dependency that was
       the actual cause of .map-frame drifting when this was min-height.
       Keeping this fixed at 100dvh means .app's available height never
       moves, so .map-frame's height literally cannot change; the overflow
       from .feed-panel being taller now just spills past #screen-app's own
       box (overflow: visible, unset/default) and is what html/body's
       overflow-y: auto below scrolls to reveal. */
    height: 100vh;
    height: 100dvh;
    /* Extra top padding vs. the 0.75rem elsewhere: .info-btn/the account
       badge now sit at a constant top:15px + 40px tall (bottom:55px) at
       every width, see .info-btn and the account-widget-clearance block
       below — needs enough clearance above the map panel that it doesn't
       reclip into its top border like it did before the badge was added. */
    padding: 1.5rem 0.85rem 0.85rem;
  }

  .app-title {
    font-size: 1.15rem;
    margin: 0 0 0.9rem;
  }

  .app {
    flex-direction: column;
    gap: 0.85rem;
    flex: 1;
    min-height: 0;
    /* NOT a plain 75%. Under the old flex:3/flex:1 split, the browser
       subtracts `gap` from .app's height BEFORE dividing 3:1 — so
       map = (appHeight - gap) * 3/4. A raw `height: 75%` on .map-frame
       is 75% of the FULL appHeight instead, ignoring the gap subtraction
       entirely — which is exactly why .map-frame grew by a few px when
       this first switched from flex-grow to a flat percentage. Redoing
       the same (appHeight - gap) subtraction here reproduces the old
       flex:3 result exactly, pixel for pixel. */
    --map-height: calc((100% - 0.85rem) * 0.75);
  }

  .map-frame {
    flex: 0 0 auto;
    height: var(--map-height);
    min-height: 0;
    width: 100%;
  }

  .feed-panel {
    flex: 0 0 auto;
    height: calc(var(--map-height) / 2);
    min-height: 0;
    width: 100%;
  }

  .panel-inner {
    border-radius: 18px;
  }

  .panel-header,
  .feed-header {
    font-size: 0.75rem;
    padding: 0.8rem;
  }

  /* Decorative corner cats: scale down so they don't crowd a narrow
     viewport. Down to 3 total now (one per corner across both panels),
     so there's no longer a doubled-up pair to hide on mobile. */
  .edge-cat {
    transform: scale(0.65);
    transform-origin: top left;
  }

  .edge-cat--bl { bottom: -8px; left: -8px; transform-origin: bottom left; }
  .edge-cat--br2 { bottom: -4px; right: -8px; transform-origin: bottom right; }
  .edge-cat--br3 { bottom: -4px; right: -8px; transform-origin: bottom right; }

  .panel {
    max-height: 80vh;
    max-height: 80dvh;
  }

  .panel-scroll {
    padding: 1.25rem;
    padding-bottom: max(1.25rem, env(safe-area-inset-bottom, 0px));
  }

  /* Nudge up the smallest information-dense text for mobile legibility;
     large pixel-font headings/buttons are left as-is by design. */
  .feed-text,
  .sighting-quote,
  .sighting-credit,
  .cat-name-credit,
  .info-list,
  .discovered-by {
    font-size: 0.85rem;
  }
}

@media (max-width: 420px) {
  .app-title {
    font-size: 0.95rem;
  }

  .cat-name-heading--primary {
    font-size: 1.1rem;
  }
}

/* ==================================================================
   Account widget clearance — shared/account-widget.js mounts a fixed
   top-right badge (see index.html) at a hardcoded top:15px with a
   horizontal inset of 10px under 480px viewport width, 32px at/above it
   (confirmed by reading the widget's own source — those two values
   aren't exposed through its init() options for mode:'fixed', only
   anchor/size are). .info-btn sits in the same corner, so its own right
   offset is pushed out past wherever the badge's left edge lands at
   whichever inset is active, with a small gap — split at the widget's
   own 480px breakpoint, not this file's 768px one, since those are two
   different things the badge switches on. top/width/height match the
   badge exactly at every width (see the base .info-btn rule) — only the
   horizontal offset needs to vary by breakpoint here.
   ================================================================== */
@media (min-width: 480px) {
  .info-btn {
    right: 82px;
  }
}

@media (max-width: 479px) {
  .info-btn {
    right: 58px;
  }
}

