/* =========================================
   COZY PET CORNER, styles
   Warm and cozy look, soft rounded shapes.
   ========================================= */

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body { height: 100%; overflow: hidden; }

body {
  font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
  background: #e6d5b8;
  color: #5b4636;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  /* **A LONG PRESS IS A GESTURE IN THIS GAME, SO THE BROWSER MUST NOT CLAIM
     IT.** Flavia, Aug 21, on a tablet: holding a finger down to collect a pet
     opened the tablet's own share and print menu instead.

     Nothing was wrong with the tablet. A long press on a web page is how you
     open the browser's menu, and the game uses that same press to put a
     decoration away and to stroke a pet. `user-select: none` above stops text
     being selected and does NOT stop the callout, which is a separate thing and
     the one that was firing. */
  -webkit-touch-callout: none;
}

/* **AND AN IMAGE IS THE WORST OFFENDER.** Every pet, toy and decoration is an
   `<img>`, and holding one on Android offers to download it, share it or search
   for it. That is the menu she saw. Dragging one out of the page is refused for
   the same reason: a decoration being dragged is our gesture, not the
   browser's. */
img {
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
  user-drag: none;
  pointer-events: none;
}
/* except where an image IS the control, which the game sets deliberately */
.wanderer img, .menu-cell img, .decor-card img, button img, .pet-card img {
  pointer-events: auto;
}

/* ---------- the stage IS the screen ----------
   #yard keeps the 4 by 3 shape the rooms are drawn in and is scaled to
   cover the viewport, so slot percentages always land on the same painted
   spot no matter the screen. --u is that scale, set in game.js, and every
   sprite size is written in design pixels times --u. */
#stage {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background: #e6d5b8;
}

/* the room again, blown up and blurred, so the edges of the screen feel like
   part of the world instead of empty letterbox bars */
#stage::before {
  content: "";
  position: absolute;
  inset: -6%;
  background-image: inherit;
  background-size: cover;
  background-position: center;
  filter: blur(38px) saturate(.85) brightness(1.03);
  opacity: .9;
}

/* the whole room always fits on screen, so no spot can ever be cut off
   and out of reach. It keeps the 4 by 3 shape the rooms are drawn in, and
   --u is how much bigger it is than the 800px design width. */

#yard {
  --u: 1;
  /* **THE ROOM DECIDES ITS OWN SHAPE, not the stylesheet.** The new front
     facing backdrops are 16:9 where the old isometric ones are 4:3, and the
     yard's shape is a single global rule, so hard coding either one would
     letterbox or stretch every room in the other style. With the ratio as a
     variable the two can live side by side and the rooms can move over one at
     a time, which is the whole point of looking at one on a phone first.
     game.js sets --ar-w / --ar-h from the room being drawn. */
  --ar-w: 4;
  --ar-h: 3;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* **COVER, NOT CONTAIN.** Flavia, Aug 12: *"if the places are rendered wider
     it is because we can cut it easier and better to be full screen on
     mobile."* The rooms are being repainted at 21:9 precisely so a phone can
     CROP them rather than letterbox them. min() fits the room inside the screen
     and leaves bars; max() fills the screen and trims the spare width.

     The rule that made contain right still holds and is why this is safe: no
     spot may ever be cut off. All seven sit between 16% and 80% of the room,
     and the worst case is a 16:9 tablet showing a 21:9 room, which trims 11.9%
     from each side. The outermost spots then land at 5% and 89%, still on
     screen. Measured, not assumed. */
  /* **STOP GUESSING WHICH VIEWPORT UNIT IS THE VISIBLE ONE AND MEASURE IT.**
     This line has now been wrong twice in one day, in both directions, which is
     the sign that the unit was never the right tool.

     `svh` is the height with the browser's address bar SHOWING, `lvh` with it
     hidden. I moved it to `lvh` reasoning that a retracting bar would uncover
     the bottom. That reasoning does not hold here, because `html, body` are
     `overflow: hidden`, so this page can never scroll and Chrome's bar therefore
     never retracts. The visible height is always the small one, and sizing to
     the large one made the room TALLER than the screen and cropped the floor
     where the pets stand.

     So `--vh` is written by fitYard from `#stage.clientHeight`, which is the
     real visible box, measured, whatever any bar is doing and whether this is a
     browser or the installed app. `100svh` stays only as the value used for the
     very first paint, before any script has run. */
  width: max(100vw, calc(var(--roomh, 100svh) * var(--ar-w) / var(--ar-h)));
  height: max(var(--roomh, 100svh), calc(100vw * var(--ar-h) / var(--ar-w)));
  background-size: 100% 100%;
  background-repeat: no-repeat;
  box-shadow: 0 0 60px rgba(70, 48, 22, .35);
  /* soft placeholder floor until the room PNGs land in imagini/ */
  background-color: #efe3cd;
  background-image: linear-gradient(180deg, #f6ecda 0%, #efe3cd 55%, #e6d5b8 100%);
}

/* ---------- the paw, the only button on the yard ---------- */
#paw {
  position: fixed;
  top: max(12px, env(safe-area-inset-top));
  left: max(12px, env(safe-area-inset-left));
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 2px solid #cbb08a;
  background: #fffaf0;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  z-index: 20;
  box-shadow: 0 4px 12px rgba(80, 55, 25, .28);
  transition: transform .15s ease;
}
#paw:hover  { transform: scale(1.07); }
#paw:active { transform: scale(.94); }

/* the room tone, tucked under the paw. Off is quiet and faded, on is warm. */
/* **44 AND NOT 40.** Swept every tap target on every screen at a real 667x375
   landscape viewport before the store work: this note was the ONLY thing in the
   whole game under the 44px minimum, on all three screens it appears on. Four
   pixels, and it is the difference between a child hitting it and a child
   hitting the room behind it. See MOBILE_FIRST_RULES.md. */
#music {
  position: fixed;
  top: calc(max(12px, env(safe-area-inset-top)) + 60px);
  left: max(12px, env(safe-area-inset-left));
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 2px solid #cbb08a;
  background: #fffaf0;
  color: #7a5c3d;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  z-index: 20;
  box-shadow: 0 3px 9px rgba(80, 55, 25, .24);
  transition: transform .15s ease, opacity .2s;
}
#music:hover  { transform: scale(1.08); }
#music:active { transform: scale(.94); }
#music.off    { opacity: .45; }
#music.hidden { display: none; }

/* ---------- wallet, drawn into the yard ---------- */
/* bottom right, clear of the bowl which sits centre bottom in the modern room */
#wallet {
  position: fixed;
  /* **THE PAW COUNTERS MOVED OFF THE FLOOR.** Flavia, Aug 12: *"we should move
     the silver and gold paws top right corner near the camera so it doesnt cover
     the pets or food or anything"*, and then *"on top of the camera"*.

     They sat bottom right, which is exactly where the front row of the room is:
     the toy row at 96% of the room height and the second food bowl at 91% both
     land under them. On a phone that is the busiest corner of the picture and
     the counters were parked on it.

     Up here the four pieces of screen furniture mirror each other, paw then
     music down the left, paws then camera down the right, and the whole floor is
     clear. The 60px step is the same one the music button uses, so the two
     columns line up. */
  top: max(12px, env(safe-area-inset-top));
  right: max(12px, env(safe-area-inset-right));
  display: flex;
  gap: 8px;
  z-index: 20;
}

.coin {
  padding: 6px 14px;
  border-radius: 999px;
  font-weight: 700;
  font-size: 15px;
  min-height: 40px;
  display: flex;
  align-items: center;
  gap: 6px;
  box-shadow: 0 3px 10px rgba(80, 55, 25, .25);
}
.coin.silver { background: #eef2f7; color: #46586a; border: 2px solid #c8d2dd; }
.coin.gold   { background: #fdf0c7; color: #8a6d1a; border: 2px solid #ecd490; }

/* ---------- room buttons, now inside Settings ---------- */
#room-buttons { display: flex; gap: 10px; margin-bottom: 14px; }

#room-buttons button {
  flex: 1;
  min-height: 44px;
  padding: 10px 14px;
  border-radius: 14px;
  border: 2px solid #d8b98f;
  background: #fff7ea;
  font-size: 15px;
  font-weight: 600;
  color: #7a5c3d;
  cursor: pointer;
  transition: transform .15s ease, background .2s;
}
#room-buttons button:hover { transform: translateY(-2px); }
#room-buttons button.active {
  background: #ffdf9e;
  border-color: #e0a94f;
  color: #5f430f;
}

/* **THE SPOT IS THE GROUND, NOT THE MIDDLE OF A BOX.**
   Flavia, Aug 10: "it must be stitched to the ground not flying", and some
   toys were sitting on the very bottom edge of the room.

   The box is 120 design pixels tall and was centred on its position, so the
   feet landed HALF A BOX BELOW the coordinate: measured, 13.3 points of the
   yard in a 16:9 room and 10.0 in a 4:3 one. Every position in rooms.js was
   checked against the point, and every toy stood well below it, which is why
   the bottom row ran off the floor in the wide room and why the drop got worse
   the moment the room got wider.

   Anchoring the BOTTOM of the box to the position makes the coordinate mean
   what everyone always assumed it meant: this is where the feet go. Nothing in
   the grounding table changes, because that was always about where the drawing
   sits inside its own box. */
/* ---------- slots ---------- */
.slot {
  position: absolute;
  width: calc(120px * var(--u));
  height: calc(120px * var(--u));
  transform: translate(-50%, -100%);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  cursor: pointer;
}

/* NO SPOT IS EVER TAPPABLE AS A SQUARE. Every spot is a 120px box, far
   bigger than what is drawn in it, and those invisible squares overlap each
   other. Whichever came later in the list swallowed the tap, so a toy in the
   middle spot stole taps meant for a pet on the Bunk Tent above it. Only the
   drawn things listen now: the glowing circle of an empty spot, and the hit
   pad that sits exactly on each picture. */
.slot { pointer-events: none; }
.slot.empty { align-items: center; }
.slot.empty .plus { pointer-events: auto; }

/* depth comes from the floor position, set per spot in game.js */

.slot.empty .plus {
  width: calc(56px * var(--u));
  height: calc(56px * var(--u));
  border-radius: 50%;
  border: 3px dashed rgba(255, 255, 255, .9);
  background: rgba(255, 236, 180, .3);
  color: #fff;
  font-size: calc(30px * var(--u));
  line-height: calc(50px * var(--u));
  text-align: center;
  transition: transform .15s;
  /* free spots glow warmly, the affordance from the original */
  box-shadow: 0 0 calc(18px * var(--u)) rgba(255, 216, 130, .85);
  animation: spot-glow 2.2s ease-in-out infinite;
}
.slot.empty:hover .plus { transform: scale(1.12); }

@keyframes spot-glow {
  0%, 100% { box-shadow: 0 0 calc(14px * var(--u)) rgba(255, 216, 130, .55); }
  50%      { box-shadow: 0 0 calc(26px * var(--u)) rgba(255, 216, 130, .95); }
}
.slot.empty:hover .plus { transform: scale(1.12); }

/* ---------- sprites (goodies and animals) ----------
   All art is loaded from the imagini/ folder. Goodies sit in
   the slot, animals layer on top of the goodie they visit.
   Until a PNG exists, the emoji backup shows in its place. */
.sprite {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.sprite img { display: block; }
.sprite .backup { display: none; }
.sprite.no-image img { display: none; }
.sprite.no-image .backup { display: block; }

/* the art always paints above its own ground shadow */
.sprite img, .sprite .backup { position: relative; z-index: 1; }

/* Soft oval shadow that anchors every toy and animal to the floor.
   --shadow-w is set per sprite in game.js from the real drawing width.

   **THE SHADOW IS DRAWN HERE, NOT IN THE ART.** Flavia, Aug 12: *"the shadows
   under the things in the game are from the creatives we made in higgs or is a
   coded shadow? i would remove it so it doesnt give the object the impression of
   flying over the floor."* Checked the art: decor_crate_open.png and
   yarn_ball.png both have none. It is this rule.

   And she is right about the effect, but the cause is one number rather than the
   shadow. `bottom: -0.12` of the width puts the oval BELOW the base rather than
   under it, and a detached shadow is the classic way to make something read as
   hovering. A shadow is also what glues a cut-out sprite to a painted floor, so
   deleting it can make things float MORE.

   **SHE LOOKED AT ALL THREE AND CHOSE NONE.** Aug 12: *"can you remove the
   shadow even for pets? seems they are flying instead of being stitched to the
   floor. better without shadow under it."* So the default is now no shadow at
   all, for animals as well as toys, and the rule below only paints when asked.

   My worry that removing it would make things float MORE was wrong, and it is
   worth saying why rather than quietly dropping it. These rooms are painted flat
   and front on, with the floor running right to the bottom edge. There is no
   perspective for a shadow to agree with, so an oval on the floor was inventing
   a light source the picture does not have, and a shape that disagrees with its
   background reads as separate from it. The sprites are already anchored by
   standing on their own base, bottom centre, which is what `translate(-50%,
   -100%)` has always done.

   Still switchable, so this can be compared again without an edit:
       ?shadow=on       the original, sitting 12% below the base
       ?shadow=tight    tucked under the base, touching
       (nothing)        none, which is what she chose */
/* Only ever painted when the address bar asks for it. */
body.shadow-on .sprite::after,
body.shadow-tight .sprite::after {
  content: "";
  position: absolute;
  left: 50%;
  /* straddles the floor line, so half of it shows below the feet */
  bottom: calc(var(--shadow-w, 60px) * -0.12);
  width: var(--shadow-w, 60px);
  height: calc(var(--shadow-w, 60px) * 0.24);
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(72, 48, 24, .34), rgba(72, 48, 24, 0) 70%);
  z-index: 0;
}

/* the toy sits behind its visitor so the animal's face is never covered */
.sprite.goodie { z-index: 1; }
/* when someone is visiting, the toy shifts left so it still reads
   next to the animal instead of disappearing behind it */
.slot:has(.sprite.animal) .sprite.goodie { transform: translateX(calc(-16px * var(--u))); }
.sprite.goodie img { width: calc(76px * var(--u)); height: calc(76px * var(--u)); object-fit: contain; }
.sprite.goodie .backup { font-size: calc(52px * var(--u)); }

/* the visitor stands on the same floor line as its toy, a little to the
   front, so the toy still reads behind it */
.sprite.animal {
  position: absolute;
  bottom: 0;              /* same floor line as its toy */
  z-index: 2;
  transform: translateX(calc(15px * var(--u)));
}
.sprite.animal img { width: calc(84px * var(--u)); height: calc(84px * var(--u)); object-fit: contain; }
.sprite.animal .backup { font-size: calc(58px * var(--u)); }

/* ---------- big toys ----------
   a Bunk Tent covers two spots, so it is drawn wider and its two guests sit
   side by side on it. The spot it swallows draws nothing and is not tappable. */
.slot.spanned { pointer-events: none; }

.sprite.goodie.big img {
  width: calc(150px * var(--u));
  height: calc(150px * var(--u));
}
.slot:has(.sprite.animal) .sprite.goodie.big { transform: translateX(0); }

.sprite.animal.seat-left  { transform: translateX(calc(-30px * var(--u))); }
.sprite.animal.seat-right { transform: translateX(calc(34px * var(--u))); }
.sprite.animal.seat-right { z-index: 3; }

/* the tap target follows the PICTURE, not the box around it */
.sprite.tappable { cursor: pointer; }
.sprite .hit { position: absolute; left: 0; right: 0; z-index: 2; pointer-events: none; }
.sprite.tappable .hit { pointer-events: auto; }


/* a play pose already contains its toy, so it sits centred on the spot
   rather than nudged to one side, and it is drawn a little larger */
.sprite.play { transform: translateX(0); }
.sprite.play img { width: calc(104px * var(--u)); height: calc(104px * var(--u)); }
.sprite.play .backup { font-size: calc(66px * var(--u)); }

/* ---------- play scenes: stillness with rare bursts ----------
   A perpetual sway reads as flying, not as a pet. So a scene sits perfectly
   still, breathing only, and every few seconds plays ONE short burst of two
   to four quick little ticks, under a second, then goes quiet again. The gap
   is randomised per sprite in game.js so the yard never ticks in unison.

   The burst rides on .rig, the breathing stays on the picture, and the
   ground shadow lives on the sprite itself so it never moves. Both hinge on
   the drawn FEET, set inline in game.js, never on the image box bottom. */
.sprite .rig { position: relative; display: block; }

/* a LIVING scene is an animated file: it carries its own life, so no CSS
   movement is layered on top, not even the breathing */
.sprite.play.living .rig,
.sprite.play.living img { animation: none; }

.sprite.play.mood-rock .rig { animation: burst-bat 6s steps(1, end) infinite; animation-timing-function: ease-in-out; }
.sprite.play.mood-eat  .rig { animation: burst-eat 6s ease-in-out infinite; }
.sprite.play.mood-sway .rig { animation: burst-settle 6s ease-in-out infinite; }
/* mood-sleep gets no burst at all, only the breathing it inherits */

/* a few quick paw bats, then still for the rest of the cycle */
@keyframes burst-bat {
  0%    { transform: rotate(0deg); }
  1.6%  { transform: rotate(-1.8deg); }
  3.2%  { transform: rotate(1.4deg); }
  4.8%  { transform: rotate(-0.9deg); }
  6.4%  { transform: rotate(0deg); }
  100%  { transform: rotate(0deg); }
}

/* a couple of head dips into the bowl. A squash, so the feet stay put */
@keyframes burst-eat {
  0%    { transform: scaleY(1); }
  1.8%  { transform: scaleY(0.972); }
  3.6%  { transform: scaleY(1); }
  5.4%  { transform: scaleY(0.978); }
  7%    { transform: scaleY(1); }
  100%  { transform: scaleY(1); }
}

/* a tiny settle, the smallest of the three */
@keyframes burst-settle {
  0%    { transform: rotate(0deg); }
  2.2%  { transform: rotate(-0.9deg); }
  4.4%  { transform: rotate(0.6deg); }
  6%    { transform: rotate(0deg); }
  100%  { transform: rotate(0deg); }
}

/* breathing, not floating: the belly lifts about 2px while the feet
   stay planted, and the shadow underneath never moves */
.sprite.animal img,
.sprite.animal .backup {
  transform-origin: bottom center;
  animation: breathe 3.6s ease-in-out infinite;
}

/* rare visitors get a golden glow */
.sprite.animal.rare img,
.sprite.animal.rare .backup { filter: drop-shadow(0 0 10px rgba(255, 210, 80, .95)); }

/* ---------- the food bowl ----------
   always out, in its own spot, never one of the four toy slots */
#bowl, #bowl2 {
  position: absolute;
  /* **THE TWO BOWLS WERE THE ONLY THINGS IN THE YARD WITHOUT A DEPTH.** Flavia,
     Aug 14, with two photographs of a room where the kibble had vanished: *"the
     kibbles get covered."* Everything else has a number, the toy spots at 90 and
     the resident at 106, and the decoration layers sit at 2 and 3. The bowls had
     `auto`, and an element with `auto` paints BELOW anything carrying a number,
     so every decoration she placed over that patch of floor covered her food.

     The bowl is not scenery. It is the thing she taps to feed, and a decoration
     hiding it hides a control, not a picture. Same depth as the toy spots, which
     is what it always should have had. */
  z-index: 90;
  transform: translate(-50%, -100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  transition: transform .15s ease;
}
/* **THE HOVER MUST NOT MOVE THE BOWL, ONLY GROW IT.** This still said
   translate(-50%, -50%) after the anchor changed to -100%, so pointing at a
   bowl yanked it half a box downwards and let go when the pointer left.
   Flavia saw it immediately. It also has to grow from the FEET, or a bowl
   swells through the floor. */
#bowl:hover, #bowl2:hover { transform: translate(-50%, -100%) scale(1.06); }
#bowl, #bowl2 { transform-origin: 50% 100%; }
#bowl2.hidden, .slot.hidden { display: none; }

/* **AND THE BOWL LABEL WENT WITH THE NAMES, Aug 20.** Flavia: *"remove the bowl
   labels too and publish."*

   Checked before removing it, because unlike a pet's name this one carried
   information: it said which food was out. It is not lost. Tapping the bowl
   opens a panel headed "What is in the bowl" which marks the current food "In
   the bowl", so the answer is one tap away and stated more clearly than a pill
   floating over a dish ever said it.

   Same treatment as .name-tag: hidden, not deleted, so the text is still built
   and one line brings it back. */
.bowl-tag { display: none; }

.bowl-tag--never-shown {
  margin-top: 8px;
  background: rgba(255, 255, 255, .92);
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 11px;
  font-weight: 700;
  color: #6b4d2e;
  white-space: nowrap;
  box-shadow: 0 2px 5px rgba(0, 0, 0, .18);
  /* **THE LABEL MUST NOT PUSH THE BOWL OFF THE GROUND.** The bowl's box is a
     column holding the picture and this caption, so with the box anchored by
     its bottom the caption sat on the floor line and the bowl floated 2.8
     points above it. Out of flow, so the box is the picture and nothing else. */
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
}

/* **THE NAMES CAME OFF THE PETS' HEADS, Aug 20.** Chat first, looking at the ad
   images: the labels read as clutter at small size. Flavia then went further:
   *"better to leave the game without labels too."*

   She is right, and the reason it is safe is that a name was never only here. A
   pet's name arrives in the message when it turns up, in the welcome panel when
   you come back, in the PETalog, and in every line a wandering cat says. Taking
   the tag off the head removes a repetition, not the name.

   What it buys is the thing the whole game is for: a room you look at. Seven
   pets in a room meant seven pale pills floating over the furniture.

   Kept in the stylesheet rather than deleted from the code, because the tags are
   still built and still carry the name for anything that needs to read it, and
   because bringing them back is one line rather than a search through five
   files. */
.name-tag { display: none; }

.name-tag--never-shown {
  position: absolute;
  top: -18px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(255, 255, 255, .92);
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 12px;
  font-weight: 700;
  color: #6b4d2e;
  white-space: nowrap;
  box-shadow: 0 2px 5px rgba(0, 0, 0, .18);
}
.sprite.animal.rare .name-tag { background: #ffe9a8; }

@keyframes breathe {
  0%, 100% { transform: scaleY(1); }
  50%      { transform: scaleY(1.025); }
}

@media (prefers-reduced-motion: reduce) {
  .sprite.animal img, .sprite.animal .backup,
  .sprite.play.mood-rock .rig,
  .sprite.play.mood-eat .rig,
  .sprite.play.mood-sway .rig { animation: none; }
}

/* ---------- hint and reset ---------- */
#hint {
  margin-top: 12px;
  font-size: 14px;
  opacity: .8;
  text-align: center;
  max-width: 640px;
}

#reset-btn {
  margin-top: 8px;
  min-height: 44px;
  padding: 8px 16px;
  border-radius: 12px;
  border: 2px solid #d9c1a3;
  background: #fbf1e2;
  color: #8a6a45;
  font-size: 13px;
  cursor: pointer;
}

/* ---------- modals (shop, away report) ---------- */
.modal {
  position: fixed;
  inset: 0;
  background: rgba(60, 40, 20, .45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
}
.modal.hidden { display: none; }

/* **THE PADDING SHRINKS WITH THE SCREEN.** Flavia, Aug 11: "dont make too much
   empty space in the boxes". 22px all round is generous on a laptop and is a
   fifth of a landscape phone's height spent on nothing. It follows the short
   side, so a phone gets 12 and a desktop keeps its 22. */
.modal-box {
  background: #fffaf1;
  border-radius: 18px;
  padding: clamp(12px, 3.4vmin, 22px);
  width: min(480px, 92vw);
  max-height: 94vh;
  overflow: auto;
  box-shadow: 0 18px 40px rgba(0, 0, 0, .3);
}
.modal-box h2 { margin-bottom: clamp(4px, 1.6vmin, 12px); font-size: clamp(17px, 4.6vmin, 22px); }

/* **THE COME BACK SCREEN PINS ITS BUTTON.** With three rows of friends the
   panel outgrew a landscape phone and the "Nice!" button fell off the bottom,
   where a child cannot reach it and there is nothing to tell her it is there.
   The box is a column now: the headline and the button always visible, and
   only the middle scrolls if the night was busy. */
#report .modal-box {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
#report #report-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
#report #report-close { flex: 0 0 auto; margin-top: clamp(6px, 2vmin, 12px); }
#report #report-when { margin-bottom: clamp(4px, 1.6vmin, 10px); }

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 6px;
  border-bottom: 1px solid #eadfcd;
}
.row span { font-size: 15px; }

/* gold priced things get a warm tint, and anything you already own
   wears a little red stamp, both straight from the original's shop */
.row { position: relative; }
.row.gold-row { background: #fffaeb; border-radius: 12px; }

/* out of reach, not locked: it keeps its picture and its price, and drifts
   a soft light across itself so it reads as something to want */
.row.someday { position: relative; overflow: hidden; border-radius: 12px; }
.row.someday .thumb, .row.someday .row-words { opacity: .72; }
.row.someday::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 35%, rgba(255, 246, 214, .85) 50%, transparent 65%);
  transform: translateX(-100%);
  animation: someday-shimmer 3.4s ease-in-out infinite;
  pointer-events: none;
}
.row.someday button {
  border-color: #d7c49a;
  background: #fdf6e6;
  color: #9a8264;
}

@keyframes someday-shimmer {
  0%   { transform: translateX(-100%); }
  55%  { transform: translateX(100%); }
  100% { transform: translateX(100%); }
}

@media (prefers-reduced-motion: reduce) {
  .row.someday::before { animation: none; opacity: .35; }
}

/* badges sit beside the name on its own line, never over the words */
.row-title { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }

.bought-stamp {
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #c0564d;
  border: 2px solid #c0564d;
  border-radius: 5px;
  padding: 0 4px;
  transform: rotate(-8deg);
  opacity: .8;
  white-space: nowrap;
}

.size-badge {
  font-size: 10px;
  font-weight: 900;
  color: #4f7a4f;
  background: #e3f4dd;
  border: 2px solid #7db07d;
  border-radius: 6px;
  padding: 0 5px;
  white-space: nowrap;
}

.row-label { display: flex; align-items: center; gap: 10px; text-align: left; }
.thumb { width: 38px; height: 38px; object-fit: contain; flex: 0 0 38px; }
.thumb-emoji { font-size: 28px; line-height: 38px; text-align: center; }

.row-words { display: flex; flex-direction: column; gap: 2px; }
.row-words b { font-size: 15px; }
.flavor { font-size: 12px; opacity: .72; font-style: italic; }

/* What a goodie DOES, under the pretty line and deliberately not italic, so the
   eye can tell the story from the fact. Only the few goodies that actually do
   something carry one. The live count under it is quieter still: it is a thing
   to notice, not a thing to obey. */
.does { font-size: 12px; opacity: .95; display: block; margin-top: 2px; }
.does-now {
  font-size: 12px;
  display: block;
  margin-top: 2px;
  color: #7a6a4a;
  background: #fdf6e4;
  border-radius: 8px;
  padding: 3px 7px;
  align-self: flex-start;
}

.row button {
  min-height: 44px;
  padding: 8px 14px;
  border-radius: 12px;
  border: 2px solid #7db07d;
  background: #e3f4dd;
  font-weight: 700;
  color: #3f6b3a;
  cursor: pointer;
  white-space: nowrap;
}
.row button:disabled { opacity: .45; cursor: not-allowed; }

.secondary-btn {
  margin-top: 14px;
  min-height: 44px;
  width: 100%;
  border-radius: 12px;
  border: 2px solid #d0b48b;
  background: #f6e8d2;
  font-weight: 700;
  color: #7a5b34;
  cursor: pointer;
  font-size: 15px;
}

/* ---------- PETalog, the collection book ---------- */
.modal-box.wide { width: min(720px, 94vw); }

.subtle { font-size: 13px; opacity: .72; margin-bottom: 12px; }

#petalog-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 10px;
}

/* Every card is the SAME HEIGHT whether the pet is met or not, which is what
   stops one long card stretching its whole row and leaving its neighbours
   half empty. It holds a picture and three short facts, nothing that wraps. */
.pet-card {
  background: #fff6e7;
  border: 2px solid #e4d2b4;
  border-radius: 14px;
  padding: 10px 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 2px;
  height: 148px;
  text-align: center;
  font: inherit;
  cursor: pointer;
  transition: transform .14s ease;
}
.pet-card:not(.locked):hover { transform: translateY(-2px); }
.pet-card.locked { cursor: default; }
.pet-card.rare { background: #fff5d6; border-color: #e6c469; }
.pet-card.locked { background: #f1ece2; border-style: dashed; border-color: #d8cdb9; }

/* A strange friend's card. Iridescent rather than gold, because gold is what
   rare already means and the tier above it has to read as a different KIND of
   thing, not a bigger one. Soft enough to stay cozy: this is still a pastel
   game and a glowing rainbow card would look like a different app. */
.pet-card.mythic {
  background: linear-gradient(150deg, #fdf1fb 0%, #f2f0ff 45%, #eaf7fb 100%);
  border-color: #cbb9e8;
}
.pet-card.mythic .pet-rank { color: #8a6fb5; }

/* The chapter that does not exist until the first sighting. It spans the whole
   grid so it reads as a heading and not as a card. */
.petalog-chapter {
  grid-column: 1 / -1;
  margin: 14px 0 2px;
  font-size: 15px;
  color: #8a6fb5;
  letter-spacing: .3px;
}

.card-art {
  width: 74px;
  height: 74px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.card-art img { max-width: 100%; max-height: 100%; object-fit: contain; }
.card-emoji { font-size: 44px; }

/* an animal we have not met is a dark silhouette */
.card-art.unknown img { filter: brightness(0) opacity(.28); }
.card-art.unknown .card-emoji { filter: brightness(0) opacity(.28); }

.pet-name { font-size: 15px; color: #5b4636; }
.pet-card.locked .pet-name { letter-spacing: 2px; opacity: .55; }

.pet-rank {
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .5px;
  color: #8a7250;
}
.pet-card.rare .pet-rank { color: #a37b16; }

.pet-facts { font-size: 11px; opacity: .75; }
.pet-hint  { font-size: 11px; opacity: .5; font-style: italic; }
.pet-story {
  font-size: 11.5px;
  line-height: 1.35;
  font-style: italic;
  opacity: .82;
  margin: 4px 0 2px;
}

/* ---------- the paw menu, nine spots on one card ---------- */
#menu-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

.menu-cell {
  position: relative;
  background: #fff6e7;
  border: 2px solid #e4d2b4;
  border-radius: 16px;
  padding: 14px 6px 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  font-family: inherit;
  color: #6b4d2e;
  min-height: 84px;
  transition: transform .14s ease, background .2s;
}
.menu-cell:hover:not(:disabled) { transform: translateY(-2px); background: #fffaf0; }
.menu-cell:active:not(:disabled) { transform: scale(.96); }

.menu-icon  { font-size: 26px; line-height: 1; }
.menu-label { font-size: 12.5px; font-weight: 700; }

.menu-cell.soon { opacity: .45; cursor: default; }
.soon-tag {
  position: absolute;
  top: 5px;
  right: 6px;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: .5px;
  text-transform: uppercase;
  background: #ece0cb;
  color: #8a7250;
  border-radius: 999px;
  padding: 1px 6px;
}

.save-code {
  width: 100%;
  margin-top: 10px;
  padding: 8px;
  border: 2px solid #e4d2b4;
  border-radius: 10px;
  background: #fff6e7;
  font-family: ui-monospace, Menlo, monospace;
  font-size: 10px;
  color: #7a5c3d;
  resize: vertical;
}
.save-code.hidden { display: none; }

/* ---------- the wish note ----------
   Paper, never a task list. No bars, no counters, no deadline. */
.wish {
  position: relative;
  background: #fffaf0;
  border: 2px solid #e0d0ae;
  border-radius: 14px;
  padding: 14px 16px 14px 40px;
  margin-bottom: 10px;
}
.wish p { font-size: 14.5px; line-height: 1.5; color: #5b4636; }

.wish-star {
  position: absolute;
  left: 14px;
  top: 13px;
  font-size: 15px;
  color: #d9b44a;
}

.wish-pay {
  display: inline-block;
  margin-top: 7px;
  font-size: 11.5px;
  font-weight: 800;
  color: #8a6d1a;
  background: #fdf0c7;
  border: 2px solid #ecd490;
  border-radius: 999px;
  padding: 1px 9px;
}

.wish-empty { background: #f6f1e6; border-style: dashed; }
.wish-empty p { opacity: .68; font-style: italic; }
.wish-empty .wish-star { opacity: .4; }

/* a wish came true: the paw wears a small star until the note is read */
#paw.has-news::after {
  content: "✦";
  position: absolute;
  top: -3px;
  right: -3px;
  font-size: 15px;
  color: #d9b44a;
  background: #fffaf0;
  border-radius: 50%;
  width: 20px;
  height: 20px;
  line-height: 19px;
  box-shadow: 0 2px 6px rgba(80, 55, 25, .3);
}

.help-list { list-style: none; display: flex; flex-direction: column; gap: 9px; }
.help-list li {
  font-size: 14px;
  line-height: 1.45;
  padding-left: 18px;
  position: relative;
}
.help-list li::before { content: "🐾"; position: absolute; left: 0; font-size: 11px; top: 2px; }

.pet-likes { display: flex; gap: 4px; margin-top: 4px; }
.pet-likes img { width: 26px; height: 26px; object-fit: contain; }
.like-emoji { font-size: 18px; }

/* ---------- the talking paper box ---------- */
#dialog {
  position: fixed;
  inset: 0;
  background: rgba(50, 34, 18, .55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 70;
  cursor: pointer;
  animation: dim-in .25s ease;
}
#dialog.hidden { display: none; }

.paper-box {
  position: relative;
  background: #fffaf0;
  border: 2px solid #cbb08a;
  border-radius: 16px;
  padding: 22px 26px 26px;
  width: min(400px, 88vw);
  text-align: center;
  box-shadow: 0 16px 36px rgba(0, 0, 0, .32);
  animation: box-pop .28s cubic-bezier(.2, 1.3, .5, 1);
}

#dialog-art { display: flex; justify-content: center; margin-bottom: 8px; }
#dialog-art .card-art { width: 104px; height: 104px; }

#dialog-text {
  font-size: 17px;
  line-height: 1.5;
  color: #5b4636;
}
#dialog-text .hi { color: #3f7bb8; font-weight: 800; }

/* the welcome is never a cage */
.skip-link {
  display: block;
  margin: 14px auto 0;
  background: none;
  border: none;
  font-family: inherit;
  font-size: 12.5px;
  color: #9a8264;
  text-decoration: underline;
  cursor: pointer;
  padding: 6px 10px;
}
.skip-link:hover { color: #6b4d2e; }
.skip-link.hidden { display: none; }

/* box 2 points at the bowl without ever blocking it */
#bowl.look-here, #bowl2.look-here { animation: look-here 1.6s ease-in-out 4; }
@keyframes look-here {
  0%, 100% { filter: none; }
  50% { filter: drop-shadow(0 0 14px rgba(255, 214, 120, 1)); }
}

.tap-on {
  position: absolute;
  right: 14px;
  bottom: 8px;
  font-size: 14px;
  color: #b39a72;
  animation: nudge 1s ease-in-out infinite;
}

@keyframes dim-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes box-pop { from { transform: scale(.88); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes nudge   { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(3px); } }

#report-list { list-style: none; }
#report-list li { padding: 6px 0; border-bottom: 1px dashed #e5d8c2; }

/* ---------- toasts (little popup messages) ---------- */
#toasts {
  position: fixed;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* **ABOVE EVERYTHING, because a message nobody can read is worse than no
     message.** Chat's screenshot caught "Puff dropped by the Cardboard Box"
     drawn UNDER the map sheet. Toasts were 60, the paper dialog is 70, so
     anything that arrived while a card was open was painted behind it. They are
     transient, so a probe could never catch it; the screenshot is the evidence.
     90 puts them over the modals, the map and the paper box alike. */
  z-index: 90;
  align-items: center;
}

.toast {
  background: rgba(60, 42, 25, .92);
  color: #fffbe8;
  padding: 10px 18px;
  border-radius: 999px;
  font-size: 14px;
  max-width: 88vw;
  animation: toast-in .25s ease, toast-out .4s ease 3.6s forwards;
}

@keyframes toast-in  { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(8px); } }

/* ---------- the camera, the album and one big postcard ---------- */
#camera {
  position: fixed;
  /* Below the paw counters now, the same 60px step the music button takes on the
     left, so the two columns match. */
  top: calc(max(12px, env(safe-area-inset-top)) + 60px);
  right: max(12px, env(safe-area-inset-right));
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 2px solid #cbb08a;
  background: #fffaf0;
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
  z-index: 20;
  box-shadow: 0 3px 10px rgba(80, 55, 25, .26);
  transition: transform .15s ease;
}
#camera:hover  { transform: scale(1.08); }
#camera:active { transform: scale(.92); }

#album-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 10px;
}

.card-thumb {
  background: #fff6e7;
  border: 2px solid #e4d2b4;
  border-radius: 12px;
  padding: 6px;
  cursor: pointer;
  transition: transform .14s ease;
}
.card-thumb:hover { transform: translateY(-2px); }
.card-thumb img { width: 100%; border-radius: 7px; display: block; }
.card-thumb figcaption { font-size: 11px; opacity: .7; text-align: center; margin-top: 4px; }

#card-view img { width: 100%; border-radius: 12px; display: block; }

/* **THE QUIETEST BUTTON WAS THE ONE YOU COULD NOT READ.** #9a8264 on #f6f1e6
   measures 3.24 to 1 where readable text needs 4.5. #6b5027 is 6.65 and is
   already the sound button's colour, so nothing new enters the palette and the
   mood does not move.

   **DELIBERATELY NOT A PASS OVER THE WHOLE INTERFACE.** The same review that
   raised this praised the game for feeling calm and premium, and that comes
   from the low contrast everywhere else. Raising it across the board would
   trade the thing people liked for the thing they suggested. One button. */
.secondary-btn.quiet { background: #f6f1e6; border-color: #ded2bc; color: #6b5027; }

.name-field {
  width: 100%;
  min-height: 44px;
  margin-bottom: 14px;
  padding: 10px 12px;
  border: 2px solid #e4d2b4;
  border-radius: 12px;
  background: #fffaf0;
  font-family: inherit;
  font-size: 15px;
  color: #5b4636;
}

/* ---------- coming back ----------
   A scene, not a receipt: faces first, one warm number, the rest counted. */
/* The headings and the gaps tighten on a small screen for the same reason the
   tiles do: three rows of friends plus a headline plus the paws plus a button
   has to fit 375 points of height without scrolling. */
.back-head {
  font-size: clamp(12px, 3.6vmin, 15px);
  font-weight: 700;
  color: #6b4d2e;
  margin: clamp(0px, 0.5vmin, 4px) 0 clamp(2px, 1.2vmin, 8px);
}

.back-row {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(5px, 2.4vmin, 12px);
  margin-bottom: clamp(4px, 2vmin, 16px);
}

/* **SMALLER TILES, BECAUSE THERE ARE THREE ROWS OF THEM NOW.** Flavia, Aug 11,
   right after asking for the faces back: "can u make the tile smaller so it
   fits the mobile better too?" A phone in landscape is 375 tall and this panel
   has to hold a headline, up to three rows of friends, the paws line and a
   button. At 86 wide with a 72 face, three rows did not fit and the screen
   scrolled, which turns a warm moment into a chore.

   They scale with the screen rather than stepping at a breakpoint, so a small
   phone gets small faces and a tablet still gets generous ones, and the caption
   holds a readable size at both ends. */
.back-pet {
  width: clamp(48px, 15vmin, 86px);
  text-align: center;
}
.back-pet .card-art {
  width: clamp(40px, 12.5vmin, 72px);
  height: clamp(40px, 12.5vmin, 72px);
  margin: 0 auto;
}
.back-pet figcaption {
  font-size: clamp(9px, 2.8vmin, 12px);
  font-weight: 700;
  color: #6b4d2e;
  margin-top: 2px;
}


/* The ones still in the yard, marked inside the shared row rather than given a
   row of their own. A soft green ring, the same colour as the spot glow, so it
   reads as "you can go and stroke this one" without needing a caption. */
.back-pet.still-here .card-art {
  box-shadow: 0 0 0 2px #9ec79e, 0 0 10px rgba(158, 199, 158, .6);
  border-radius: 12px;
}


/* the ones you met for the first time get a soft glow */
.back-row.new-friends .card-art img { filter: drop-shadow(0 0 9px rgba(255, 214, 120, .95)); }

.back-paid {
  font-size: clamp(13px, 3.4vmin, 15px);
  color: #5b4636;
  background: #fff6e7;
  border: 2px solid #e4d2b4;
  border-radius: 12px;
  /* slimmer on a phone, for the same reason the tiles are */
  padding: clamp(6px, 2vmin, 10px) clamp(9px, 2.6vmin, 14px);
  margin-bottom: clamp(5px, 1.8vmin, 10px);
}

.back-rest { font-size: 13.5px; opacity: .75; line-height: 1.5; }

.back-more {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 72px;
  font-size: 12.5px;
  font-weight: 700;
  color: #9a8264;
  background: #f6f1e6;
  border: 2px dashed #ded2bc;
  border-radius: 12px;
}

/* ---------- breeding: the nest, the newborn, the hearts ----------
   A pet born in this yard is not a rare guest, so it gets its own warm
   glow rather than borrowing the golden one. See breeding.js. */
.sprite.animal.born img,
.sprite.animal.born .backup { filter: drop-shadow(0 0 11px rgba(255, 186, 214, .95)); }
.sprite.animal.born .name-tag { background: #ffe1ee; }

.pet-card.born { background: #fff2f7; border-color: #eec4d8; }
.pet-card.born .pet-rank { color: #b7638c; }

/* the closest friendships, on the pet's own card */
.pet-bonds {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-top: 5px;
  font-size: 11px;
  color: #8a7250;
}
.pet-bonds .bond { display: flex; align-items: center; justify-content: center; gap: 4px; }
.bond-hearts { color: #e08aae; letter-spacing: -1px; }

/* the naming card. The room dims, the newborn is the only thing lit. */
#nursery .modal-box { text-align: center; }
#nursery h2 { font-size: 19px; line-height: 1.35; }
#nursery-art {
  width: 168px;
  height: 168px;
  margin: 6px auto 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: radial-gradient(circle, #fff3f8 0%, #fdeaf2 62%, rgba(253, 234, 242, 0) 100%);
}
#nursery-art .card-art { width: 132px; height: 132px; }
#nursery-art .card-emoji { font-size: 68px; }
#nursery-line { font-size: 14.5px; line-height: 1.55; margin-bottom: 12px; }
#nursery label { display: block; margin-bottom: 6px; }

/* ---------- the map ----------
   One drawn page with markers on top. Everything is positioned in percent
   of the picture, so the whole world scales with the screen and the plots
   stay exactly where they were measured. MAP_BRIEF.md holds the rules. */
.modal-box.map-page {
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: stretch;
  /* the world is the point, so this card is wider than the others and
     the drawing takes as much of it as the screen will allow */
  width: min(940px, 94vw);
  max-height: 92vh;
  padding: 16px;
}

/* The frame HUGS the picture and the picture decides its own shape. The
   world went from 4 by 3 to 8 by 3 when the island was added, and it may
   change again, so nothing here hardcodes a ratio. This is also what keeps
   #map-marks exactly over the drawing: the frame is never bigger than it. */
.map-frame {
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
}
.map-canvas {
  position: relative;
  width: fit-content;
  margin: 0 auto;
  line-height: 0;
}
#map-bg {
  display: block;
  max-width: 100%;
  max-height: 66vh;
  width: auto;
  height: auto;
  border-radius: 14px;
  border: 2px solid #e4d2b4;
}
#map-marks { position: absolute; inset: 0; }

/* every marker is a round badge sitting on its own measured clearing */
/* **THE SIZE AND THE SPOT ARE SHARED, ON PURPOSE.** `.map-news` is the little
   box the "somebody visited" dot lives in, laid exactly over its badge. It has
   to be the same size and in the same place as the badge or the dot slides off
   the shoulder, so the two read the numbers from one rule rather than each
   keeping a copy. Every later rule that changes the badge's size, on a short
   screen and on a phone, changes both. See newsDot in map.js for why the dot
   is a neighbour of the badge instead of living inside it: the short version is
   41 ms a frame down to 8 ms. */
.map-mark,
.map-news {
  position: absolute;
  width: 5.75%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
}
.map-mark {
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
  transition: transform .15s ease;
}
/* the dot's box is never a tap target: the badge underneath is */
.map-news { pointer-events: none; }
.map-mark img { width: 100%; height: 100%; object-fit: contain; display: block; }

/* The badges arrive as SQUARE pictures with white corners, not as round
   cut-outs, so laid on the map they each sat in a little white box. The
   drawn medallion fills 96% of the square and is centred, measured, so
   clipping to a circle of 48.2% trims the corners and loses no art.
   If chat ever ships them cut out, this line can simply go. */
.map-mark img,
#map-panel-art img,
#room-buttons button img { clip-path: circle(48.2% at 50% 50%); }
.map-mark:hover { transform: translate(-50%, -50%) scale(1.08); }
/* and the dot rides along, because paintMap puts it straight after its badge */
.map-mark:hover + .map-news { transform: translate(-50%, -50%) scale(1.08); }

/* LOCKED IS GREY, OWNED IS FULL COLOUR. Flavia's call, and done with a
   filter rather than a second set of grey pictures: no extra files, no
   extra weight, and it hands us the reward moment for nothing, because the
   very same filter is what gets animated away when a place is bought.
   Grey plus a small drop in brightness reads as "not yet". Never a padlock. */
.map-mark img { transition: filter .25s ease, opacity .25s ease; }
.map-mark.locked img { filter: grayscale(1) brightness(.94); opacity: .78; }

/* the glow has to sit on the BUTTON, not the picture: clip-path trims
   whatever the picture's own filter draws outside the circle */
.map-mark.owned { filter: drop-shadow(0 0 6px rgba(255, 206, 96, 1)); }

/* and the moment it becomes yours, the colour floods back in */
@keyframes colour-floods {
  from { filter: grayscale(1) brightness(.94); opacity: .78; }
  60%  { filter: grayscale(0) brightness(1.06); opacity: 1; }
  to   { filter: none; opacity: 1; }
}
.map-mark.blooming img { animation: colour-floods 1s ease-out both; }
@media (prefers-reduced-motion: reduce) { .map-mark.blooming img { animation: none; } }
.map-mark.chosen,
.map-news.chosen { transform: translate(-50%, -50%) scale(1.16); }
.map-mark.chosen::after {
  content: "";
  position: absolute;
  inset: -8%;
  border-radius: 50%;
  border: 3px solid #f0b6cd;
}
.map-mark.here::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -14%;
  width: 22%;
  aspect-ratio: 1;
  transform: translateX(-50%);
  border-radius: 50%;
  background: #f5a8c4;
  box-shadow: 0 0 0 3px #fff8ee;
}
.map-mark.no-image img { display: none; }

/* the little house is drawn into the picture, so this is a tap target only */
.map-home {
  position: absolute;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border: 0;
  background: none;
  border-radius: 50%;
  cursor: pointer;
}

/* friends out on the paths between the yards */
.map-pet {
  position: absolute;
  width: 3.25%;
  transform: translate(-50%, -70%);
  pointer-events: none;
  filter: drop-shadow(0 2px 3px rgba(90, 70, 50, .35));
}

/* **THE MAP PICTURE WAS COLLAPSING TO A 50 PIXEL STRIP ON A PHONE**, which is
   why Flavia could not tap a place: all twenty badges were crushed into a band
   the height of a fingernail, overlapping each other.

   The cause is flex arithmetic, not the map. On a 384 tall phone the box gets
   353, the picture wants 253 and the panel wants 259, so something has to give.
   The panel's contents have a natural minimum and the picture does not, so the
   PICTURE lost every time and shrank to 50.

   **The map is the point of that screen**, so it now takes the space it needs
   and the panel keeps only what it must. */
.map-frame {
  flex: 1 1 auto;
  min-height: 46%;
}
#map-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: clamp(2px, 0.8vmin, 4px);
  flex: 0 0 auto;
}
#map-panel .secondary-btn { max-width: 320px; margin-top: clamp(4px, 1.4vmin, 14px); }
/* the badge in the panel shrinks with the screen, like everything else does */
#map-panel-art, #map-panel-art .card-art {
  width: clamp(44px, 13vmin, 96px);
  height: clamp(44px, 13vmin, 96px);
}
#map-panel-name { font-size: clamp(14px, 3.6vmin, 17px); color: #5b4636; }
#map-panel-line { font-size: clamp(11px, 2.9vmin, 12.5px); line-height: 1.45; font-style: italic; opacity: .85; }
#map-panel-open {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-bottom: clamp(2px, 1vmin, 8px);
  font-size: clamp(11px, 2.8vmin, 12px);
  line-height: 1.45;
  color: #8a7250;
}
#map-panel-open .map-price { font-weight: 800; color: #a37b16; font-size: 13px; }
#map-panel-open .map-count { font-weight: 700; color: #6f5a3d; }
#map-panel .secondary-btn { width: 100%; margin: 0; }
#map-panel .secondary-btn + .secondary-btn { margin-top: 6px; }

/* the room buttons wear their map badges rather than an emoji */
#room-buttons button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
#room-buttons button img { width: 30px; height: 30px; object-fit: contain; }

/* the tenth menu cell, "Yard", sits centred under the other nine */
/* **THIS CENTRED A LEFTOVER, AND THEN THERE WAS NO LEFTOVER.** With eleven
   entries the tenth was the odd one out and pushing it to the middle column
   made the last row look deliberate. Adding Friends made twelve, which fills
   three by four exactly, and the rule was still shoving the tenth sideways:
   Help jumped to the middle, Friends to the right, and Yard was left alone on
   a fifth row with a hole beside it. A layout rule written for one count is a
   bug at every other count, so this one now only applies when there IS an odd
   one out. */
.menu-cell:nth-child(10):nth-last-child(2) { grid-column: 2; }

/* A PHONE HELD SIDEWAYS is the reference screen, and there the map and its
   panel have to sit side by side or the buttons fall below the fold. So on
   a short screen the drawing is sized by HEIGHT and the frame hugs it,
   which keeps the markers lined up with their clearings. */
/* ON A PHONE THE MAP IS THE SCREEN, not a picture in a box.
   The old rule keyed off screen HEIGHT, so a phone held upright never met
   it and the world was squeezed into a strip about a fifth of the screen
   with twenty five badges piled on top of each other. Chat caught it at
   375 by 812. Keyed off WIDTH now, so both orientations get it.

   The world is drawn at a fixed comfortable size, bigger than the window,
   and you DRAG it. That is what the brief always described. It also means
   more land never makes the map worse, which the strip did: every new
   island had to share the same few hundred points. */
@media (max-width: 760px) {
  #map .modal-box.map-page {
    position: relative;
    width: 100vw;
    max-width: 100vw;
    height: 100vh;
    max-height: 100vh;
    flex-direction: column;
    border-radius: 0;
    padding: 0;
    gap: 0;
    overflow: hidden;
  }

  /* The frame is the window onto the world, and it pans both ways. Its
     background is the SEA sampled from the drawing, #9cc4d2, so the space
     above and below a short wide world reads as more ocean rather than as a
     white void. The world is also centred in it rather than pinned to the
     top. */
  .map-frame {
    flex: 1 1 auto;
    width: 100%;
    height: 100%;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    background: #9cc4d2;
    display: flex;
    align-items: center;
  }
  .map-canvas { margin: 0 auto; flex: 0 0 auto; }
  /* 760 across puts a badge at 44 points with 49 between, so a thumb lands
     on one place and not on two. Never size this by the screen. */
  #map-bg {
    width: 760px;
    height: auto;
    max-width: none;
    max-height: none;
    border-radius: 0;
    border: 0;
  }

  /* **THE CARD BECAME A WALL ACROSS THE MAP.** Measured at 667x375: the panel
     was 221px of a 375px screen, 59 percent of it, carrying four things: a
     badge, a name, "You are here" and one button. The player saw the top third
     of the neighbourhood and could not reach the rest.

     So on a short screen it is a BAR, not a card: one row, the badge small on
     the left, the name beside it, the buttons on the right, and the
     description line dropped. It is lovely and it is not worth a third of the
     map. Buttons stay at 44 so the tap rule holds.

     **WHAT I DID NOT DO, and why.** Chat also asked for the whole map picture
     to fit above the bar. It cannot without breaking an older deliberate rule:
     `#map-bg` is pinned at 760px across because that is what puts each badge
     at 44 points with 49 between, so a thumb lands on one place and not two.
     Fitting a 424px tall map into the ~300px left would shrink every badge to
     about 32 points. The frame scrolls instead, and with the bar at 64 instead
     of 221 the visible map went from 154px to about 300px, so the scroll is
     now a nudge rather than the only way to see two thirds of the world. */
  #map-panel {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    text-align: left;
    gap: 10px;
    padding: 6px 12px calc(6px + env(safe-area-inset-bottom));
    min-height: 64px;
    background: #fffaf1;
    border-top: 2px solid #e4d2b4;
    border-radius: 18px 18px 0 0;
    box-shadow: 0 -10px 28px rgba(90, 70, 50, .22);
  }
  /* the badge shrinks to the row, the name sits beside it, the line goes away */
  #map-panel-art, #map-panel-art .card-art { width: 40px; height: 40px; flex: 0 0 auto; }
  #map-panel-name { font-size: 14.5px; font-weight: 800; flex: 1 1 auto; min-width: 0;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  #map-panel-line { display: none; }
  #map-caption { display: none; }
  #map-panel-open { flex-direction: row; gap: 8px; margin: 0; align-items: center;
    flex: 0 0 auto; font-size: 11.5px; }
  #map-panel .secondary-btn { width: auto; min-width: 96px; max-width: 150px;
    min-height: 44px; margin: 0; flex: 0 0 auto; padding: 0 12px; font-size: 13.5px; }
  #map-panel .secondary-btn + .secondary-btn { margin-top: 0; margin-left: 6px; }
  /* the frame ends where the bar begins, so nothing is ever hidden underneath */
  .map-frame { padding-bottom: 68px; }
  #map-panel .secondary-btn { max-width: none; }
  #map-panel-art, #map-panel-art .card-art { width: 62px; height: 62px; }
  #map-panel-line { font-size: 12px; }
  #map-panel-open { font-size: 11.5px; margin-bottom: 4px; }

  /* The 44 point floor is a THUMB rule, so it belongs here with the phone
     rather than applying to a mouse as well. The plots sit 6.5% apart and
     the world is drawn 760 across here, which puts 49 points between
     badges, so a finger lands on one place and never on two.

     The news dot's box takes the same floor, or the dot slides off the
     shoulder of a badge that grew underneath it. */
  .map-mark,
  .map-news { min-width: 44px; }
}

/* The caption, used only when the map shows itself. It lives INSIDE the
   panel so it can never take part in the card's own layout: as a sibling
   with a 100% basis it squeezed the map to nothing the day the card became
   a row. And it needs its own hidden rule, because this game has no global
   one, every element that hides itself carries its own. */
#map-caption {
  margin: 0 0 6px;
  text-align: center;
  font-size: 13px;
  font-weight: 700;
  color: #7a6446;
}
#map-caption.hidden { display: none; }

/* a place appearing on its clearing for the first time */
@keyframes place-arrives {
  0%   { transform: translate(-50%, -50%) scale(.2); opacity: 0; }
  60%  { transform: translate(-50%, -50%) scale(1.18); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
.map-mark.arriving { animation: place-arrives .7s ease-out both; }
@media (prefers-reduced-motion: reduce) { .map-mark.arriving { animation: none; } }

/* drawn menu icons, sized to sit exactly where the emoji sat */
.menu-icon img { width: 34px; height: 34px; object-fit: contain; display: block; }
#wallet .coin img { width: 20px; height: 20px; object-fit: contain; vertical-align: -4px; }

/* ---------- THE YARD PANS ON A PHONE ----------
   The original does this and Flavia remembered it: the yard is WIDER than
   the screen and you drag it, and their yard expansion turns it into two
   screens you swipe between. We were scaling the room to fit the width,
   which on a 375 phone gave a 375 by 281 band across the middle, a third of
   the screen, with blurred wood filling the rest. That is why the pets
   looked small.

   Two screen widths, not three. Three would fill the height and turn the
   game into dragging. On a 375 by 812 phone this makes the room 750 by 562,
   about two thirds of the height, and every pet DOUBLES.

   It lands on the Big Yard exactly: the five starting spots sit in the first
   screen, the two it adds live in the second, so buying it gives you more
   yard to walk into rather than shrinking what you have.

   Panning is the browser's own overflow scrolling, deliberately, not a drag
   handler. Native scrolling already knows the difference between a drag and
   a tap, so a finger moving across a pet never steals its tap, and momentum
   comes free. `min-width: 100vw` keeps a phone held sideways exactly as it
   is now, since two screens there would be far taller than the screen. */
/* **HELD UPRIGHT ONLY.** This whole block is the two-screens-of-swipe design and
   every number in it was measured on a phone in PORTRAIT. It was keyed on width,
   which was the same thing back when a wide screen meant a computer. It is not
   any more: a phone held sideways can be under 760 too, and then it landed here
   and got `width: min(...)`, which FITS the picture inside the screen. Flavia,
   Aug 12: *"u made the place wallpaper too small on mobile horizontal instead of
   letting it big 21:9 so u can swipe on it. u fit the photo to the screen."*
   Exactly right, and the word is min. Landscape belongs to the cover rule. */
@media (max-width: 760px) and (orientation: portrait) {
  #stage {
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: none;
    /* centred, so the blurred surround sits evenly above and below rather
       than piling up under the room */
    display: flex;
    align-items: center;
  }
  #yard {
    position: relative;
    top: auto;
    left: auto;
    transform: none;
    flex: 0 0 auto;
    margin: 0 auto;
    /* up to TWO screens wide, but never taller than the screen. That
       second half is the whole trick: a phone held sideways is short,
       so the height ceiling brings it back to exactly the fit-on-screen
       room it always was, while a phone held upright gets the big one. */
    /* **THE ROOM WAS USING HALF THE PHONE.** Measured on a 375x812 phone: the
       room was 422 tall against 812 of screen, with 79 unused pixels above it
       and 143 below. The old ceiling of a full screen height was written for
       the isometric 4:3 rooms, where filling the height made the room enormous.
       A 16:9 room is barely half as tall for the same width, so the same rule
       left half the phone as blurred surround.

       0.79 of the screen height is the band between the lowest button and the
       top of the wallet, which is the space actually free. On that phone it
       gives a room 1140 wide and 641 tall, so it fills the visible area and
       costs three screens of swipe instead of two. Filling the whole 812 would
       be four screens, which turns every visit into a hunt for your pets. */
    /* **AND THE CAP THAT WAS ACTUALLY BINDING WAS THE WIDTH, NOT THE HEIGHT.**
       My first attempt only changed the height ceiling and the room did not
       move a pixel: on a 375x812 phone `200vw` is 750, the height rule allowed
       1140, and the smaller of the two wins. Two screens of swipe WAS the
       constraint. Raising it to 320vw lets the height rule bind instead, which
       is what makes the room fill the free band. */
    /* **THE YARD IS THE WHOLE SCREEN.** ATSUME_FEEL_BRIEF.md: "the yard fills
       the entire screen, especially on a phone in portrait, and every menu is a
       warm paper card floating OVER the yard. The world never disappears."

       **AND THE 21% WAS MINE, not a reserved band.** The paw, camera, music and
       wallet were already `position: fixed` floating over the stage; they never
       took layout space. The missing fifth of the screen was the 0.79 I put in
       an hour earlier to leave room for them, which was solving a problem that
       did not exist. Full height now.

       THE COST IS REAL AND IT PULLS THE OTHER WAY: 812 tall at 16:9 is 1445
       wide, so a portrait phone shows a quarter of the room and swiping it end
       to end is nearly four screens. Filling the glass and seeing the room are
       opposites here, and only a portrait REDRAW of the rooms escapes it. */
    width: min(400vw, calc(100svh * var(--ar-w) / var(--ar-h)));
    height: auto;
    aspect-ratio: var(--ar-w) / var(--ar-h);
  }
  /* the blurred surround is fixed to the screen, so it cannot scroll away */
  #stage::before { position: fixed; }
}

/* ---------- one pet, big ----------
   Where the writing finally has room. In the grid the line was four words
   wide and wrapped to eight lines, so nobody read the best thing in the book. */
.petpage-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 2px;
  padding-bottom: 6px;
}
.petpage-body .card-art.big { width: 148px; height: 148px; }
.petpage-name { font-size: 22px; color: #5b4636; margin: 4px 0 0; }
.petpage-body.rare .pet-rank { color: #a37b16; }
.petpage-body.born .pet-rank { color: #b7638c; }

.petpage-story {
  /* about 40 characters a line, which is where reading is comfortable */
  max-width: 34ch;
  margin: 10px auto 12px;
  font-size: 15px;
  line-height: 1.55;
  font-style: italic;
  color: #6b5540;
}
.petpage-facts { font-size: 12.5px; opacity: .75; margin-bottom: 10px; }
.petpage-label {
  font-size: 10.5px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: #a08a68;
  margin-top: 6px;
}
.petpage-body .pet-likes img { width: 34px; height: 34px; }
.petpage-body .pet-bonds { font-size: 12.5px; margin-top: 2px; }

@media (max-width: 760px) {
  /* three columns on a phone, since the card no longer holds a sentence */
  #petalog-grid { grid-template-columns: repeat(3, 1fr); }
  .petpage-story { max-width: none; font-size: 14.5px; }
}

/* the two sound switches sit like the room buttons, but they are toggles */
.sound-row button.active { background: #ffeec4; border-color: #d8b98f; color: #6b5027; }

/* ---------- petting ----------
   A heart drifts off the pet you just stroked, and the pet gives a small
   happy squish. Both are decoration and both clean up after themselves. */
.heart {
  position: absolute;
  bottom: 55%;
  font-size: calc(20px * var(--u));
  color: #f0879f;
  text-shadow: 0 1px 2px rgba(255, 255, 255, .8);
  pointer-events: none;
  animation: heart-up 1.2s ease-out forwards;
}
@keyframes heart-up {
  0%   { opacity: 0; transform: translateY(0) scale(.6); }
  20%  { opacity: 1; transform: translateY(-8px) scale(1); }
  100% { opacity: 0; transform: translateY(-38px) scale(1.05); }
}

/* the little acknowledgement that the stroke landed */
.sprite.petted .rig { animation: petted-squish .45s ease-out; }
@keyframes petted-squish {
  0%   { transform: scaleY(1)    scaleX(1); }
  35%  { transform: scaleY(.94)  scaleX(1.05); }
  100% { transform: scaleY(1)    scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
  .heart { animation-duration: .6s; }
  .sprite.petted .rig { animation: none; }
}

/* the pet that lives here, in its own corner */
#resident {
  position: absolute;
  width: calc(120px * var(--u));
  height: calc(120px * var(--u));
  transform: translate(-50%, -100%);
  pointer-events: none;
}
#resident.hidden { display: none; }
#resident .sprite { pointer-events: auto; }
#stay .modal-box { text-align: center; }
#stay-art { width: 132px; height: 132px; margin: 0 auto 10px; }
#stay-art .card-art { width: 132px; height: 132px; }
#stay-line { font-size: 15px; line-height: 1.55; margin-bottom: 14px; }

/* ---------- COMING AND GOING (arrivals.js) ----------
   Flavia, Aug 10: "ok fading". Half a second, both directions, and nothing
   in the game waits for either of them. The leaving copy has no pointer
   events at all, so the seat is free and the tip is paid while it is still
   on screen. */
@keyframes pet-arrive {
  from { opacity: 0; transform: translateY(9%) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes pet-leave {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(7%) scale(0.98); }
}
.sprite.animal.arriving { animation: pet-arrive .5s ease-out both; }
.slot.ghost { pointer-events: none; }
.slot.ghost .sprite.animal { animation: pet-leave .5s ease-in both; }

/* A player who asks for less movement gets none of this. Same switch the
   rest of the game already respects. */
@media (prefers-reduced-motion: reduce) {
  .sprite.animal.arriving { animation: none; }
  .slot.ghost { display: none; }
}

/* ---------- a place that had visitors while you were elsewhere ----------
   Deliberately the quietest thing on the map: a small warm dot on the badge's
   shoulder, no number, no red, nothing to clear. It is there to help you
   choose where to go next, not to ask anything of you. It breathes slowly
   rather than blinking, because a blink is a demand.

   **IT USED TO SIT INSIDE THE BADGE AND IT WAS COSTING THE MAP EVERY FRAME.**
   Measured Aug 12, phone screen, processor at a quarter speed: with eleven
   places carrying news a drag frame took 41 ms and the map ran at 24 pictures a
   second even standing still. With the dot gone, 8 ms. The badge wears four
   stacked drop shadows and a browser has to work a shadow like that out again
   over everything inside the badge every time anything inside it moves, so a
   dot that breathes forever meant four shadows redrawn sixty times a second,
   eleven times over.

   Now it lives in `.map-news`, its own box laid exactly over the badge with no
   shadows on it. Nothing about how it looks changed. 41 ms down to 8 ms. */
.map-news::after {
  content: "";
  position: absolute;
  top: 2%;
  right: 2%;
  width: 26%;
  height: 26%;
  border-radius: 50%;
  background: #f6c66a;
  border: 2px solid #fffaf0;
  box-shadow: 0 1px 4px rgba(90, 60, 20, .35);
  animation: news-breathe 3.4s ease-in-out infinite;
}
@keyframes news-breathe {
  0%, 100% { opacity: .78; transform: scale(1); }
  50%      { opacity: 1;   transform: scale(1.08); }
}
@media (prefers-reduced-motion: reduce) {
  .map-news::after { animation: none; }
}


/* ---------- making a place readable on a busy map ----------
   A soft yellow glow on a pale cream clearing has nothing to push against, so
   the badges vanished into the ground. Chat's fix and their measurement: the
   glow alone cannot separate pale from pale, it needs contrast in BOTH
   directions. A dark rim first so the SHAPE reads, then the warm halo outside
   it so it still feels alive. The dark rim is the part that matters.

   **THERE USED TO BE A FOURTH, WIDEST GLOW, AND IT COST 41 MS EVERY TIME THE
   MAP OPENED.** Measured Aug 12 on a phone screen with the processor at a
   quarter speed: opening the map dropped one frame of 58 ms, and taking that
   one line away brought it to 17 ms. Nothing else on the map came close.

   A glow of that width does not just draw a wider ring. It makes the browser
   set aside a patch three times the size of the badge and blur the whole patch,
   twenty times over, before a single badge appears. The three that are left
   cover 10 points of warm halo, which is what you actually see.

   How different does it look? Compared side by side at twice the size: 76
   pixels of the entire screen changed by more than about a tenth, and the
   worst of them by a sixth. The dark rim, the part that matters, is untouched.
   If it is ever wanted back it is one line in each of these two blocks:
     drop-shadow(0 0 20px rgba(255, 170, 40, .55))   here
     drop-shadow(0 0 24px rgba(255, 170, 40, .6))    in .map-mark.owned */
.map-mark {
  filter:
    drop-shadow(0 0 1.5px rgba(70, 45, 30, .95))
    drop-shadow(0 0 3px   rgba(70, 45, 30, .55))
    drop-shadow(0 0 10px  rgba(255, 196, 80, .95));
}
.map-mark.owned {
  filter:
    drop-shadow(0 0 1.5px rgba(70, 45, 30, .95))
    drop-shadow(0 0 3px   rgba(70, 45, 30, .6))
    drop-shadow(0 0 12px  rgba(255, 206, 96, 1));
}

/* ---------- the HUD floats over the room now ----------
   With the yard filling the screen these sit on top of the art rather than on
   a band of their own, so they have to hold up over an Ice Palace wall and a
   Sweet Bakery tile as well as over a dark manor. The buttons already carry a
   solid cream face; what they lacked was separation from a pale background, so
   each gets a soft dark ring in addition to its drop shadow. The wallet is the
   one thing that must stay readable over anything, so it gets the same. */
#paw, #camera, #music {
  box-shadow: 0 4px 12px rgba(80, 55, 25, .28),
              0 0 0 1px rgba(90, 62, 30, .28);
}
.coin {
  box-shadow: 0 3px 10px rgba(80, 55, 25, .25),
              0 0 0 1px rgba(90, 62, 30, .22);
  backdrop-filter: blur(2px);
}

/* ---------- your friends, one step out ----------
   **SQUARE TILES, because this is read on a phone.** A tall card wastes the
   width and only fits one per row; a square one fits two on the narrowest
   phone and four on a tablet, and a square is also the shape a postcard crops
   to without cutting anybody's head off. The name sits ON the picture rather
   than under it, so the tile stays square whether the name is short or long. */
.friend-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: 12px;
  margin: 12px 0 6px;
}
.friend-house {
  position: relative;
  aspect-ratio: 1 / 1;          /* the square she asked for */
  width: 100%;
  padding: 0;
  overflow: hidden;
  border-radius: 16px;
  cursor: pointer;
  background: #fffaf0;
  border: 2px solid #e0cba6;
  box-shadow: 0 3px 10px rgba(80, 55, 25, .18);
  display: block;
}
.friend-house img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;            /* fill the square, never letterbox */
  display: block;
}
/* the name rides over the picture on a soft band, so it is readable over a
   bright garden and a dark manor alike */
.friend-house b,
.friend-house small {
  position: absolute;
  left: 0; right: 0;
  padding: 4px 8px;
  background: linear-gradient(180deg, rgba(40, 26, 12, 0), rgba(40, 26, 12, .62));
  color: #fffaf0;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .5);
  font-size: 13px;
  line-height: 1.25;
}
.friend-house b     { bottom: 18px; font-size: 14px; }
.friend-house small { bottom: 0; font-size: 11px; opacity: .9; background: rgba(40, 26, 12, .62); }
.friend-empty { max-width: 30em; margin: 12px auto 4px; line-height: 1.5; }

/* ---------- the Decorate tab ----------
   Square tiles like the friends' houses, for the same reason: a phone reads
   two per row comfortably and a square crops a picture without cutting the
   subject. Grouped by where a piece goes rather than by what it costs. */
.decor-layer { margin: 14px 0 6px; font-weight: 700; }
.decor-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; }
.decor-card {
  /* **RECTANGULAR, PICTURE BESIDE THE WORDS.** Flavia, Aug 14: *"i dont like
     much the taller tile on mobile. try rectangular."*

     A square tile with the line underneath had to grow to 147px tall to fit
     46 characters of text, which halved how many pieces she could see at once.
     Turned on its side the same words sit BESIDE the picture instead of under
     it, so the card is wide and short: the text has a long line to use, the
     artwork keeps its square, and the shelf stays shallow. */
  min-height: 104px;      /* the picture is 92 now and needs room to breathe */
  padding: 6px 8px;
  border-radius: 14px;
  background: #fffaf0;
  border: 2px solid #e0cba6;
  box-shadow: 0 3px 9px rgba(80, 55, 25, .16);
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
}
/* **THE DRAWING IS THE REASON ANYONE OPENS THIS PANEL, AND IT WAS A SIXTH OF THE
   CARD.** Chat measured it on the live page, Aug 14: 56 by 56 inside a 298 by 66
   card. In a list of six rugs you could not tell them apart, so a panel full of
   artwork read like a price list. At 92 the spiral on the round rug, the stripes
   on the runner and the checks on the mat are all visible. Flavia saw the
   comparison and agreed. The picture keeps a fixed square whatever the words do. */
.decor-card img {
  width: 92px;
  height: 92px;
  flex: 0 0 auto;
  object-fit: contain;
}
/* Everything written stacks up in the space left over. */
.decor-words {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  min-width: 0;          /* lets long words wrap instead of stretching the card */
  flex: 1 1 auto;
}
.decor-card b { font-size: 12px; line-height: 1.2; }
.decor-card .decor-price { font-size: 11px; }
.decor-card .decor-flavor { font-size: 12px; text-align: left; }
/* This used to centre the name, from the days when the tile was a square with
   the words underneath. On a sideways card the name starts at the same left
   edge as the price and the line, or the three of them stagger. */
.decor-card b { font-size: 13px; line-height: 1.2; text-align: left; }
.decor-price { font-size: 11px; color: #8a6d1a; }
.decor-soon { text-align: center; margin-top: 14px; }

/* ---------- decorations in the yard ----------
   Three layers, and the order is the whole point: a rug goes UNDER the toys, a
   stool stands AMONG them, a picture hangs BEHIND everything. They never take
   a tap unless you are arranging, so a decoration can never steal a stroke
   meant for a pet. */
.decor-layer-box { position: absolute; inset: 0; pointer-events: none; }
#decor-wall  { z-index: auto; }
#decor-under { z-index: auto; }
#decor-floor { z-index: auto; }
.decor-thing {
  position: absolute;
  transform: translate(-50%, -100%);   /* stands on its point, like everything else */
  pointer-events: none;
  user-select: none;
  /* **WITHOUT THIS, A FINGER PANS THE ROOM INSTEAD OF MOVING THE THING.** The
     stage scrolls sideways on a phone, and a touch drag is claimed by the
     browser's own scrolling before any pointer event arrives. `preventDefault`
     on pointerdown is too late by then. Saying the element handles its own
     touches is the only thing that stops the scroll taking it. A mouse never
     showed this, which is why it has to be tested at phone size. */
  touch-action: none;
}
.decor-thing img { width: 100%; display: block; }
/* **THE LAYER BOXES MUST NEVER TAKE A TAP, ONLY THE THINGS INSIDE THEM.**
   They are full-size overlays, so giving the boxes pointer-events meant the
   topmost one swallowed every tap: only the 47 `floor` pieces could be moved,
   while all 27 `wall` and 4 `under` pieces were stuck where they landed. A
   child with pointer-events auto still receives taps inside a parent set to
   none, which is exactly what is wanted here.

   **AND WHILE ARRANGING THEY COME TO THE FRONT.** A rug lives UNDER the toys,
   which is the point of it, but that also puts it under the toys' tap areas,
   so a finger reached the toy instead of the rug. During arranging the three
   layers lift above the spots; the moment you press Done they drop back and
   the room looks exactly as it did. */
body.arranging .decor-layer-box { z-index: 200; }
body.arranging #decor-wall  { z-index: 200; }
body.arranging #decor-under { z-index: 201; }
body.arranging #decor-floor { z-index: 202; }
body.arranging .decor-thing { pointer-events: auto; cursor: grab; filter: drop-shadow(0 0 6px rgba(255, 196, 80, .9)); }
body.arranging .decor-thing.held { cursor: grabbing; filter: drop-shadow(0 0 12px rgba(255, 170, 40, 1)); }
/* **THE BAR SITS AT THE TOP, because the bottom of the room is where things
   go.** On a phone it was floating over the front of the floor, which is
   exactly where the front row of decorations lands, so a finger reaching for a
   rug hit the bar instead. The top of the picture is wall, where far less is
   placed, and it keeps clear of the paw button. */
/* **THE BAR FOUGHT ITSELF, AND IT SAT ON THE ROOM.** Flavia, Aug 14: *"i dont
   like how this looks."* From her screenshot: a two sentence hint wrapping onto
   five lines in a narrow column, with three buttons in a wide row beside it, the
   whole thing a pale blob parked over the top middle of the room. Tall skinny
   text against short wide buttons, in one rounded lump, covering the one thing
   decorate mode exists to let you look at.

   Three changes, and the order matters.

   **The hint is a tutorial line, not a control, so it leaves.** It appears when
   arranging starts and fades once the first drag lands, or after a few seconds
   if nothing is dragged. Nobody needs telling how to drag on their fortieth
   visit, and a permanent instruction is what forced the two column shape in the
   first place. Chat: *"If only one of these gets done, do number 2."*

   **The words are half as long.** "Drag a thing to move it. Press and hold to
   put it away." became "Drag to move. Hold to put away." Same meaning, one line.

   **And it moves off the room to the bottom edge.** Anchored bottom right rather
   than top centre: the room is looked at from the middle outward, the three
   buttons are reached with a thumb, and the floor at the very bottom edge is the
   part of the picture with the least in it. `right` rather than centred so it
   never sits over the thing being dragged. */
#decor-bar {
  position: fixed;
  right: max(12px, env(safe-area-inset-right));
  bottom: max(12px, env(safe-area-inset-bottom));
  left: auto; top: auto; transform: none;
  display: flex; align-items: center; gap: 8px;
  background: #fffaf0; border: 2px solid #cbb08a; border-radius: 999px;
  padding: 6px 8px; z-index: 80;
  box-shadow: 0 6px 18px rgba(80, 55, 25, .3);
}
#decor-bar.hidden { display: none; }

/* The hint is its own floating line now, above the buttons rather than inside
   them, so its width never shapes the bar. It fades rather than vanishing,
   because something disappearing on a hard cut reads as a glitch. */
#decor-hint {
  position: fixed;
  left: 50%; transform: translateX(-50%);
  bottom: calc(max(12px, env(safe-area-inset-bottom)) + 52px);
  max-width: min(92vw, 420px);
  text-align: center;
  white-space: nowrap;
  /* **IT WAS THE ONLY DARK THING IN THE GAME.** Flavia caught it mid fade and
     asked if it was a bug. It was not, but a brown slab with pale writing on it
     belongs to a different game: every other panel here is cream with brown
     writing and a soft tan edge, and the bar it sits above is exactly that. So
     it wears the same clothes, quieter than the buttons because it is a whisper
     rather than a control. */
  background: rgba(255, 250, 240, .94);
  color: #6b4d2e;
  border: 2px solid #e0cba6;
  font-size: 12px;
  padding: 5px 14px;
  border-radius: 999px;
  box-shadow: 0 3px 10px rgba(80, 55, 25, .18);
  z-index: 80;
  pointer-events: none;
  transition: opacity .5s ease;

  /* **`class="hidden"` DID NOTHING HERE AND THE PILL SAT OVER THE ROOM FROM THE
     MOMENT THE PAGE LOADED.** Flavia: *"that drag to move message stays even
     after i close the decors and everything."* It was never staying, it had
     never left: there is no generic `.hidden` rule in this stylesheet. Every one
     of them names its own element, `#music.hidden`, `.modal.hidden`,
     `#decor-bar.hidden`. I wrote the class onto the markup and never wrote the
     rule, so a permanent instruction floated over the game until the first time
     somebody opened Decorate and its timer happened to take it away.

     So it is hidden by DEFAULT and shown by an explicit class, which is the way
     round that fails safe: forget to add the class and nothing appears, rather
     than forget to add a rule and everything does. */
  display: none;
  opacity: 0;
}
#decor-hint.showing { display: block; opacity: 1; }
#decor-hint.showing.gone { opacity: 0; }

/* **AND IT CANNOT OUTLIVE ARRANGING, whatever the script does.** Belt and
   braces on top of the class: the hint explains how to drag things, so outside
   decorate mode there is no state in which it should be on screen. Leaving that
   to a function call means one missed path and it is stuck there again. */
body:not(.arranging) #decor-hint { display: none; }
#decor-hint { font-size: 12px; color: #6b5c48; max-width: 46vw; }

/* the rest of the collection, shown but not for sale here */
.decor-elsewhere-title {
  margin: 22px 0 2px; font-size: 15px;
  border-top: 2px dashed #e0cba6; padding-top: 16px;
}
/* **NO LONGER FADED.** `away` used to mean "shown but not for sale here", so it
   was greyed. Now it only means "came from another place", and a piece you can
   buy must not look like one you cannot. The little line under the price says
   where it is from, and that is enough. */
.decor-card.away { opacity: 1; }

/* the little count under a milestone badge: quiet on purpose, and never a
   tap target, so the badge stays a full finger wide */
.map-progress {
  position: absolute; left: 50%; top: 100%;
  transform: translate(-50%, 2px);
  font-size: 10px; line-height: 1;
  padding: 2px 6px; border-radius: 999px;
  background: rgba(255, 255, 255, .86); color: #6b6259;
  white-space: nowrap; pointer-events: none;
}

/* ---------- the corner X ----------
   Flavia, Aug 12: *"to the decor list we need an X to close it so we dont need
   to scroll down to close it."* She is right, and it is not only the decorations:
   any list long enough to scroll buries its own Close button, and the decoration
   shelf is the longest in the game at 78 items.

   So the X sits in the corner of every long list. The button at the bottom
   stays, because it says what it does in words and some people look for that.
   This is the one you can always reach with the thumb already holding the phone.

   44 square, like everything else you touch. */
.corner-x {
  position: sticky;
  top: 0;
  float: right;
  z-index: 5;
  width: 44px;
  height: 44px;
  margin: -6px -6px 0 0;
  border: 2px solid #e0d2b6;
  border-radius: 50%;
  background: #fffaf1;
  color: #7a5b34;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(80, 55, 25, .18);
}
.corner-x:active { transform: scale(.94); }

/* ---------- the map on a short screen ----------
   Stacking a map above a panel cannot work on a phone held sideways. The screen
   is 384 tall, the panel needs 259 of it, and what is left cannot hold a map
   with twenty tappable places on it. Measured: the picture collapsed to 50
   points and then to 111, and the badges came out 26 across where a finger
   needs 44.

   **A landscape phone is wide and short, so the map goes BESIDE the panel
   rather than above it.** That is what the shape of the screen is asking for.
   The map then gets the full height and the badges grow with it.

   Only below 520 points tall, so a tablet and a laptop keep the stacked layout
   they already look right in. */
@media (max-height: 520px) {
  #map .modal-box {
    display: flex;
    flex-direction: row;
    align-items: stretch;
    gap: 12px;
    max-width: 96vw;
    width: 96vw;
  }
  .map-frame {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    align-items: center;
  }
  /* **THIS RULE IS FOR THE MAP DRAWING AND IT WAS EATING THE PETS.** Written to
     make the map fill the height on a short screen, `.map-canvas img` reaches
     every image in the canvas, and the friends walking the paths are images in
     the canvas. It beats `.map-pet` on specificity, one class plus an element
     against one class alone, so a cat that should be 3.25% of the width, about
     15px, was drawn at 257: the FULL height of the map. Reproduced live, both
     royal_cat and golden_dog at 257x257.

     Excluded by name rather than by nesting. `.map-canvas > img` would also work
     today but only while the pets stay exactly one level down, and nothing here
     should quietly depend on that. `.map-pet` then keeps its own 3.25% width and
     takes its natural height. Measured after: the drawing is still 458x257 and
     the pets are 15x15. */
  .map-canvas img:not(.map-pet) { max-height: none; height: 100%; width: auto; max-width: none; }
  #map-panel {
    flex: 0 0 clamp(150px, 26%, 230px);
    justify-content: center;
    overflow-y: auto;
  }
  #map-panel-line { display: none; }   /* the flavour line is the first thing to go */

  /* **AND THE BADGES GROW.** They are 5.75% of the map, which is generous on a
     laptop and a fingernail on a phone. The map is sparse enough to carry
     bigger ones without them touching.

     The news dot's box grows with them, or the dot lands in the middle of a
     picture instead of on its shoulder. */
  .map-mark,
  .map-news { width: 10.5%; }

  /* and give the map the last of the height: on a phone the modal's own padding
     is spending points the badges need. 41 across became 48. */
  #map .modal-box { padding: 8px; max-height: 96vh; }
  #map h2, #map .subtle:not(#map-panel-open) { display: none; }
}

/* where a decoration came from, now a note rather than a rule */
.decor-from { display: block; font-size: 10.5px; opacity: .6; margin-top: 1px; }

/* ============================================================
   SLIDING THE ROOM ON A COMPUTER, WITH THE BARS ON SHOW
   ============================================================
   Flavia, Aug 12: *"can you make the website scrollable too? i mean to show the
   scrolling bars both ways."*

   **THERE WAS ALREADY SOMETHING TO REACH, AND NO WAY TO REACH IT.** The rooms
   now COVER the screen rather than fit inside it, so a 21:9 room on a 16:9
   monitor hangs over both edges. On a phone that was fine, a finger slides it.
   On a computer `#stage` was `overflow: hidden`, so the trimmed part was simply
   gone, with nothing on screen admitting it existed.

   **AND THE BARS HAVE TO BE FORCED.** A Mac hides scrollbars until you scroll,
   which is the whole reason she cannot tell there is more room. Giving
   ::-webkit-scrollbar an explicit width opts out of the overlay behaviour and
   the bars stay put, which is what she asked for. They are styled in the game's
   own wood and cream rather than left grey. */
/* Any screen held sideways, phone included, not just anything wide. */
@media (orientation: landscape), (min-width: 761px) {
  #stage {
    /* **SIDEWAYS ONLY.** Flavia, Aug 12: *"leave the scroll only left and right
       no up and down."* The cover sizing overflows on whichever axis is spare,
       and on a normal wide monitor that is the width, so up and down was an
       empty bar that did nothing but sit there. */
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;          /* a mouse gets one back, just below */
    -webkit-overflow-scrolling: touch;
    /* **THE PICTURE PULLING AWAY FROM THE EDGE WAS THE RUBBER BAND.** Flavia,
       Aug 12: *"when i scroll left the photo gets out of the frame."* A phone
       held sideways is about 844 wide, so it never matched the `max-width: 760`
       rule where this already was, and dragging past the left end bounced the
       room away from the edge and showed what was behind it. A tablet hid it
       because the room overhangs far less there, which is why that one looked
       perfect.

       **AND IT HAS TO BE `none`, NOT `contain`.** They are not the same thing
       and the difference is exactly this bug. `contain` stops the scroll
       CHAINING to whatever is behind, which was never the complaint. It still
       permits the local bounce, which is the picture leaving the edge. `none`
       stops both. Proved the pan itself is innocent first: asked the browser to
       scroll to -500 and got 0 back, asked for 99999 and got exactly the 63px
       maximum. Native scrolling clamps to its content by construction, so
       nothing needed fencing. It was only ever the bounce. */
    overscroll-behavior: none;
    display: flex;
    /* **THE ROOM SITS ON THE FLOOR, IT DOES NOT FLOAT IN THE MIDDLE.** Drawn at
       1.4 times the screen height, 28.6% of the room is off screen. Centred,
       that is 14% taken off the top and 14% off the BOTTOM, and the bottom is
       where everything lives: the front row of toys at 96% and both food bowls
       with it. Measured, centring loses all three. Anchored to the bottom the
       same crop takes 28.6% off the ceiling, where there is wall and nothing
       else, and every spot survives. */
    align-items: flex-end;
    justify-content: safe center;   /* `safe` keeps the left edge reachable */
  }
  #yard {
    position: relative;
    top: auto;
    left: auto;
    transform: none;
    flex: 0 0 auto;
    /* `max(100vw, ...)` came 2px short at 812 wide, because the height it
       derives from is fractional and rounds down. Two pixels is a hairline, not
       the bars she saw, but a cover that is sometimes 2px short is not a cover.
       100% of the stage is exact by definition, whatever rounds where. */
    min-width: 100%;
  }
}

/* **THE BAR IS FOR A MOUSE, NOT FOR A THUMB.** A phone held sideways is about
   844 wide, so it lands in the rule above with every desktop. Giving
   ::-webkit-scrollbar a height opts out of overlay scrollbars, which on a phone
   means a permanent 12px strip taken out of the room and a bar nobody asked for
   under a game that is meant to be seamless. `pointer: fine` is the honest test:
   it asks whether there is a mouse, rather than guessing from the width. */
@media (min-width: 761px) and (pointer: fine) {
  #stage { scrollbar-width: auto; scrollbar-color: #b08d57 #cbb38d; }
  #stage::-webkit-scrollbar { height: 12px; }
  #stage::-webkit-scrollbar-track {
    background: #cbb38d;
  }
  #stage::-webkit-scrollbar-thumb {
    background: #b08d57;
    border-radius: 8px;
    border: 2px solid rgba(70, 48, 22, .10);
  }
  #stage::-webkit-scrollbar-thumb:hover { background: #9c7a45; }
}

/* The scrollbar lift that used to live here is gone: the paw counters moved to
   the top right on Aug 12, so nothing of ours sits on the bottom bar any more. */

/* ============================================================
   THE SHELF ON A SMALL PHONE
   ============================================================
   Measured at 426x320, which Android's own documentation gives as the smallest
   screen it supports: the decoration shelf showed **between two and three of its
   78 pieces at a time**. Nothing was off screen and every tap target was already
   over 44px, so none of the usual checks complained. It was simply a very long
   corridor to walk down on a small phone.

   `auto-fill` with a 116px minimum lands just short of three columns: three need
   368px of room and a 426 wide phone leaves about 360. Twelve pixels decided it.
   Asking for 96px lets three fit, which makes each card about 113px, still far
   over the 44px a finger needs, and roughly doubles what she sees at once.

   Keyed on HEIGHT, not width, because the game is landscape and a short screen
   is what makes a vertical list painful.

   **AND IT IS EVERY PHONE, NOT ONLY THE SMALLEST.** Flavia, Aug 12, after seeing
   it: *"make smaller boxes even for the other mobile sizes, it looks better."*
   500px catches every phone held sideways, which run from about 320 to 430 tall,
   and stops short of a tablet, which is 768 and up and has room to spare. */
@media (max-height: 500px) {
  /* **COMPACT, THE WAY A PHONE APP IS COMPACT.** Flavia, Aug 12: *"a smaller
     menu tile, more compact apple style."* 96px still gave only two columns on
     the floor device, because three of them plus the gaps came to more than the
     292px the panel actually leaves. 78 fits three there, six on a 640 phone and
     eight on an 844 one, and the tile stays 95px at its smallest, more than
     double the 44px a finger needs.

     The chrome comes down with it. A tile this size cannot carry a 2px border, a
     14px corner and a 3px shadow without looking like a button from 2011. One
     hairline, a tighter corner, and the picture gets the room instead. */
  .decor-row { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 6px; }
  .decor-card {
    padding: 4px;
    border-radius: 10px;
    border-width: 1px;
    box-shadow: 0 1px 4px rgba(80, 55, 25, .12);
  }
  .decor-card b { font-size: 10px; line-height: 1.15; }
  .decor-price { font-size: 9px; }
}

/* A piece already standing in the room, so the shelf shows at a glance what is
   out and what is still in the drawer. Deliberately quiet: a soft ring rather
   than a badge, because the shelf is for browsing and a row of loud markers
   would fight the pictures. */
.decor-card.placed {
  border-color: #b08d57;
  box-shadow: 0 0 0 2px rgba(176, 141, 87, .35), 0 1px 4px rgba(80, 55, 25, .12);
}
.decor-card.placed .decor-price { color: #7a5c1f; font-weight: 600; }

/* ?shadow=tight  the oval tucks under the base instead of sitting below it */
body.shadow-tight { --shadow-drop: 0.02; }

/* **A DECORATION SITS ON ITS DRAWN BASE, NOT ON THE BOTTOM OF ITS FILE.**
   Every decoration picture carries transparent space below the artwork, from 2%
   to 35% of its height, so anchoring the FILE to the floor left the drawing
   hovering above it by that much. The ART table has always measured this as
   `pad`; sprites used it and decorations did not. decorate.js writes the piece's
   own pad into --decor-drop and this pushes it down by exactly that share of its
   height, so the drawn base lands on the floor line. */
.decor-thing.sits-on-its-feet {
  transform: translate(-50%, calc(-100% + var(--decor-drop, 0%)));
}

/* ============================================================
   THE WANDERERS
   ============================================================
   A cat walking through the room. See wanderers.js for why it exists and for
   the rules of calm that keep it from becoming a screensaver.

   **THEY TAKE NO TAPS.** `pointer-events: none` until Flavia decides what a
   tapped wanderer does, a little Silver or only a sound. Until then a finger
   passes straight through to whatever is behind, which is the safe default: a
   wanderer stealing a stroke meant for a pet she has collected would be a real
   loss, and doing nothing is merely nothing. */
#wander-layer {
  position: absolute;
  inset: 0;
  /* **A TAPPED WANDERER PAYS NOW, so it has to be tappable.** Flavia decided
     Aug 12: a little Silver and a soft sound. The LAYER stays transparent to
     touches and only the cat itself takes them, so a finger on empty floor still
     reaches whatever is behind. */
  pointer-events: none;
  z-index: auto;
}
.wanderer {
  position: absolute;
  /* stands on its point, the same rule every other thing in the yard follows */
  transform: translate(-50%, -100%);
  /* **72, NOT 96. Flavia's number, Aug 13.** Measured out of this stylesheet:
     a pet she owns is 84 sitting and 104 playing, and a wanderer was 96. A cat
     passing through was drawn BIGGER than a cat she had collected and placed.
     At 72 it reads as further away and never competes with the pets that are
     hers. */
  /* The default, for a film with no render width recorded. wanderers.js
     overrides this per film so a wide pose draws wide. See filmWidth there. */
  width: calc(72px * var(--u));
  pointer-events: auto;
  cursor: pointer;
  transition: opacity 320ms ease, transform 320ms ease;
}
.wanderer img { display: block; width: 100%; height: auto; }

/* **NEVER HARD CUT ONE IN.** An instant appearance reads as a glitch. It arrives
   at 92% and settles to full while fading in, which is the difference between
   arrived and pasted. The walk that used to do this job was deleted on Aug 14:
   a still picture sliding sideways reads as flying. */
.wanderer.arriving {
  opacity: 0;
  transform: translate(-50%, -100%) scale(0.92);
}
.wanderer.leaving {
  opacity: 0;
  transform: translate(-50%, -100%) scale(0.96);
}

/* Somebody who has asked for stillness gets a still room. */
/* **THE CAT NOTICES YOU.** A tap replays its clip and lifts it a touch, so the
   touch has a consequence now that it no longer has a payment. Small on purpose:
   a big bounce would read as a button being pressed, and this is a cat. */
.wanderer.noticed { transform: translate(-50%, -100%) scale(1.06); }
.wanderer.noticed.sits-on-its-feet { transform: translate(-50%, -100%) scale(1.06); }

body.wants-stillness .wanderer { transition: none; }
body.wants-stillness .wanderer.noticed { transform: translate(-50%, -100%); }

/* **A WANDERER STANDS IN FRONT OF THE FURNITURE, NEVER INSIDE IT.** Flavia sent
   a photograph of a coon with a ball track drawn across its face. Position
   avoidance handles most of it, but a busy room can genuinely run out of clear
   floor, and when a cat does end up near something the difference between "in
   front of" and "impaled by" is only drawing order. Above the toys and the
   decorations, below the paper cards and the menus. */
#wander-layer { z-index: auto; }

/* The decoration's own line, under its price. See decorate.js. */
.decor-flavor {
  font-size: 9px;
  line-height: 1.25;
  opacity: .72;
  font-style: italic;
  text-align: center;
}

/* ---------- the keepsake shelf ---------- */
/* Deliberately plain. Chat: "No art needed to ship it. The names and lines carry
   it, and the shelf can be words on a shelf." So it is words on a shelf, and if
   it earns its keep the twenty get drawn later and slot straight in. */
.shelf-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 8px;
  margin-top: 10px;
}
.shelf-thing {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 8px 10px;
  border-radius: 12px;
  background: #fffaf0;
  border: 2px solid #e0cba6;
  box-shadow: 0 3px 9px rgba(80, 55, 25, .14);
}
.shelf-thing b { font-size: 13px; line-height: 1.2; }
/* The cat's name is the warm part, so it gets the colour rather than the item */
.shelf-from { font-size: 11px; color: #8a6d1a; }
.shelf-line { font-size: 12px; opacity: .78; font-style: italic; }
@media (max-height: 500px) {
  .shelf-list { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
}

/* ---------- the language picker ---------- */
/* **IT WENT OUT UNSTYLED AND LOOKED IT.** Flavia sent a photograph of Settings
   where the five language buttons sat as bare browser buttons, boxed and
   crammed onto two lines, directly under the Zen and Modern pair which are
   properly dressed. Building the control and not dressing it is only half of
   building it.

   They wrap onto as many lines as they need, because five names of different
   lengths in two writing systems will never fit one row on a phone, and forcing
   them to would shrink 日本語 to nothing. */
/* **AN EVEN GRID, NOT A RAGGED WRAP.** As a flex row the five names wrapped
   four and then one, because they are all different lengths, and a lone 日本語
   sitting under a full row looks like a mistake rather than a choice. A grid
   gives every language the same width whatever its script, which is also the
   fairer thing: no language looks like an afterthought. */
#language-buttons {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 8px;
  margin: 10px 0 2px;
}
#language-buttons button {
  justify-content: center;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  /* **44px IS THE RULE AND THESE MEASURED 43.** The one failure in the phone
     size pass at 667x375, one pixel under the tap target minimum. min-height
     rather than more padding, so the text sits where it always did. */
  min-height: 44px;
  align-items: center;
  padding: 8px 14px;
  border-radius: 999px;
  border: 2px solid #e0cba6;
  background: #fffaf0;
  color: #6b4d2e;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
/* The one you are reading right now, marked the way the room buttons mark theirs */
#language-buttons button.chosen {
  background: #f6d68a;
  border-color: #d9b45c;
}
/* "40% done" is a caveat, not a label. It whispers. */
/* stacked under the name rather than beside it, so a long caveat cannot make
   one button wider than its neighbours */
#language-buttons button { flex-direction: column; gap: 1px; align-items: center; padding: 10px 12px; }
#language-buttons button .flavor {
  font-size: 11px;
  font-weight: 400;
  font-style: italic;
  opacity: .7;
}

/* **A LAYER WITH A z-index TRAPS EVERYTHING INSIDE IT.** Flavia, Aug 21:
   *"i still have coons that overlap the goodies. mostly are behind it."*
   She was right and the cause was here, not in the placement.

   `#wander-layer` carried `z-index: 6`, which makes a stacking context. Every
   cat inside it then paints at 6, BELOW every toy at 90, however large a number
   the cat itself is given. I had given each cat a depth of its own days ago and
   verified it by comparing those numbers, which is exactly the wrong thing to
   measure: the numbers were right and never reached the screen. The same was
   true of the three decoration boxes at 1, 2 and 3.

   So the boxes stop competing and their children compete instead, in the room's
   own stacking context, using the room's own rule of floor line plus ten. A wall
   decoration and a rug keep fixed low numbers, because those belong behind
   whatever their position. */
.decor-thing { z-index: 4; }                 /* a floor piece is overridden per item */
#decor-wall  .decor-thing { z-index: 1; }   /* a picture on the wall is always behind */
#decor-under .decor-thing { z-index: 2; }   /* a rug is meant to be stood on */

/* ---------- the Paw Shop ----------
   **A CLASS WITH NO RULE HIDES NOTHING.** `.quiet` existed only as
   `.secondary-btn.quiet`, so a preview row carrying it would have looked
   exactly like a row you can buy from, which is the one thing this panel must
   never do. The rule below is what actually makes it read as inactive. */
.pouch-preview { margin-top: 4px; }
/* .55 washed the new pouch drawings out almost completely. The row still has
   to read as "not buyable here", but the pictures are the point of the panel,
   so the words carry the dimming and the drawing stays legible. */
.row.quiet { opacity: .82; }
.row.quiet .row-words { opacity: .7; }
.row.quiet .row-words b { color: #6b5027; }
.row.quiet::after {
  /* nothing to press, and it should look like nothing to press */
  content: "";
  position: absolute;
  inset: 0;
  cursor: default;
}

/* ---------- the back arrow, beside the close button ----------
   Same shape and size as `.corner-x` on purpose: they are a pair, they sit in
   the same strip, and a child should not have to work out that one round button
   means close and a different looking one means back. It floats LEFT where the
   X floats right, so the two take opposite corners of the same line and neither
   needs to know the other exists. 44px, which is the tap target rule. */
.corner-back {
  position: sticky;
  top: 0;
  float: left;
  z-index: 5;
  /* **SMALLER THAN THE X, AND BOLDER.** Flavia: *"make it a bit smaller to fit
     the design and bold"*. The X is the loud one because leaving is the bigger
     action; going back is a smaller step and should look like one. 38px is as
     small as this can go: the tap target rule is 44, so the button keeps 44 of
     real touch area through the padding below while drawing at 38. */
  width: 38px;
  height: 38px;
  padding: 0;
  margin: -3px 0 0 -3px;
  border: 2px solid #e0d2b6;
  border-radius: 50%;
  background: #fffaf1;
  color: #7a5b34;
  font-size: 19px;
  font-weight: 800;
  line-height: 1;
  cursor: pointer;
}
/* the drawn circle is 38, the thing a thumb can hit is still 44 */
.corner-back::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.corner-back:active { transform: scale(.94); }

/* A panel with no X of its own still gets an arrow, and without this its first
   real element would wrap around the float instead of clearing it. */
.modal-box > .corner-back + * { clear: none; }

/* the first-basket offer line: quieter than the name, warmer than the flavour,
   and it never carries a number of its own. See store.js for why. */
.pouch-offer {
  font-size: 12px;
  font-weight: 700;
  color: #a3712a;
}

/* The sleeping newborn's colour used to be a CSS tint applied here. It shipped
   in v268 and was removed the same day, when chat delivered six real cocoons:
   multiplying a colour over a drawing that is already blue or mint turns it
   muddy. See newborn_look.js. */
}

/* ---------- a trace, something left behind ----------
   Small and quiet. It is not a reward and must not look like one: no glow, no
   bounce, no badge. It sits on the floor and waits to be noticed. 44px of tap
   area around a much smaller picture, because it IS small and a thumb is not. */
.trace {
  position: absolute;
  width: 44px;
  height: 44px;
  margin: -22px 0 0 -22px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.trace img {
  max-width: 30px;
  max-height: 30px;
  object-fit: contain;
  pointer-events: none;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,.18));
}
.trace:active img { transform: scale(.92); }

/* the return greeting: the warmest line in the panel, slightly larger than the
   subtle line under it and never a heading */
.back-greeting { font-size: 16px; font-weight: 600; color: #5d451f; margin: 2px 0 6px; }

/* Bao's crime scene: the paw print and the coins by the emptied bowl. It sits
   exactly on the bowl, so the tap that fixes it is the tap you would make
   anyway, and the whole thing is one 44px target. */
.bao-scene {
  position: absolute;
  width: 44px; height: 44px;
  margin: -22px 0 0 -22px;
  background: none; border: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  /* above the bowl's 90: `auto` loses to every number, which is why two thirds
     of him was painted behind the food bowl */
  z-index: 95;
}
/* **BAO AT VISITOR SIZE, NOT ICON SIZE.** He is the scene; the paw print and
   the coin are the footnote. The cat arrives with the game's own fade. */
/* **THE FADE ANIMATES OPACITY ONLY.** It used to animate transform, which is
   the same property the placement uses, and an animation with fill:both wins:
   the clamp computed in bao.js would have been silently thrown away the moment
   the fade finished. Position belongs to the code that can measure the screen. */
.bao-scene .bao-cat {
  width: 84px; height: 84px; object-fit: contain;
  display: block; margin: 0 auto;
  filter: drop-shadow(0 6px 8px rgba(60, 40, 20, .28));
  animation: baoArrive .45s ease-out both;
  position: relative; z-index: 2;
}
@keyframes baoArrive { from { opacity: 0 } to { opacity: 1 } }
@media (prefers-reduced-motion: reduce) { .bao-scene .bao-cat { animation: none; } }
.bao-scene.bao-leaving .bao-cat { opacity: 0; transition: opacity .28s ease; }
.bao-scene .bao-marks { position: absolute; left: 50%; bottom: -2px; transform: translateX(-50%);
  display: flex; gap: 2px; align-items: flex-end; pointer-events: none; }
.bao-scene .bao-print { font-size: 16px; transform: rotate(-18deg); }
.bao-scene .bao-gold  { font-size: 13px; }
@media (max-height: 470px) { .bao-scene .bao-cat { width: 72px; height: 72px; transform: translateY(-18px); } }

/* the friend hint: the same accent the shop uses for gold prices, with the
   star carrying the meaning so colour never has to. Distinct, not shouting. */
.decor-friend { font-size: 11px; color: #8a6d1a; font-weight: 600; }
p.decor-friend { font-size: 13px; margin: 2px 0 6px; }

/* the price report, shown only when the app's shelf came back empty: technical
   on purpose, it exists to be photographed and sent to Code */
.pouch-diag { font: 11px/1.5 monospace; background: #f6f1e6; border-radius: 8px;
              padding: 8px 10px; white-space: pre-wrap; color: #5d451f; }



/* ---------- the review bubble, the pet that just left is saying it ----------
   Flavia, Aug 23: "we need a better design for the review notification, a cute
   bubble". The first version used class names that do not exist in this file,
   so there was no card at all: a heart and two default browser buttons over
   the room. Chat's design, game tokens only. */
/* **:not(.hidden) IS LOAD BEARING.** `#review-ask.modal` is an id plus a
   class and outranks the game's own `.hidden{display:none}`, which is a
   single class, so the card carried `hidden` and still computed
   display:flex: an invisible full screen layer over the whole game with
   pointer events on, swallowing every tap on the room. Measured live
   before this fix: classes 'modal hidden', display flex, 667x288, and a
   tap in the middle of the room hit the paper box behind nothing.
   Any rule shaped #someid.modal has the same trap. */
#review-ask.modal:not(.hidden){display:flex;align-items:center;justify-content:center;background:rgba(58,42,30,.42);backdrop-filter:blur(2px);}
#review-ask .ra-wrap{position:relative;display:flex;flex-direction:column;align-items:center;animation:raPop .32s cubic-bezier(.2,1.3,.5,1) both;}
@keyframes raPop{from{opacity:0;transform:translateY(14px) scale(.92)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){#review-ask .ra-wrap{animation:none}}
#review-ask .ra-bubble{position:relative;width:min(84vw,332px);background:#FFFAF1;border:3px solid #EBD9BC;border-radius:30px;padding:20px 22px 16px;box-shadow:0 16px 36px rgba(0,0,0,.26);text-align:center;color:#5B4636;}
#review-ask .ra-bubble:after{content:"";position:absolute;left:50%;bottom:-15px;width:24px;height:24px;background:#FFFAF1;border-right:3px solid #EBD9BC;border-bottom:3px solid #EBD9BC;border-bottom-right-radius:7px;transform:translateX(-50%) rotate(45deg);}
#review-ask .ra-hearts{font-size:20px;line-height:1;margin-bottom:4px;}
#review-ask .ra-title{font-size:18px;font-weight:800;margin:0 0 6px;line-height:1.25;text-wrap:balance;}
#review-ask .ra-text{font-size:14px;line-height:1.45;margin:0 0 14px;color:#7A6350;text-wrap:balance;}
#review-ask .ra-yes{display:block;width:100%;min-height:46px;border-radius:14px;border:2px solid #DDB362;background:linear-gradient(#FAE2AE,#F4CE87);color:#6B4E21;font-weight:800;font-size:15.5px;cursor:pointer;box-shadow:0 3px 0 #DDB362;}
#review-ask .ra-yes:active{transform:translateY(2px);box-shadow:0 1px 0 #DDB362;}
#review-ask .ra-no{display:block;width:100%;min-height:40px;margin-top:6px;border:0;background:none;color:#9C866F;font-weight:700;font-size:13.5px;cursor:pointer;}
#review-ask .ra-pet{width:136px;height:136px;margin-top:12px;object-fit:contain;filter:drop-shadow(0 10px 12px rgba(0,0,0,.20));}
#review-ask .ra-spark{position:absolute;font-size:15px;opacity:.85;animation:raFloat 3.2s ease-in-out infinite;}
#review-ask .ra-spark.a{left:-14px;top:26px;}
#review-ask .ra-spark.b{right:-12px;top:64px;animation-delay:.9s;font-size:12px;}
@keyframes raFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}

/* **AND IT HAS TO FIT A PHONE HELD SIDEWAYS.** The design above is lovely on a
   tall screen and overflowed the canonical 667x375 landscape reference: the
   title was cut off the top and Bao off the bottom, measured on the live site.
   Kotomi is mobile-horizontal first, so on short screens everything shrinks
   together rather than the card being scrolled or clipped. Same drawing, same
   colours, same 44px tap rule: the yes button stays 44, the no button 40. */
@media (max-height: 470px) {
  #review-ask .ra-bubble { width: min(78vw, 300px); padding: 10px 16px 8px; border-radius: 22px; }
  #review-ask .ra-hearts { font-size: 15px; margin-bottom: 2px; }
  #review-ask .ra-title { font-size: 15px; margin: 0 0 4px; }
  #review-ask .ra-text { font-size: 12.5px; line-height: 1.35; margin: 0 0 7px; }
  #review-ask .ra-yes { min-height: 44px; font-size: 14.5px; border-radius: 12px; }
  #review-ask .ra-no { min-height: 40px; margin-top: 1px; font-size: 12.5px; }
  #review-ask .ra-pet { width: 60px; height: 60px; margin-top: 4px; }
  #review-ask .ra-bubble:after { bottom: -11px; width: 18px; height: 18px; }
  #review-ask .ra-spark { display: none; }
}

/* **NOBODY MAY EVER BE TRAPPED BY THIS CARD.** Flavia, Aug 23: "the review
   button is stuck on my tablet screen and i cant do anything". On a screen
   shorter than the card, the buttons sat below the edge with nothing to tap.
   Two guarantees now, belt and braces: the whole thing can scroll inside the
   screen, and tapping the dark area around it closes it. */
#review-ask.modal:not(.hidden) { overflow: auto; padding: 8px; }
#review-ask .ra-wrap { max-height: 100%; }

/* ---------- Today at the stall ----------
   **THREE TILES, ALL THREE VISIBLE, NO SCROLLING TO FIND THE THIRD.** The list
   below uses wide cards, picture beside a name, a price and a flavour line,
   which is right for reading a catalogue and wrong for a shop window: two of
   them filled a sideways phone and the third was off the edge. Up here the
   tile turns upright and drops the sentence, so the DRAWING is what sells it,
   the three share the width evenly, and a long name shortens with dots rather
   than growing the tile. */
#decor-stall { margin: 4px 0 10px; }
#decor-stall .stall-head { margin: 0; font-weight: 800; color: #5B4636; font-size: 15px; }
#decor-stall .stall-sub { margin: 1px 0 6px; color: #9C866F; font-size: 12.5px; }
#decor-stall .stall-row { display: flex; gap: 8px; align-items: stretch; }
#decor-stall .decor-card {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  flex-direction: column;
  justify-content: flex-start;
  text-align: center;
  padding: 8px 6px 7px;
  gap: 3px;
}
#decor-stall .decor-card img { width: 62px; height: 62px; }
#decor-stall .decor-words { align-items: center; text-align: center; width: 100%; gap: 1px; }
#decor-stall .decor-card b {
  font-size: 12px; text-align: center; width: 100%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#decor-stall .decor-card .decor-price { font-size: 11.5px; font-weight: 700; }
/* the sentence and the friend star belong to the catalogue below, not here */
#decor-stall .decor-flavor, #decor-stall .decor-friend { display: none; }

/* on a phone held sideways the whole strip tightens rather than pushing the
   list off the bottom of the panel */
@media (max-height: 470px) {
  #decor-stall { margin: 2px 0 7px; }
  #decor-stall .stall-head { font-size: 13.5px; }
  #decor-stall .stall-sub { font-size: 11.5px; margin-bottom: 4px; }
  #decor-stall .decor-card { padding: 6px 5px 5px; }
  #decor-stall .decor-card img { width: 52px; height: 52px; }
  #decor-stall .decor-card b { font-size: 11.5px; }
  #decor-stall .decor-card .decor-price { font-size: 11px; }
}

/* the hours an animal keeps, on its own page: quieter than its story line,
   because it is a habit and not a headline */
.petpage-habit { margin: 2px 0 0; font-size: 12.5px; color: #8a6d1a; font-style: italic; }

/* Bao's payout card: the review bubble's shell, with his picture on top and
   the two amounts under the words */
#bao-payout.modal:not(.hidden) { display: flex; align-items: center; justify-content: center;
  background: rgba(58, 42, 30, .42); backdrop-filter: blur(2px); overflow: auto; padding: 8px; }
#bao-payout .ra-bubble { position: relative; width: min(84vw, 320px); background: #FFFAF1;
  border: 3px solid #EBD9BC; border-radius: 28px; padding: 14px 20px 14px;
  box-shadow: 0 16px 36px rgba(0,0,0,.26); text-align: center; color: #5B4636; }
#bao-payout .bao-payout-cat { width: 84px; height: 84px; object-fit: contain; display: block; margin: 0 auto 4px; }
#bao-payout .bao-payout-line { margin: 2px 0; font-weight: 800; font-size: 15px; color: #6B4E21;
  display: flex; align-items: center; justify-content: center; gap: 6px; }
#bao-payout .bao-paw-icon { width: 22px; height: 22px; object-fit: contain; }
/* **THE BUTTON RULES WERE WRITTEN UNDER THE OTHER CARD'S ID**, so this one
   inherited nothing and measured 19px tall against the game's 44px tap rule.
   Same look, its own selector. */
#bao-payout .ra-yes {
  display: block; width: 100%; min-height: 46px; margin-top: 10px;
  border-radius: 14px; border: 2px solid #DDB362;
  background: linear-gradient(#FAE2AE, #F4CE87); color: #6B4E21;
  font-weight: 800; font-size: 15.5px; cursor: pointer; box-shadow: 0 3px 0 #DDB362;
}
#bao-payout .ra-yes:active { transform: translateY(2px); box-shadow: 0 1px 0 #DDB362; }
#bao-payout .ra-title { font-size: 18px; font-weight: 800; margin: 0 0 6px; }
#bao-payout .ra-text { font-size: 14px; line-height: 1.45; margin: 0 0 10px; color: #7A6350; }
@media (max-height: 470px) {
  #bao-payout .ra-bubble { padding: 10px 16px; border-radius: 22px; }
  #bao-payout .bao-payout-cat { width: 56px; height: 56px; }
  #bao-payout .ra-title { font-size: 15px; margin: 0 0 3px; }
  #bao-payout .ra-text { font-size: 12.5px; margin: 0 0 6px; }
  #bao-payout .bao-payout-line { font-size: 13.5px; }
  #bao-payout .bao-paw-icon { width: 18px; height: 18px; }
  #bao-payout .ra-yes { min-height: 44px; font-size: 14.5px; margin-top: 7px; }
}
