/* =========================================================
   GARDEN WONDER — storybook UI
   Palette, chunky outlines and bouncy motion inspired by
   modern side-scrolling platformers.
   ========================================================= */

:root{
  /* ink + paper */
  /* The Wonder palette, said once. Four layers carry this exact six-colour
     sweep — the Wonder Effect's veil and halo, and the weather veil and takeover
     cue that Wonderfall borrows from them — and a rainbow copied four times is
     four places to forget when one of them changes. */
  --wonder-sweep:#ff6b6b,#ffd43b,#69db7c,#4dabf7,#b197fc,#ff8fab;
  --ink:#2c1a10;
  --ink-2:#5a3a1f;
  --ink-soft:#7a6047;
  --paper:#fff8e7;
  --paper-2:#ffeecd;
  --paper-3:#ffe0ad;

  /* Drained: asleep, out of food, stopped working. The interface's own paper
     with the warmth taken out, because "asleep" is a state and blue/purple/gold
     are a rarity vocabulary a player has already learned. */
  --paper-dim:#ebe5d9;
  --paper-dim-2:#d8d0c0;
  --paper-dim-edge:#bcb0a0;

  /* nature */
  --grass:#5cc45c;
  --grass-d:#3f9d45;
  --grass-l:#8fe08a;
  --soil:#a9713f;
  --soil-d:#7d4f2a;
  --soil-dd:#5f3a1e;

  /* currency */
  /* Saved Seeds — the forever money, and the one new token the Garden Year's
     surface adds. Deliberately not gold (that is coins), not cyan (gems), and
     clear of all three rarity colours: a player has learned blue/purple/gold
     means a tier, and doc 05 forbids borrowing that vocabulary for anything
     else. */
  --seed:#7bd88f;
  --seed-d:#3f9d45;
  --seed-l:#c9f5cf;
  --coin:#ffc93c;
  --coin-d:#f08c00;
  --ticket:#ff8fab;
  --gem:#8ce0ff;

  /* rarity */
  --rare:#4dabf7;
  --epic:#b197fc;
  --legend:#ffd43b;

  /* sky (animated by JS) */
  --sky1:#7ec8f2;
  --sky2:#bfe9ff;
  --sky3:#e9f8ff;
  --star-op:0;
  --sun-x:76%;
  --sun-y:16%;
  --sun-c:#fff3bf;

  --shake-x:0px; --shake-y:0px; --shake-r:0deg;

  /* Safe areas, named once. Every rule that needs an inset reads these rather
     than calling env() itself, because env() cannot be simulated in a desktop
     browser — overriding four numbers on :root is the only way to lay eyes on
     the notched-phone layout without an installed build on a real handset.
     That blind spot is how the dead band under the dock shipped twice. */
  --sat:env(safe-area-inset-top,0px);
  --sar:env(safe-area-inset-right,0px);
  --sab:env(safe-area-inset-bottom,0px);
  --sal:env(safe-area-inset-left,0px);

  /* How far the dock stops short of the physical bottom. The whole bottom inset
     is more room than the home indicator needs under a row of buttons nobody
     swipes from, and spending all of it left a band of dead lawn below the dock
     on every notched phone. Take the inset less the slack, never under 10px so
     a phone with no inset at all still has a margin. */
  --bottom-gap:max(10px, calc(var(--sab) - 12px));

  --r-lg:26px;
  --r-md:18px;
  --r-sm:12px;

  --font:'Baloo 2', ui-rounded, 'SF Pro Rounded', -apple-system, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif;
  --shadow-pop:0 10px 24px rgba(44,26,16,.24);
}

*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}

html,body{
  height:100%;margin:0;padding:0;
  /* The meadow, and deliberately FLAT — the same #4fae54 the lawn ends on, with
     no stripes. Whatever is left uncovered is always at the BOTTOM of the screen,
     where the game draws lawn: an installed PWA on iOS showed a band of sky blue
     under the dock, and iOS also fills the strip below a short web view with this
     colour. It carried the mown stripes for a while, which was worse than useless
     — a repeating gradient starts from its own box, so the page's stripes and the
     meadow's could never line up and the mismatch drew the join as a line. The
     meadow fades its own stripes out before it ends instead, so flat meets flat
     and there is nothing to see.

     It is a variable because iOS paints any strip below a short window with THIS
     colour, so it has to be whatever the bottom of the screen is currently
     showing — lawn normally, the sheet's paper while a sheet is open. */
  background-color:var(--page-fill,#4fae54);
  font-family:var(--font);
  color:var(--ink);
  overflow:hidden;
  overscroll-behavior:none;
  -webkit-user-select:none;user-select:none;
  -webkit-text-size-adjust:100%;
}

button{font-family:inherit;color:inherit;border:0;background:none;padding:0;cursor:pointer;touch-action:manipulation}

/* The keyboard's ring, and it is an `outline` for one reason: every surface in
   this game carries its lip in `box-shadow`, so a ring written that way deletes
   the lip of whatever it lands on — the state-modifier trap doc 05 records,
   arriving on every button at once instead of one component at a time. `outline`
   is a separate property, follows the border radius on its own, and cannot reach
   the stack. `:focus-visible` rather than `:focus` because this is played with a
   thumb: a tap must not leave a ring sitting behind it. Ink one step out from the
   contour the button already wears, so it reads as emphasis and borrows nothing
   from the rarity and currency colours. */
button:focus-visible{
  outline:3px solid var(--ink);
  outline-offset:3px;
}

/* The box the whole game is measured against, and the layout's one hard
   requirement: it has to end exactly at the bottom of the screen. Two attempts
   missed it on an installed iPhone — `inset: 0` alone, then `height: 100dvh` —
   and both were made while this element carried the shake transform, which makes
   it its own containing block and is the case WebKit gets wrong. So the shake
   moved down to `.world` and this is a plain untransformed fixed box again.

   `min-height` rather than `height` is the belt to that brace: the box is the
   TALLER of what the browser says (`inset: 0`) and what JS measured (`--app-h`,
   the largest of innerHeight, clientHeight and, in an installed app, the screen
   itself). Whichever signal is short, the other one still reaches the bottom, and
   a stale or missing measurement can only ever fail by doing nothing. */
.game{
  position:fixed;inset:0;
  min-height:var(--app-h,0px);
  overflow:hidden;
}

/* Everything that shakes. Sits inside the clip so the transform can never drag
   the game's own edge off the screen. */
.world{
  position:absolute;inset:0;
  transform:translate3d(var(--shake-x),var(--shake-y),0) rotate(var(--shake-r));
  will-change:transform;
}

/* Chunky outlined type */
.outlined{
  color:#fff;
  text-shadow:
    -2px 0 var(--ink), 2px 0 var(--ink), 0 -2px var(--ink), 0 2px var(--ink),
    -2px -2px var(--ink), 2px -2px var(--ink), -2px 2px var(--ink), 2px 2px var(--ink),
    0 4px 0 rgba(44,26,16,.35);
}

/* =========================================================
   SCENERY
   ========================================================= */
.scenery{position:absolute;inset:0;overflow:hidden;pointer-events:none}

.sky{
  position:absolute;inset:0;
  background:linear-gradient(180deg,var(--sky1) 0%,var(--sky2) 55%,var(--sky3) 100%);
  transition:background 1.6s linear;
}

.celestial{
  position:absolute;left:var(--sun-x);top:var(--sun-y);
  width:74px;height:74px;margin:-37px 0 0 -37px;
  border-radius:50%;
  background:radial-gradient(circle at 38% 34%, #fff, var(--sun-c) 62%, rgba(255,255,255,0) 72%);
  box-shadow:0 0 60px 24px color-mix(in srgb, var(--sun-c) 55%, transparent);
  transition:left 1.6s linear, top 1.6s linear, background 1.6s linear;
}

.stars{
  position:absolute;inset:0 0 40% 0;opacity:var(--star-op);
  transition:opacity 1.6s linear;
  background-image:
    radial-gradient(1.6px 1.6px at 12% 22%, #fff 99%, transparent),
    radial-gradient(1.4px 1.4px at 28% 12%, #fff 99%, transparent),
    radial-gradient(1.8px 1.8px at 46% 30%, #fff 99%, transparent),
    radial-gradient(1.3px 1.3px at 62% 10%, #fff 99%, transparent),
    radial-gradient(1.7px 1.7px at 78% 26%, #fff 99%, transparent),
    radial-gradient(1.4px 1.4px at 88% 14%, #fff 99%, transparent),
    radial-gradient(1.5px 1.5px at 36% 44%, #fff 99%, transparent),
    radial-gradient(1.2px 1.2px at 68% 40%, #fff 99%, transparent);
  animation:twinkle 4s ease-in-out infinite alternate;
}
@keyframes twinkle{from{filter:brightness(.7)}to{filter:brightness(1.25)}}

.layer{position:absolute;left:0;right:0;pointer-events:none}

/* clouds are generated in JS */
.clouds-far,.clouds-near{top:0;height:30%}
.cloud{
  position:absolute;
  width:var(--w);height:calc(var(--w) * .42);
  background:#fff;border-radius:999px;
  opacity:var(--o,.92);
  filter:drop-shadow(0 4px 0 rgba(120,170,200,.28));
  animation:drift var(--dur) linear infinite;
  animation-delay:var(--delay);
}
.cloud::before,.cloud::after{
  content:'';position:absolute;background:#fff;border-radius:50%;
}
.cloud::before{width:56%;height:110%;left:8%;top:-52%}
.cloud::after{width:44%;height:92%;right:10%;top:-38%}
@keyframes drift{from{transform:translateX(-30vw)}to{transform:translateX(130vw)}}

.hills-far{
  bottom:26%;height:24%;
  background:
    radial-gradient(120% 100% at 18% 100%, #9fd9a0 0 60%, transparent 61%),
    radial-gradient(120% 100% at 62% 100%, #94d59a 0 60%, transparent 61%),
    radial-gradient(120% 100% at 96% 100%, #a6dda6 0 60%, transparent 61%);
  filter:saturate(.8) brightness(1.04);
}
.hills-mid{
  bottom:20%;height:22%;
  background:
    radial-gradient(110% 100% at 8% 100%, #6cc46e 0 62%, transparent 63%),
    radial-gradient(120% 100% at 45% 100%, #63bd68 0 62%, transparent 63%),
    radial-gradient(110% 100% at 84% 100%, #6fc773 0 62%, transparent 63%);
}
.hills-near{
  bottom:14%;height:20%;
  background:
    radial-gradient(120% 100% at 28% 100%, #55b45c 0 62%, transparent 63%),
    radial-gradient(120% 100% at 74% 100%, #4faa55 0 62%, transparent 63%);
}
.meadow{
  bottom:0;height:16%;
  background:
    linear-gradient(180deg,#63c266 0 6px,#4fae54 6px 100%);
  box-shadow:inset 0 6px 0 rgba(255,255,255,.22);
}
/* Mown stripes, faded out over the last stretch of lawn. The fade is the whole
   trick behind the bottom of the screen: below the lawn is flat #4fae54, whether
   that is the page behind a short game box or the strip iOS paints under a short
   web view, and flat can only meet flat invisibly. Anything textured down there
   reads as a cut instead. */
.meadow::after{
  content:'';position:absolute;inset:0;
  background:repeating-linear-gradient(72deg, rgba(255,255,255,.10) 0 6px, transparent 6px 18px);
  mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
  -webkit-mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
}
.fence{
  bottom:12%;height:56px;
  background:
    /* posts */
    repeating-linear-gradient(90deg,
      transparent 0 46px, #d7a973 46px 57px, #b8834f 57px 61px, transparent 61px 92px),
    /* upper rail */
    linear-gradient(180deg, transparent 0 12px, #d7a973 12px 21px, #b8834f 21px 24px, transparent 24px 100%),
    /* lower rail */
    linear-gradient(180deg, transparent 0 34px, #d7a973 34px 43px, #b8834f 43px 46px, transparent 46px 100%);
  opacity:.62;
  mask-image:linear-gradient(180deg,#000 0 74%,transparent 100%);
  -webkit-mask-image:linear-gradient(180deg,#000 0 74%,transparent 100%);
}
/* Darkens the edges of the world — but never the last stretch of lawn. The page
   behind the game is flat meadow green, so any strip the game fails to reach has
   to meet lawn of exactly that colour; a vignette running all the way down drew
   the join as a hard line, which is what a short box looked like on a phone. */
.vignette{
  position:absolute;inset:0;
  background:radial-gradient(130% 90% at 50% 42%, transparent 52%, rgba(30,20,50,.20) 100%);
  mask-image:linear-gradient(180deg,#000 0 74%,transparent 92%);
  -webkit-mask-image:linear-gradient(180deg,#000 0 74%,transparent 92%);
}

/* =========================================================
   WONDER EFFECT
   ========================================================= */
.wonder-veil{
  position:absolute;inset:0;pointer-events:none;opacity:0;
  background:linear-gradient(125deg,var(--wonder-sweep));
  background-size:400% 400%;
  mix-blend-mode:overlay;
  transition:opacity .6s ease;
}
.wonder .wonder-veil{opacity:.62;animation:wonderShift 5s linear infinite}
@keyframes wonderShift{from{background-position:0% 50%}to{background-position:400% 50%}}
.wonder .scenery{animation:wonderWarp 7s ease-in-out infinite}
@keyframes wonderWarp{0%,100%{filter:saturate(1.5) hue-rotate(0deg)}50%{filter:saturate(1.9) hue-rotate(22deg)}}
.wonder .garden{animation:wonderBob 1.6s ease-in-out infinite}
@keyframes wonderBob{0%,100%{transform:rotate(-1.1deg) scale(1.01)}50%{transform:rotate(1.1deg) scale(1.03)}}
/* rainbow halo on the planter so the effect reads even over the UI layer */
.wonder .garden::after{
  content:'';position:absolute;inset:-8px;border-radius:32px;pointer-events:none;
  background:conic-gradient(var(--wonder-sweep),#ff6b6b);
  filter:blur(9px);opacity:.75;z-index:-1;
  animation:haloSpin 3.6s linear infinite;
}
@keyframes haloSpin{to{transform:rotate(360deg)}}

.fx-canvas{position:absolute;inset:0;pointer-events:none;z-index:40}

/* =========================================================
   LAYOUT
   ========================================================= */
.ui{
  position:absolute;inset:0;
  display:grid;
  grid-template-rows:auto auto auto minmax(0,1fr) auto;
  grid-template-columns:minmax(0,1fr);
  padding:
    calc(var(--sat) + 8px)
    calc(var(--sar) + 10px)
    var(--bottom-gap)
    calc(var(--sal) + 10px);
  gap:6px;
  z-index:20;
  max-width:560px;margin:0 auto;
}

/* Rows are pinned so a hidden rail never shifts the grid. */
.hud{grid-row:1}
.quest-strip{grid-row:2}
.rail{grid-row:3}
.stage{grid-row:4}
.dock{grid-row:5}

/* ---------- HUD ---------- */
/* The wallets, the almanac and the settings are the one thing that must be true
   everywhere — you should always be able to see what you have and get to your
   settings, whatever room you are standing in. `.ui` is a stacking context at
   z-index 20, so nothing inside it can climb above a place layer; the layers go
   UNDER `.ui` instead and the HUD sits at the top of it. */
.hud{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;
  position:relative;z-index:6}
.wallets{display:flex;gap:6px;flex-wrap:nowrap}
/* NOWRAP, deliberately. A wrapping HUD grows row 1 by ~46px, which costs the
   `1fr` stage the same and makes sizeGarden() visibly shrink the board — a
   layout that reshapes itself as you earn, which is the one thing this row must
   never do. So nothing wraps; instead the numberless meter is the only pill
   allowed to compress, and it gives up padding (not a digit) when a 375px
   phone is carrying a million coins and four figures of gems. */
.wallet{flex:none;min-width:0}

.wallet{
  display:flex;align-items:center;gap:5px;
  padding:5px 11px 5px 6px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  border:3px solid var(--ink);
  border-radius:999px;
  box-shadow:0 3px 0 var(--ink-2), 0 6px 10px rgba(44,26,16,.18);
  font-weight:800;font-size:15px;
  font-variant-numeric:tabular-nums;
  line-height:1;
  transition:transform .12s cubic-bezier(.34,1.56,.64,1);
}
.wallet.pop{animation:walletPop .34s cubic-bezier(.34,1.56,.64,1)}
@keyframes walletPop{0%{transform:scale(1)}40%{transform:scale(1.18) rotate(-3deg)}100%{transform:scale(1)}}
/* THE YEAR METER PILL RETIRED, 2026-08-30 (phase 3.5). Its body was the meter
   and its tap was the projection; both moved into the dock's Turn button and
   the Year panel, which have room for what a 39px pill never did. */

.w-icon{width:22px;height:22px;display:block;flex:none}
.w-icon svg{width:100%;height:100%;display:block}
.w-num{min-width:1ch}


.hud-actions{display:flex;gap:6px;flex:none}
.round-btn{
  width:44px;height:44px;flex:none;
  display:grid;place-items:center;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  border:3px solid var(--ink);
  border-radius:50%;
  box-shadow:0 4px 0 var(--ink-2), 0 7px 12px rgba(44,26,16,.2);
  transition:transform .08s ease, box-shadow .08s ease;
}
.round-btn svg{width:24px;height:24px}
.round-btn:active{transform:translateY(4px);box-shadow:0 0 0 var(--ink-2), 0 3px 6px rgba(44,26,16,.25)}
.round-btn.ghost{width:38px;height:38px;box-shadow:0 3px 0 var(--ink-2)}
.round-btn.ghost svg{width:20px;height:20px}

/* ---------- quest strip ---------- */
.quest-strip{
  display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:8px;
  width:100%;text-align:left;
  padding:5px 8px 5px 5px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  border:3px solid var(--ink);border-radius:18px;
  box-shadow:0 3px 0 var(--ink-2), 0 6px 10px rgba(44,26,16,.16);
  min-height:0;
}
.quest-strip:active{transform:translateY(2px);box-shadow:0 1px 0 var(--ink-2)}
.quest-strip.ready{
  background:linear-gradient(180deg,#fff6c2,#ffe066);
  animation:affordPulse 1.4s ease-in-out infinite;
}
.q-pip-wrap{
  width:36px;height:36px;flex:none;
  display:grid;place-items:center;
}
.q-pip-ring{
  grid-area:1/1;width:36px;height:36px;border-radius:50%;
  background:conic-gradient(from -90deg,#69db7c calc(var(--p,0) * 360deg), rgba(44,26,16,.16) 0);
}
.q-pip{
  grid-area:1/1;position:relative;z-index:1;
  width:26px;height:26px;
  display:grid;place-items:center;
  background:linear-gradient(180deg,#ffe066,#ffc93c);
  border:2.5px solid var(--ink);border-radius:50%;
  font-weight:900;font-size:13px;line-height:1;
  box-shadow:0 1px 0 var(--ink-2);
}
.q-bar{
  position:relative;display:block;height:24px;min-width:0;
  border-radius:999px;overflow:hidden;
  background:rgba(44,26,16,.1);border:2.5px solid var(--ink);
}
.q-bar i{
  position:absolute;inset:0;width:100%;
  background:linear-gradient(90deg,#8ce99a,#ffe066);
  clip-path:inset(0 calc(100% - var(--fill,0%)) 0 0);
}
.q-bar-label{
  position:relative;z-index:1;
  display:flex;align-items:center;justify-content:space-between;gap:8px;
  height:100%;padding:0 10px;
  color:var(--ink);
}
.q-text{
  font-weight:800;font-size:13px;line-height:1;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;
}
.q-count{flex:none;font-weight:900;font-size:12px;font-variant-numeric:tabular-nums}
.q-reward{
  flex:none;display:flex;align-items:center;gap:3px;
  padding:4px 8px 4px 6px;
  background:linear-gradient(180deg,#fff,#fff5e2);
  border:2.5px solid var(--ink);border-radius:999px;
  font-weight:900;font-size:12px;line-height:1;
  box-shadow:0 2px 0 var(--ink-2);
}
.q-reward svg{width:14px;height:14px;display:block}
.quest-strip.rest .q-reward{display:none}

/* ---------- status rail ---------- */
.rail{
  display:flex;gap:6px;align-items:center;
  overflow-x:auto;overflow-y:hidden;
  scrollbar-width:none;
  min-height:0;
  padding:1px 0;
  -webkit-overflow-scrolling:touch;
}
.rail::-webkit-scrollbar{display:none}
/* THE TRACK IS RESERVED, NOT COLLAPSED. It used to vanish when empty, which was
   free while boosts were spent from a chip inside it — the row and its cause
   were the same object. Since phase 3.5 they are spending from POWER-UP, 30px
   from the board, and a board that shrinks 9% on a short phone the moment you
   tap a button is the layout changing shape under the player. Same call the HUD
   made when it took sub-44px buttons over a row that wraps as you earn. */
.rail{min-height:33px}

.chip{
  display:flex;align-items:center;gap:5px;flex:none;
  padding:3px 9px 3px 4px;
  border:2.5px solid var(--ink);
  border-radius:999px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 2px 0 var(--ink-2);
  font-size:12px;font-weight:700;line-height:1;
  white-space:nowrap;
}
.chip svg{width:17px;height:17px;display:block}
.chip .chip-ico{width:17px;height:17px;flex:none}
.chip.timed{background:linear-gradient(180deg,#fff,#f3e7ff)}
.chip .ring{
  width:19px;height:19px;flex:none;border-radius:50%;
  background:conic-gradient(var(--tint,#9775fa) calc(var(--p) * 360deg), rgba(44,26,16,.14) 0);
  display:grid;place-items:center;
  font-size:8.5px;font-weight:800;
}
.chip .ring i{
  width:13px;height:13px;border-radius:50%;background:#fff;
  display:grid;place-items:center;font-style:normal;font-size:8px;
}
/* `.chip.buyable` retired with the rail's shop — spending a boost is the band's
   POWER-UP button now, and the rail keeps only the countdowns. */
.chip .chip-price{
  display:flex;align-items:center;gap:2px;
  background:var(--ink);color:#fff;border-radius:999px;
  padding:1px 6px;font-size:10px;
}
.chip .chip-price svg{width:12px;height:12px}

/* ---------- stage / garden ---------- */
/* The yard is reserved as padding rather than overlaying, or the garden board
   grows down over it and a creature ends up standing on a plot — which reads as
   something to harvest. */
.stage{--yard-h:clamp(70px,13vh,108px);position:relative;display:grid;place-items:center;min-height:0;min-width:0;padding:2px 0 var(--yard-h);overflow:visible}
.garden-frame{
  width:100%;height:100%;
  display:grid;place-items:center;
  min-height:0;min-width:0;
}
/* exact pixel size is set by JS so the board is always a perfect square */
.garden{
  --gap:clamp(6px, 2.2vw, 13px);
  position:relative;
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  grid-template-rows:repeat(3,minmax(0,1fr));
  gap:var(--gap);
  padding:calc(var(--gap) * .85);
  transform-origin:center;
  border:4px solid var(--ink);
  border-radius:26px;
  background:
    repeating-linear-gradient(96deg, rgba(255,255,255,.07) 0 3px, transparent 3px 14px),
    linear-gradient(180deg,#d9a870 0%, #bd8551 42%, #97643a 100%);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.28),
    inset 0 -9px 0 rgba(0,0,0,.2),
    0 9px 0 #6b4423,
    0 18px 26px rgba(44,26,16,.3);
}
/* grass tufts peeking over the planter rim */
.garden::before{
  content:'';position:absolute;left:5%;right:5%;top:-10px;height:12px;
  background:radial-gradient(ellipse 52% 100% at 50% 100%, var(--grass-d) 0 92%, transparent 94%);
  background-size:22px 12px;
  background-repeat:repeat-x;
  opacity:.9;pointer-events:none;
}

/* ---------- plots ---------- */
.plot{
  position:relative;
  border-radius:var(--r-md);
  border:3px solid var(--ink);
  background:
    radial-gradient(circle at 26% 22%, rgba(255,255,255,.16) 0 12%, transparent 13%),
    radial-gradient(circle at 72% 62%, rgba(0,0,0,.10) 0 9%, transparent 10%),
    linear-gradient(180deg,var(--soil) 0%, var(--soil-d) 72%, var(--soil-dd) 100%);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.16),
    inset 0 -7px 0 rgba(0,0,0,.18),
    0 4px 0 var(--soil-dd),
    0 8px 14px rgba(44,26,16,.24);
  overflow:visible;
  display:block;
  transition:transform .1s ease, box-shadow .1s ease, filter .25s ease;
  isolation:isolate;
}
.plot:active{transform:translateY(3px);box-shadow:inset 0 5px 0 rgba(255,255,255,.16), inset 0 -7px 0 rgba(0,0,0,.18), 0 1px 0 var(--soil-dd), 0 4px 8px rgba(44,26,16,.22)}
.plot-inner{position:absolute;inset:0;border-radius:15px;overflow:hidden}

/* Farm-game convention: the cost of hurrying a plant sits on the plant, always visible, so the
   option teaches itself rather than hiding behind a long-press nobody finds. */
.skip-chip{
  position:absolute;top:5px;right:5px;z-index:4;
  display:none;align-items:center;gap:2px;
  padding:2px 6px 2px 4px;border-radius:999px;
  border:2px solid var(--ink);
  background:#cdeeff;
  font-size:10px;font-weight:900;color:var(--ink);line-height:1.4;
  box-shadow:0 2px 0 var(--ink-2);
  touch-action:manipulation;
}
.plot[data-skip] .skip-chip{display:inline-flex}
.plot[data-skip="no"] .skip-chip{background:#e6e0d8;opacity:.65}
.skip-chip svg{width:11px;height:11px}
/* The price and the wait are one fact, so they never wrap apart. Landscape is
   not a supported orientation and collapses the board to ~100px, where the chip
   is wider than the tile it labels — it was already overflowing there before
   the wait joined it, and this at least keeps it on one line. */
.skip-chip{white-space:nowrap;max-width:calc(100% - 10px)}
.skip-chip:active{transform:translateY(2px);box-shadow:0 0 0 rgba(44,26,16,.35)}
@media (max-height:600px){ .skip-chip{padding:1px 5px 1px 3px;font-size:9px} }

/* The welcome-back scene. Reads as a short account of what happened, not a receipt. */
.away-lede{font-size:14px;font-weight:800;line-height:1.4;margin:2px 0 12px}
.away-list{list-style:none;margin:0 0 14px;padding:0;display:flex;flex-direction:column;gap:8px}
.away-list li{
  display:flex;align-items:flex-start;gap:9px;
  padding:9px 11px;border-radius:12px;
  background:rgba(44,26,16,.06);
  border-left:4px solid rgba(44,26,16,.22);
  font-size:12.5px;font-weight:700;line-height:1.35;
}
.away-list li svg{width:18px;height:18px;flex:none;margin-top:1px}
.away-list li.away-earn{
  border-left-color:var(--coin);
  background:rgba(255,201,60,.18);
}
.away-cap{
  display:flex;align-items:flex-start;gap:8px;
  margin:0 0 12px;padding:9px 11px;border-radius:12px;
  background:rgba(44,26,16,.05);
  border-left:4px solid #c99a6b;
  font-size:11.5px;font-weight:700;line-height:1.35;opacity:.85;
}
.away-cap svg{width:16px;height:16px;flex:none;margin-top:1px}
.away-list li.away-mut{
  border-left-color:var(--mut,#8fd6ff);
  background:color-mix(in srgb, var(--mut,#8fd6ff) 16%, transparent);
}

/* Deliberately invisible: a hit area beside the gem wallet, not a control. Sized to the 44px
   touch minimum so it can actually be hit on a phone, and out of the tab order so a keyboard
   user never lands on it by accident. */
/* Absolutely positioned so it can never take a flex row of its own and grow the HUD — as a
   sibling in flow it wrapped and made the wallets three rows tall. */
.wallets{position:relative}
.dev-dot{
  position:absolute;left:100%;top:0;
  width:44px;height:100%;min-height:32px;
  border:0;background:transparent;padding:0;
  opacity:0;
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;
  cursor:default;
}
.dev-label{
  margin:14px 0 6px;font-size:11px;font-weight:900;
  text-transform:uppercase;letter-spacing:.05em;opacity:.55;
}
.dev-row{display:flex;flex-wrap:wrap;gap:6px}
.dev-btn{
  flex:0 0 auto;
  padding:7px 11px;border-radius:10px;
  border:2px solid var(--ink);
  background:var(--dev,#fffaf0);
  font-size:12px;font-weight:800;color:var(--ink);
  box-shadow:0 2px 0 var(--ink-2);
  touch-action:manipulation;
}
.dev-btn:active{transform:translateY(2px);box-shadow:0 0 0 rgba(44,26,16,.35)}
.dev-btn.on{outline:3px solid #51cf66;outline-offset:1px}
.dev-btn.warn{background:#ffe3e3}
.dev-armed{
  margin:8px 0;padding:6px 10px;border-radius:9px;
  background:rgba(81,207,102,.18);border-left:4px solid #51cf66;
  font-size:11.5px;font-weight:800;
}

/* A mutation is visible the moment it lands and stays visible until harvest — that permanence is
   the whole mechanic, and it is what separates a mutation from a rarity roll revealed at the end. */
.plot[data-mutation]{
  border-color:var(--mut,#8fd6ff);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.16),
    inset 0 -7px 0 rgba(0,0,0,.18),
    0 4px 0 var(--soil-dd),
    0 0 0 3px var(--mut,#8fd6ff),
    0 0 18px 2px var(--mut-glow,#cfeeff),
    0 8px 14px rgba(44,26,16,.24);
}
.plot[data-mutation] .plant{filter:drop-shadow(0 0 6px var(--mut-glow,#cfeeff))}
.plot[data-mutation]::before{
  content:'';position:absolute;inset:0;border-radius:15px;pointer-events:none;z-index:2;
  background:radial-gradient(circle at 50% 40%, var(--mut-glow,#cfeeff) 0%, transparent 62%);
  opacity:.28;
  animation:mutPulse 2.6s ease-in-out infinite;
}
/* Rank climbs with the treatment: the top two tiers get a moving sheen the lower two don't. */
.plot[data-mutation="prismatic"] .plant,
.plot[data-mutation="wonderstruck"] .plant{animation:mutShimmer 2.2s ease-in-out infinite}
@keyframes mutPulse{0%,100%{opacity:.20}50%{opacity:.40}}
@keyframes mutShimmer{
  0%,100%{filter:drop-shadow(0 0 6px var(--mut-glow,#cfeeff)) saturate(1)}
  50%{filter:drop-shadow(0 0 14px var(--mut-glow,#cfeeff)) saturate(1.5)}
}
@media (prefers-reduced-motion:reduce){
  .plot[data-mutation]::before{animation:none;opacity:.3}
  .plot[data-mutation="prismatic"] .plant,
  .plot[data-mutation="wonderstruck"] .plant{animation:none}
}

/* Weather tints the whole scene rather than replacing the sky, so the day/night cycle keeps
   running underneath it. */
.game{--weather-fade:.55s}
.scenery::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  background:var(--weather-tint,transparent);
  opacity:0;transition:opacity var(--weather-fade) ease, background var(--weather-fade) ease;
  mix-blend-mode:multiply;
  /* The same stop `.season-tint` takes below, and for the same reason: a storm
     is heavy enough that a tint running to the bottom edge draws the join
     against the flat lawn iOS paints below a short window. */
  -webkit-mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
  mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
}
/* The year ripening — Summer's palette warming toward autumn as the meter
   fills. Visual only, derived from the meter every slow tick and never saved;
   it sits UNDER the weather tint, so a storm in a late year still reads as a
   storm and the day/night cycle keeps running under both. */
.season-tint{
  position:absolute;inset:0;pointer-events:none;
  background:var(--season-c,#ffb066);
  opacity:var(--season-o,0);
  mix-blend-mode:multiply;
  transition:opacity 1.4s ease;
  /* Faded out over the last 44px, exactly as .meadow::after fades its stripes
     and .vignette fades its darkening. iOS paints any strip below a short
     window with --page-fill, which is the UNtinted lawn — so a tint that ran
     to the bottom edge would draw the join that three rounds of layout work
     went into making invisible. */
  -webkit-mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
  mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
}
/* The flat colour fade this pass exists to replace. `.wx-wash` now carries the
   sky — a gradient rather than a wall of one colour, phased with the front and
   folded into `theme-color` — so these stay at zero. The layer itself is kept
   because `--weather-tint` is still written and a future sky may want a flat
   wash again, and because deleting it would take the reduced-motion rule under
   it with no replacement. Two tints over one sky darkened every sky twice. */
.game[data-weather="rain"] .scenery::after,
.game[data-weather="storm"] .scenery::after,
.game[data-weather="aurora"] .scenery::after,
.game[data-weather="wonderfall"] .scenery::after{opacity:0}
@media (prefers-reduced-motion:reduce){
  .scenery::after{transition:none}
}

/* Adjacency, shown only at the moment a verb flower goes in and then faded out. A permanent
   indicator would clutter a board whose readability is the point. */
.plot.verb-source::after,
.plot.verb-linked::after{
  content:'';position:absolute;inset:-4px;border-radius:calc(var(--r-md) + 4px);
  border:3px solid var(--verb,#8fd6ff);
  pointer-events:none;z-index:3;
  animation:verbLink 1.6s ease-out forwards;
}
.plot.verb-source::after{border-style:solid;box-shadow:0 0 0 3px rgba(44,26,16,.35)}
.plot.verb-linked::after{border-style:dashed}
@keyframes verbLink{
  0%{opacity:0;transform:scale(.9)}
  18%{opacity:1;transform:scale(1.04)}
  32%{transform:scale(1)}
  70%{opacity:1}
  100%{opacity:0}
}
@media (prefers-reduced-motion:reduce){
  .plot.verb-source::after,
  .plot.verb-linked::after{animation:verbLinkCalm 1.6s linear forwards}
  @keyframes verbLinkCalm{0%{opacity:0}10%{opacity:1}70%{opacity:1}100%{opacity:0}}
}

/* soil furrows */
.plot-inner::after{
  content:'';position:absolute;inset:auto 8% 8% 8%;height:34%;
  background:repeating-linear-gradient(2deg, rgba(0,0,0,.12) 0 3px, transparent 3px 11px);
  border-radius:8px;opacity:.7;
}

.plot .plant-slot{
  position:absolute;inset:5% 5% 15% 5%;
  display:grid;place-items:end center;
  pointer-events:none;
}
.plant{width:100%;height:100%;overflow:visible;display:block}
.plant.has-glow{filter:drop-shadow(0 0 7px var(--glow))}

/* growth staging */
.plot[data-stage="0"] .plant{opacity:0}
.plot .f-stemwrap{transform-origin:50% 120px;transform:scaleY(var(--sg,1))}
.plot .f-head{transform-origin:50px 44px}
.plot .f-leaves{transform-origin:50px 96px;transform:scale(var(--lg,1));opacity:var(--lo,1)}

.plot[data-stage="1"]{--sg:.42;--lg:.7;--lo:1}
.plot[data-stage="1"] .f-head{transform:translate(50px,44px) scale(0)}
.plot[data-stage="2"]{--sg:.78;--lg:.9}
.plot[data-stage="2"] .f-head{transform:translate(50px,44px) scale(.34)}
.plot[data-stage="2"] .f-petals{transform:scale(.55)}
.plot[data-stage="3"]{--sg:1;--lg:1}
.plot[data-stage="3"] .f-head{transform:translate(50px,44px) scale(var(--bloom,1))}
.plot .f-stemwrap,.plot .f-head,.plot .f-leaves,.plot .f-petals{transition:transform .6s cubic-bezier(.34,1.4,.64,1)}

/* progress bar */
.plot .bar{
  position:absolute;left:8%;right:8%;bottom:6px;height:9px;
  background:rgba(35,20,10,.5);
  border:2px solid var(--ink);
  border-radius:999px;
  overflow:hidden;
  box-shadow:0 1px 0 rgba(255,255,255,.2);
}
.plot .bar i{
  display:block;height:100%;width:0%;
  background:linear-gradient(90deg,#8ce99a,#51cf66 60%,#ffd43b);
  border-radius:999px;
  transition:width .18s linear;
}
.plot[data-state="empty"] .bar,.plot[data-state="locked"] .bar{display:none}

/* empty state */
.plot .empty-mark{
  position:absolute;inset:0;display:grid;place-items:center;
  opacity:.62;pointer-events:none;
}
.plot .empty-mark svg{width:30%;height:30%;max-width:44px}
.plot[data-state="empty"] .empty-mark{opacity:.62}
#game.onboard .plot[data-state="empty"] .empty-mark{animation:emptyBob 2.6s ease-in-out infinite}
@keyframes emptyBob{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-5%) scale(1.06)}}
.plot:not([data-state="empty"]) .empty-mark{display:none}

/* locked state */
.plot[data-state="locked"]{
  background:linear-gradient(180deg,#9aa39a,#6f7a70 74%,#59635a);
  filter:saturate(.35);
}
.plot .lock-badge{
  position:absolute;inset:0;display:none;
  flex-direction:column;align-items:center;justify-content:center;gap:3px;
}
.plot[data-state="locked"] .lock-badge{display:flex}
.plot .lock-badge svg{width:26%;height:26%;max-width:34px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.25))}
.plot .lock-cost{
  display:flex;align-items:center;gap:3px;
  background:var(--paper);border:2.5px solid var(--ink);border-radius:999px;
  padding:2px 7px;font-size:11px;font-weight:800;line-height:1;
  box-shadow:0 2px 0 var(--ink-2);
}
.plot .lock-cost svg{width:13px;height:13px;max-width:none}
.plot[data-afford="1"] .lock-cost{background:#d3f9d8;animation:affordPulse 1.4s ease-in-out infinite}
.plot[data-gated="1"] .lock-cost{background:var(--paper);animation:none}
@keyframes affordPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.09)}}

/* ready state */
.plot[data-state="ready"]{
  animation:readyBounce 1.05s ease-in-out infinite;
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.16),
    inset 0 -7px 0 rgba(0,0,0,.18),
    0 0 0 4px rgba(255,224,102,.75),
    0 0 22px 6px rgba(255,212,59,.5),
    0 6px 0 var(--soil-dd),
    0 10px 16px rgba(44,26,16,.26);
}
@keyframes readyBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
.plot[data-state="ready"] .plant{animation:plantWiggle .8s ease-in-out infinite}
@keyframes plantWiggle{0%,100%{transform:rotate(-2.2deg)}50%{transform:rotate(2.2deg)}}

.plot .ready-pop{
  position:absolute;top:-12px;right:-8px;
  width:28px;height:28px;border-radius:50%;
  background:linear-gradient(180deg,#fff3bf,var(--coin));
  border:3px solid var(--ink);
  display:none;place-items:center;
  font-weight:800;font-size:16px;line-height:1;
  box-shadow:0 3px 0 var(--ink-2);
  z-index:3;
}
.plot[data-state="ready"] .ready-pop{display:grid;animation:tagPop .9s cubic-bezier(.34,1.56,.64,1) infinite}
@keyframes tagPop{0%,100%{transform:scale(1) rotate(-6deg)}50%{transform:scale(1.16) rotate(6deg)}}

/* shine sweep for ready plots */
.plot[data-state="ready"] .plot-inner::before{
  content:'';position:absolute;top:-30%;bottom:-30%;width:44%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.55),transparent);
  animation:sweep 1.9s linear infinite;
}
@keyframes sweep{from{transform:translateX(-160%) rotate(8deg)}to{transform:translateX(320%) rotate(8deg)}}

/* rarity aura left behind after a harvest */
.plot[data-aura="rare"]{box-shadow:inset 0 5px 0 rgba(255,255,255,.16),inset 0 -7px 0 rgba(0,0,0,.18),0 0 0 3px rgba(77,171,247,.7),0 4px 0 var(--soil-dd)}
.plot[data-aura="epic"]{box-shadow:inset 0 5px 0 rgba(255,255,255,.16),inset 0 -7px 0 rgba(0,0,0,.18),0 0 0 3px rgba(177,151,252,.8),0 4px 0 var(--soil-dd)}
.plot[data-aura="legend"]{box-shadow:inset 0 5px 0 rgba(255,255,255,.16),inset 0 -7px 0 rgba(0,0,0,.18),0 0 0 3px rgba(255,212,59,.9),0 0 16px rgba(255,212,59,.5),0 4px 0 var(--soil-dd)}

/* auto-plant tag */
.plot .auto-tag{
  position:absolute;top:-9px;left:50%;transform:translate(-50%,6px);
  background:var(--paper);border:2.5px solid var(--ink);border-radius:999px;
  padding:1px 7px;font-size:10px;font-weight:800;line-height:1.4;
  opacity:0;transition:opacity .2s ease, transform .2s ease;
  box-shadow:0 2px 0 var(--ink-2);pointer-events:none;z-index:4;
}
.plot .auto-tag.show{opacity:1;transform:translate(-50%,0)}

/* ---------- Rain Dance proc: cloud pops in, rain falls, plot reacts ---------- */
.rain-cloud{
  position:absolute;left:50%;top:0;width:40px;height:22px;
  transform:translate(-50%,-70%) scale(.3);opacity:0;
  pointer-events:none;z-index:6;
  animation:cloudPop .9s cubic-bezier(.34,1.56,.64,1) forwards;
}
.rain-cloud i{position:absolute;display:block;border-radius:50%;background:#eaf6ff;border:2.5px solid var(--ink)}
.rain-cloud i:nth-child(1){left:0;top:7px;width:19px;height:19px}
.rain-cloud i:nth-child(2){left:12px;top:0;width:24px;height:24px}
.rain-cloud i:nth-child(3){left:27px;top:8px;width:15px;height:15px}
@keyframes cloudPop{
  0%{transform:translate(-50%,-70%) scale(.3);opacity:0}
  32%{transform:translate(-50%,-100%) scale(1.08);opacity:1}
  74%{transform:translate(-50%,-100%) scale(1);opacity:1}
  100%{transform:translate(-50%,-84%) scale(.82);opacity:0}
}

.rain-drops{position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:5}
.rain-drop{
  position:absolute;top:-14%;left:var(--x,50%);width:3px;height:13px;border-radius:3px;
  background:linear-gradient(180deg,rgba(116,192,252,0),#74c0fc);
  animation:rainFall var(--dur,.5s) linear var(--delay,0s) both;
}
@keyframes rainFall{
  0%{transform:translateY(0) rotate(8deg) scaleY(1);opacity:0}
  14%{opacity:1}
  80%{transform:translateY(var(--fall,86%)) rotate(8deg) scaleY(1);opacity:1}
  100%{transform:translateY(var(--fall,86%)) rotate(8deg) scale(2.4,.3);opacity:0}
}

.plot-inner.watered{animation:wetFlash .8s ease-out}
/* Multiplies whatever the sky has already put on the plot rather than replacing
   it, so watering during rain reads as a splash on wet soil instead of the rain
   briefly stopping. */
@keyframes wetFlash{
  0%{filter:brightness(var(--wx-wet-b,1)) saturate(var(--wx-wet-s,1))}
  30%{filter:brightness(calc(var(--wx-wet-b,1) * .8)) saturate(calc(var(--wx-wet-s,1) * 1.3))}
  100%{filter:brightness(var(--wx-wet-b,1)) saturate(var(--wx-wet-s,1))}
}

.plot .bar i.flash{animation:barFlash .6s ease-out}
@keyframes barFlash{
  0%{box-shadow:0 0 0 0 rgba(116,192,252,0)}
  35%{box-shadow:0 0 10px 3px rgba(116,192,252,.95)}
  100%{box-shadow:0 0 0 0 rgba(116,192,252,0)}
}

.plot .plant-slot.perk{transform-origin:50% 100%;animation:plantPerk .5s cubic-bezier(.34,1.56,.64,1)}
@keyframes plantPerk{0%{transform:scale(1)}45%{transform:scale(1.1)}100%{transform:scale(1)}}

/* ---------- Bee Swarm proc: a bee visits the flower, then heads off to the hive ---------- */
.bee-fly{
  position:fixed;width:26px;height:26px;margin:-13px 0 0 -13px;
  left:var(--cx);top:var(--cy);z-index:80;pointer-events:none;
  animation:beeFlight .95s cubic-bezier(.45,0,.35,1) both;
}
.bee-fly svg{width:100%;height:100%;display:block;filter:drop-shadow(0 2px 2px rgba(44,26,16,.35))}
@keyframes beeFlight{
  0%{transform:translate(var(--ex),var(--ey)) scale(.5) rotate(-12deg);opacity:0}
  10%{opacity:1}
  38%{transform:translate(0,0) scale(1) rotate(0deg);opacity:1}
  50%{transform:translate(3px,-4px) scale(1.06) rotate(-8deg);opacity:1}
  62%{transform:translate(-3px,3px) scale(1.06) rotate(8deg);opacity:1}
  74%{transform:translate(0,0) scale(1) rotate(0deg);opacity:1}
  100%{transform:translate(var(--xx),var(--xy)) scale(.5) rotate(16deg);opacity:0}
}

/* ---------- Lucky Ladybug proc: a sticker rides the plot until it's harvested ---------- */
.plot .lucky-badge{
  position:absolute;top:-10px;left:-9px;width:26px;height:26px;border-radius:50%;
  background:linear-gradient(180deg,#ff8787,#fa5252);border:2.5px solid var(--ink);
  display:none;place-items:center;box-shadow:0 2px 0 var(--ink-2);
  pointer-events:none;z-index:4;
}
.plot .lucky-badge svg{width:16px;height:16px}
.plot .lucky-badge.show{display:grid;animation:luckyIdle 2.4s ease-in-out infinite}
.plot .lucky-badge.land{animation:luckyLand .6s cubic-bezier(.34,1.56,.64,1)}
@keyframes luckyIdle{0%,100%{transform:scale(1) rotate(-5deg)}50%{transform:scale(1.08) rotate(5deg)}}
@keyframes luckyLand{
  0%{transform:scale(.3) translateY(-42px) rotate(-24deg);opacity:0}
  60%{transform:scale(1.28) translateY(2px) rotate(10deg);opacity:1}
  100%{transform:scale(1) translateY(0) rotate(0deg);opacity:1}
}

/* ---------- the talking flower ---------- */
.flower-cell{position:relative;display:grid;place-items:center;overflow:visible}
.flower-glow{
  position:absolute;width:150%;height:150%;border-radius:50%;
  background:radial-gradient(circle, rgba(255,236,150,.65) 0%, rgba(255,214,245,.35) 42%, transparent 68%);
  animation:glowPulse 3.2s ease-in-out infinite;pointer-events:none;
}
@keyframes glowPulse{0%,100%{transform:scale(.94);opacity:.75}50%{transform:scale(1.08);opacity:1}}

.combo-ring{
  position:absolute;width:112%;height:112%;border-radius:50%;
  background:conic-gradient(from -90deg, var(--coin) calc(var(--combo,0) * 360deg), rgba(255,255,255,.28) 0);
  -webkit-mask:radial-gradient(farthest-side, transparent calc(100% - 7px), #000 calc(100% - 6px));
  mask:radial-gradient(farthest-side, transparent calc(100% - 7px), #000 calc(100% - 6px));
  pointer-events:none;
  filter:drop-shadow(0 0 4px rgba(255,201,60,.6));
  transition:opacity .3s ease;
  opacity:var(--combo-op,.35);
}

.flower-btn{
  position:relative;width:100%;height:100%;
  display:grid;place-items:center;overflow:visible;
  transition:transform .09s cubic-bezier(.34,1.7,.64,1);
}
.talker{width:118%;height:118%;overflow:visible;filter:drop-shadow(0 6px 0 rgba(44,26,16,.22))}
.flower-btn:active{transform:scale(.9) translateY(3px)}
.flower-btn.bounce .tf-head{animation:headSquash .32s cubic-bezier(.34,1.56,.64,1)}
@keyframes headSquash{
  0%{transform:translate(60px,56px) scale(1,1)}
  35%{transform:translate(60px,60px) scale(1.16,.84)}
  70%{transform:translate(60px,52px) scale(.92,1.1)}
  100%{transform:translate(60px,56px) scale(1,1)}
}
.tf-head{transform:translate(60px,56px)}
.tf-stemwrap{transform-origin:60px 130px;animation:stemSway 3.4s ease-in-out infinite}
@keyframes stemSway{0%,100%{transform:rotate(-1.6deg)}50%{transform:rotate(1.6deg)}}
.tf-petals{animation:petalSpin 26s linear infinite;transform-origin:0 0}
@keyframes petalSpin{from{transform:rotate(0)}to{transform:rotate(360deg)}}
.tf-lid{transform-origin:center top;transform:scaleY(0);animation:blink 5.4s infinite}
@keyframes blink{0%,92%,100%{transform:scaleY(0)}95%,97%{transform:scaleY(1)}}
.flower-btn.happy .tf-lid{animation:none;transform:scaleY(0)}
.flower-btn.squint .tf-lid{animation:none;transform:scaleY(.55)}
.tf-pupil,.tf-shine{transform:translate(calc(var(--px,0) * 1px), calc(var(--py,0) * 1px));transition:transform .18s ease-out}
.tf-cheek{animation:cheekPulse 3s ease-in-out infinite}
@keyframes cheekPulse{0%,100%{opacity:.65}50%{opacity:.95}}
.tf-leaf-l{transform-origin:60px 108px;animation:leafWaveL 2.6s ease-in-out infinite}
.tf-leaf-r{transform-origin:60px 100px;animation:leafWaveR 2.9s ease-in-out infinite}
@keyframes leafWaveL{0%,100%{transform:rotate(0)}50%{transform:rotate(-9deg)}}
@keyframes leafWaveR{0%,100%{transform:rotate(0)}50%{transform:rotate(9deg)}}

/* speech bubble */
.speech{
  position:absolute;left:50%;bottom:calc(100% + 10px);
  transform:translate(-50%,8px) scale(.7);
  transform-origin:50% 100%;
  background:var(--paper);
  border:3px solid var(--ink);
  border-radius:16px;
  padding:6px 11px;
  font-size:13px;font-weight:800;line-height:1.2;
  white-space:nowrap;max-width:80vw;
  box-shadow:0 4px 0 var(--ink-2), var(--shadow-pop);
  opacity:0;pointer-events:none;z-index:12;
  transition:opacity .22s ease, transform .28s cubic-bezier(.34,1.7,.64,1);
}
.speech::after{
  content:'';position:absolute;left:50%;top:100%;margin-left:-9px;
  width:0;height:0;border:9px solid transparent;border-top-color:var(--ink);
}
.speech::before{
  content:'';position:absolute;left:50%;top:calc(100% - 4px);margin-left:-6px;z-index:1;
  width:0;height:0;border:6px solid transparent;border-top-color:var(--paper);
}
.speech.show{opacity:1;transform:translate(-50%,0) scale(1)}

/* ---------- dock ---------- */
/* Columns follow the number of buttons rather than being pinned at four, or a
   fifth tab wraps onto a second row and covers the lawn. The IA doc's hard cap
   is five; past that the labels stop fitting and something has to leave. */
.dock{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);gap:6px}
/* These were written before the dock had a `.five` class and `:has()` takes its
   argument's specificity, so they silently out-ranked everything `.dock.five`
   set — the phase's own 9.5px label rule never applied at any size. Converted,
   not outranked. */
.dock.five{gap:5px}
.dock.five .dock-label{letter-spacing:-.01em}
.dock.five .dock-ico{width:22px;height:22px}
.dock.five .dock-btn{padding:6px 2px 5px}
.dock-btn{
  position:relative;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;
  padding:7px 4px 6px;
  min-height:56px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-3));
  border:3px solid var(--ink);
  border-radius:var(--r-md);
  box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.22);
  transition:transform .08s ease, box-shadow .08s ease, background .2s ease;
}
.dock-btn:active,.dock-btn.pressed{transform:translateY(5px);box-shadow:0 0 0 var(--ink-2), 0 3px 7px rgba(44,26,16,.25)}
.dock-ico{width:24px;height:24px;display:block}
.dock-ico svg{width:100%;height:100%;display:block}
.dock-label{font-size:11px;font-weight:800;line-height:1}
@media (min-width:420px){
  .dock-ico{width:26px;height:26px}
  .dock-label{font-size:12px}
  .dock.five .dock-ico{width:24px;height:24px}
}
.dock-dot{
  position:absolute;top:-6px;right:-4px;
  width:16px;height:16px;border-radius:50%;
  background:#ff6b6b;border:2.5px solid var(--ink);
  box-shadow:0 2px 0 var(--ink-2);
  display:grid;place-items:center;
  font-size:9.5px;font-weight:800;line-height:1;color:#fff;
  text-shadow:0 1px 0 rgba(44,26,16,.75);
  animation:tagPop 1.1s ease-in-out infinite;
}
/* A count needs room; a bare nudge does not. */
.dock-dot.wide{width:auto;min-width:16px;padding:0 4px;border-radius:999px}
.dock-dot[hidden]{display:none}

/* =========================================================
   THE BIG FIVE — phase 3.5, docs/36
   Orders & Quests | Cards | GARDEN | Turn | Shop
   ========================================================= */
/* The row is pinned to the dock's own height and aligned to its END, so the
   raised centre button can be TALLER than the row and rise out of the top
   without growing grid row 5. Nothing else in `.ui` is re-measured — which is
   the rule that stopped a fifth button wrapping onto a second row. */
/* Explicit heights, not min-heights. `Orders & Quests` sets on two lines and
   pushed its own button 5px taller than the other four, which with
   `align-items:end` made one button stand proud of the row for no reason. Every
   button is exactly the row; only the pedestal is allowed to be taller. */
/* `.critter-yard` is z-index:3 and `.stage` is not a stacking context, so
   without this the dock paints UNDER the creatures — they wash their glow
   across the crown of the one button that must read as *play*, and they steal
   its taps. `#dock`, not `.dock`: the Hollow's and the meadow's docks wear the
   same class and must not be raised over their own rooms. */
#dock{position:relative;z-index:5}
.dock.five{grid-template-rows:56px;align-items:end}
.dock.five .dock-btn{min-height:0;height:56px}
.dock.five .dock-btn.home{height:74px}
.dock.five .dock-btn.home .dock-label{font-size:11.5px;letter-spacing:.02em}
.dock.five .dock-btn.home .dock-ico{width:28px;height:28px}
.dock.five .dock-label{font-size:9.5px;line-height:1.05}

/* THE PEDESTAL. Grass, because it is the one button the colour of the place it
   takes you back to — so it reads as *play* by material and not only by size. */
.dock-btn.home{
  border-radius:22px;border-width:3.5px;
  background:linear-gradient(180deg,#8fe08a,var(--grass) 55%,var(--grass-d));
  /* The lip is the pedestal's OWN dark, not a lighter grass: at #2f7a34 it was
     rgb(47,122,52) against a lawn of rgb(70,144,76) — same hue, 1.2:1 — so the
     one button that must read as *play* was the one button that read as flat. */
  box-shadow:0 6px 0 #173d1c, 0 12px 18px rgba(44,26,16,.3);
  color:#173d1c;
}
.dock-btn.home .dock-ico{width:28px;height:28px}
.dock-btn.home .dock-label{font-size:11.5px;letter-spacing:.02em;
  text-shadow:0 1px 0 rgba(255,255,255,.45)}
.dock-btn.home:active,.dock-btn.home.pressed{
  box-shadow:0 1px 0 #173d1c, 0 4px 9px rgba(44,26,16,.28)}
.dock.five .dock-btn.home .dock-ico{width:26px;height:26px}

/* THE TURN BUTTON IS THE METER. The fill rises from the bottom rather than
   wiping across: the pill it replaces is 39px wide and had no room to do
   anything else, while a button has 56px of height to travel — and rising is
   the right picture, because the pouch fills. `isolation` keeps the z-index:-1
   fill above the button's own background and below its content. */
/* THE CLIP BELONGS TO THE FILL, NOT THE BUTTON. `overflow:hidden` on the button
   also ate the pouch chip that hangs 9px above it — the top of a 21px chip
   sliced off flat, taking its ink contour and the tops of its digits with it,
   on the one number this phase newly promoted to an always-visible surface. The
   fill is its own clip box instead; 15px is the button's inner curve (18px
   outer radius less the 3px border). */
.dock-btn.turn{isolation:isolate}
.turn-fill{
  position:absolute;inset:0;z-index:-1;
  overflow:hidden;border-radius:15px;
}
.turn-fill::before{
  content:'';position:absolute;left:0;right:0;bottom:0;
  height:var(--year-p,0%);
  /* The waterline is the gradient's own first stop, not a border: a border sits
     OUTSIDE the box, so at 0% the element was still 2px tall and a fresh save
     wore a grey-tan line along the inside of the button that meant nothing. */
  background:linear-gradient(180deg,var(--seed-d) 0 2px,var(--seed-l) 2px,var(--seed));
  transition:height .6s ease;
}
/* The ready breath is the full pill's own ring, moved onto the button. It is
   worn by exactly one thing in the game and it has only ever meant one thing:
   the Turn is ready. */
.dock-btn.turn.ready{animation:turnFull 1.4s ease-in-out infinite}
@keyframes turnFull{
  0%,100%{box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.22), 0 0 0 0 rgba(255,201,60,.55)}
  50%    {box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.22), 0 0 0 5px rgba(255,201,60,.5)}
}
/* AND THE GLINT. The breath says "something here"; a Turn is agency over the
   season itself and deserves a moment that says "look now". It is the ready
   plot's own `sweep` — same 100deg band, same 8deg tilt — parked off the button
   for seven eighths of its cycle so it lands as an event rather than as the
   constant travel a plot can afford at 1.9s. Gold, not the plot's white,
   because gold is already the one thing that means "the Turn is ready" on this
   button, and white over cream paper is a lightening streak with nothing
   attached to it. It rides `.turn-fill`, which is already the clip box at the
   button's inner curve — the clip may never move to the button, which would eat
   the pouch chip hanging above it. */
.dock-btn.turn.ready .turn-fill::after{
  content:'';position:absolute;top:-30%;bottom:-30%;left:0;width:38%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.9) 38%,rgba(255,201,60,.7) 64%,transparent);
  animation:turnShine var(--turn-shine,9s) linear infinite;
}
@keyframes turnShine{
  0%,88%{transform:translateX(-170%) rotate(8deg)}
  100%  {transform:translateX(330%) rotate(8deg)}
}
/* The pouch, promoted. The number already exists in three panels; this is the
   first time it is on an always-visible surface. Year one never shows it. */
.seedchip{
  position:absolute;top:-9px;left:50%;transform:translateX(-50%);
  display:flex;align-items:center;gap:2px;
  padding:1px 6px 1px 3px;white-space:nowrap;
  border:2.5px solid var(--ink);border-radius:999px;
  background:linear-gradient(180deg,#fffdf7,var(--seed-l));
  box-shadow:0 2px 0 var(--ink-2);
  font-size:10px;font-weight:800;line-height:1.5;
}
.seedchip svg{width:12px;height:12px;display:block}
.seedchip[hidden]{display:none}
/* A dot on a button that is already breathing is noise. */
.dock-btn.turn.ready .dock-dot{display:none}

/* ---------- THE BAND — the owner's sketch, 2026-08-30 ----------
   UPGRADE, the tending creatures, POWER-UP, in one row between the garden and
   the dock. It costs the layout NOTHING: it lives in the yard `.stage` already
   reserves (`--yard-h`), so the board measures the same before and after.

   Both buttons are inset past the season edge tabs, which keep the screen edges
   they have always had — 38px of tab, 34px of inset, 6px of daylight. */
.fpill,.fround{
  position:absolute;bottom:4px;z-index:4;
  margin:0;border:3.5px solid var(--ink);cursor:pointer;
  transition:transform .08s ease, box-shadow .08s ease;
}
.fpill{
  left:34px;display:flex;flex-direction:column;align-items:center;gap:1px;
  padding:7px 10px 6px;border-radius:16px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-3));
  box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.24);
  font:inherit;font-size:9px;font-weight:800;letter-spacing:.02em;color:var(--ink);
}
.fpill .f-ico{width:24px;height:24px;display:block}
.fpill .f-ico svg{width:100%;height:100%;display:block}
.fround{
  right:34px;width:58px;height:58px;border-radius:50%;
  display:grid;place-items:center;padding:0;
  background:
    radial-gradient(circle at 34% 26%,rgba(255,255,255,.9),rgba(255,255,255,.5)),
    var(--tint,#9775fa);
  box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.24);
}
.fround svg{width:28px;height:28px;display:block}
.fpill:active,.fround:active{transform:translateY(4px);
  box-shadow:0 1px 0 var(--ink-2), 0 3px 7px rgba(44,26,16,.25)}
.f-count{
  position:absolute;top:-6px;right:-4px;
  min-width:20px;height:20px;padding:0 4px;border-radius:999px;
  display:grid;place-items:center;
  border:2.5px solid var(--ink);
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 2px 0 var(--ink-2);
  font-size:10px;font-weight:800;color:var(--ink);
}
/* THE EMPTY SLOT — drained paper, the same family a locked season tab wears,
   because this game already says "a thing you do not have yet" that way. It is
   not a dead button: tapping it says where boosts come from. */
.fround.empty{
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  box-shadow:0 5px 0 var(--paper-dim-edge), 0 9px 14px rgba(44,26,16,.18);
}
/* Drained paper carries "you do not have this yet" on its own — the same way a
   locked season tab does. A dashed contour and a faded glyph are two more ways
   of saying it, in a vocabulary doc 05 does not have. The glyph keeps its ink
   and takes the drained body's own value instead. */
.fround.empty svg{color:var(--paper-dim-edge)}
/* Held, but the one you hold is already running. Not drained — you still own it
   and the badge still counts it — just visibly not offering itself. */
.fround.busy{opacity:.72;box-shadow:0 3px 0 var(--ink-2), 0 6px 10px rgba(44,26,16,.18)}
/* A place puts its own dock up and hides the garden's; the band goes with it. */
.in-hollow .fpill,.in-hollow .fround,
.in-meadow .fpill,.in-meadow .fround,
.in-gate .fpill,.in-gate .fround{display:none}

/* =========================================================
   THE YEAR PANEL — phase 3.5
   The projection card the HUD pill used to pop, with room.
   ========================================================= */
.yr-meter{
  position:relative;height:78px;margin:2px 0 12px;overflow:hidden;
  border:3.5px solid var(--ink);border-radius:var(--r-md);
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.18);
}
.yr-meter > i{
  position:absolute;left:0;right:0;bottom:0;height:var(--p,0%);
  background:linear-gradient(180deg,var(--seed-l),var(--seed));
  transition:height .6s ease;
}
/* A LOCKED meter is still a meter: the drained rim says "not yet", the fill
   underneath says how far. Year one is a mystery, never a blank. */
.yr-meter.locked{
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  box-shadow:0 4px 0 var(--paper-dim-edge), 0 8px 14px rgba(44,26,16,.18);
}
/* Its own breath, because it has its own lip — borrowing the dock button's
   keyframe restated a 5px/9px shadow this 3.5px-bordered panel does not have. */
.yr-meter.ready{animation:yrFull 1.4s ease-in-out infinite}
@keyframes yrFull{
  0%,100%{box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.18), 0 0 0 0 rgba(255,201,60,.55)}
  50%    {box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.18), 0 0 0 5px rgba(255,201,60,.5)}
}
.yr-lab{
  position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:8px;
  padding:0 14px;font-size:15px;font-weight:800;text-align:center;line-height:1.25;
}
.yr-lab svg{width:22px;height:22px;flex:none}
.yr-gates{display:grid;gap:7px;margin-bottom:12px}
.yr-gate{display:flex;align-items:center;gap:8px;font-size:11.5px;font-weight:800}
.yr-gate .lab{display:flex;align-items:center;gap:4px;width:104px;flex:none}
.yr-gate .lab svg{width:15px;height:15px}
.yr-gate .track{
  flex:1;height:13px;overflow:hidden;
  border:2.5px solid var(--ink);border-radius:999px;background:var(--paper-dim-2);
}
/* GOLD WHILE IT IS SHORT, GREEN WHEN IT IS MET. Green is this game's yes — a
   gate sitting at 20% must not wear it. */
.yr-gate .track i{display:block;height:100%;
  background:linear-gradient(180deg,#ffe066,var(--coin));transition:width .3s ease}
/* The binding gate — the lower of the two, and the one the dock button's fill
   is actually drawing. The deeper gold, so the panel and the button agree. */
.yr-gate.bind .track i{background:linear-gradient(180deg,var(--coin),var(--coin-d))}
.yr-gate.met .track i{background:linear-gradient(180deg,#8fe08a,var(--grass-d))}
.yr-gate .tag{
  flex:none;min-width:46px;text-align:center;font-size:9.5px;
  background:var(--ink);color:var(--paper-2);border-radius:999px;padding:2px 7px;
}
.yr-gate .tag svg{width:12px;height:12px;display:block;margin:0 auto}
.yr-gate.met .tag{background:#2f9e44}
.yr-say{text-align:center;font-style:italic;font-size:13px;line-height:1.45}
.yr-spend{margin-top:16px;display:flex;align-items:center;gap:7px}

/* Petal spending, a card per flower. The Almanac keeps the record rows. */
.pt-cards{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:9px}
.pt-card{
  min-width:0;padding:8px;
  border:3px solid var(--ink);border-radius:var(--r-md);
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 3px 0 var(--ink-2), 0 6px 10px rgba(44,26,16,.14);
}
.pt-head{display:flex;align-items:center;gap:6px;font-size:12.5px;font-weight:800}
.pt-art{width:26px;height:26px;flex:none;line-height:0}
.pt-art svg{width:100%;height:100%;overflow:visible}
.pt-card .petal-track{column-gap:3px;margin-top:6px;min-height:0}
.pt-card .petal-track .pl{width:44px;font-size:9px;line-height:1.05}
.pt-card .petal-track .pips i{width:6px;height:6px}
.pt-card .petal-track .price{font-size:10px;padding:2px 7px;min-width:0;white-space:nowrap}
.pt-card .petal-track .pv{font-size:9px;line-height:1.2;margin-bottom:2px}
/* Two cards to a row on a phone, so the sentence is the same words at a size
   the column can carry. */
.pt-card .petal-track .pd{font-size:9px;line-height:1.25;margin-bottom:2px}

/* The seam between the two halves of Orders & Quests. Not a heading — the two
   panels bring their own — just enough air that they read as two lists. */
.ord-split{height:3px;margin:18px 0 6px;border-radius:999px;background:var(--ink-2)}

/* =========================================================
   BOTTOM SHEET
   ========================================================= */
.scrim{
  position:absolute;inset:0;z-index:45;
  background:rgba(30,18,10,.45);
  backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);
  opacity:0;transition:opacity .3s ease;
}
.scrim.show{opacity:1}

.sheet{
  position:absolute;left:0;right:0;bottom:0;z-index:50;
  height:min(80dvh, 660px);
  max-width:560px;margin:0 auto;
  display:flex;flex-direction:column;
  background:linear-gradient(180deg,var(--paper) 0%, var(--paper-2) 100%);
  border:4px solid var(--ink);
  border-bottom:0;
  border-radius:28px 28px 0 0;
  /* No shadow while it is parked. The closed sheet sits just below the game's
     bottom edge, and a 30px upward blur from there painted a dark band across
     the bottom of the lawn that `.game` then cut off square — the hard line the
     bottom of the screen kept showing was this, not just the missing pixels
     under it. It only casts a shadow when there is something to cast it onto. */
  transform:translateY(102%);
  transition:transform .34s cubic-bezier(.22,1,.28,1), box-shadow .34s ease;
  will-change:transform;
  padding-bottom:var(--sab);
}
.sheet.open{transform:translateY(0);box-shadow:0 -8px 30px rgba(44,26,16,.32)}
.sheet.dragging{transition:none}
/* The ceremony, after it commits. closeSheet() refuses while the Tally rolls;
   these two hide the controls that would otherwise invite the tap it refuses. */
.sheet.no-exit .sheet-head .round-btn.ghost{display:none}
.sheet.no-exit .sheet-grip span{opacity:.25}
.sheet.no-exit .sheet-grip{touch-action:auto;cursor:default}

.sheet-grip{padding:9px 0 3px;display:grid;place-items:center;touch-action:none;cursor:grab}
.sheet-grip span{width:52px;height:6px;border-radius:999px;background:rgba(44,26,16,.28)}

.sheet-head{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:0 14px 8px;
}
.sheet-head h2{margin:0;font-size:21px;font-weight:800;letter-spacing:.01em}

.sheet-tabs{display:flex;gap:6px;padding:0 14px 10px;overflow-x:auto;scrollbar-width:none}
.sheet-tabs::-webkit-scrollbar{display:none}
.sheet-tabs:empty{display:none}
.tab{
  flex:none;padding:7px 14px;
  border:3px solid var(--ink);border-radius:999px;
  background:#fff;font-weight:800;font-size:13px;line-height:1;
  box-shadow:0 3px 0 var(--ink-2);
  transition:transform .08s ease, box-shadow .08s ease, background .18s ease;
}
.tab[aria-selected="true"]{background:linear-gradient(180deg,#ffe066,var(--coin));transform:translateY(1px)}
.tab:active{transform:translateY(3px);box-shadow:0 0 0 var(--ink-2)}

.sheet-body{
  flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;
  padding:2px 14px 22px;
}
.sheet-body::-webkit-scrollbar{width:0}

.sheet-note{
  font-size:13px;font-weight:600;opacity:.72;
  margin:0 0 10px;line-height:1.35;
}
.cere .sheet-note.centred{margin:14px 0 0}

/* ---------- cards ---------- */
.card-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.card-grid.one{grid-template-columns:1fr}

.card{
  position:relative;text-align:left;
  display:flex;flex-direction:column;gap:5px;
  padding:10px 10px 9px;
  background:linear-gradient(180deg,#fff,#fff5e2);
  border:3px solid var(--ink);
  border-radius:var(--r-md);
  box-shadow:0 4px 0 var(--ink-2), 0 7px 12px rgba(44,26,16,.14);
  transition:transform .09s ease, box-shadow .09s ease, filter .2s ease;
  overflow:hidden;
}
.card:active{transform:translateY(4px);box-shadow:0 0 0 var(--ink-2), 0 3px 7px rgba(44,26,16,.2)}
.card[disabled]{filter:grayscale(.55) opacity(.62);box-shadow:0 3px 0 var(--ink-2)}
.card.affordable{background:linear-gradient(180deg,#fff,#eaffe8)}
/* A card that is still for sale and worth nothing yet — the drained treatment
   the locked seed rows use, because it is an advert for the thing you are not
   ready for and its numbers have to survive. box-shadow is one property, so the
   lip is restated here or it is deleted. */
.card.idle{
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  box-shadow:0 4px 0 var(--paper-dim-edge), 0 7px 12px rgba(44,26,16,.14);
}
.card.idle .card-badge{filter:saturate(.35)}
.card.idle .stat{
  background:linear-gradient(180deg,#f6f2ea,var(--paper-dim-2));
  box-shadow:0 2px 0 var(--paper-dim-edge);
}
.card.idle.affordable::after{content:none}
.card.affordable::after{
  content:'';position:absolute;inset:0;border-radius:15px;
  box-shadow:inset 0 0 0 3px rgba(81,207,102,.55);pointer-events:none;
}
.card-top{display:flex;align-items:center;gap:8px}
.card-badge{
  --badge-c:#ffe9a8;
  width:46px;height:46px;flex:none;border-radius:50%;
  display:grid;place-items:center;
  background:radial-gradient(circle at 34% 28%, #fff, var(--badge-c));
  border:3px solid var(--ink);
  box-shadow:0 3px 0 var(--ink-2);
}
.card-badge svg{width:26px;height:26px}
.card-badge .bloom{width:34px;height:34px}
.card-top > span:last-child{min-width:0}
.card-title{display:block;font-size:14px;font-weight:800;line-height:1.1}
.card-sub{display:block;font-size:11px;font-weight:700;opacity:.62;line-height:1.15;margin-top:1px}
.card-desc{font-size:11.5px;font-weight:600;line-height:1.3;color:var(--ink-soft)}
/* What you have and what the price buys, in the same cream pills the seed rows
   use. The card is 150px wide on a phone, so the pair is allowed to stack. */
.card-vals{display:flex;flex-wrap:wrap;gap:4px}
.card-vals:empty{display:none}
.card-vals .stat{font-size:10px;padding:2px 6px}
/* The second pill is the promise, not the fact, so it steps back a shade
   without leaving the family — same body, same contour, same lip. */
.card-vals .stat.next{background:linear-gradient(180deg,#fffdf7,var(--paper-3));opacity:.82}

.pips{display:flex;gap:3px;flex-wrap:wrap;margin-top:1px}
.pip{width:7px;height:7px;border-radius:50%;background:var(--coin);border:1.6px solid var(--ink)}
.pip.off{background:rgba(44,26,16,.14)}
.lvl-text{font-size:11px;font-weight:800;opacity:.7}

.price{
  display:inline-flex;align-items:center;gap:4px;align-self:flex-start;
  margin-top:auto;padding:4px 9px;
  border:2.5px solid var(--ink);border-radius:999px;
  background:linear-gradient(180deg,#fff,var(--paper-3));
  font-size:12.5px;font-weight:800;line-height:1;
  font-variant-numeric:tabular-nums;
  box-shadow:0 2px 0 var(--ink-2);
}
.price svg{width:15px;height:15px}
.price.ok{background:linear-gradient(180deg,#d3f9d8,#8ce99a)}
.price.no{background:linear-gradient(180deg,#ffe3e3,#ffc9c9)}
.price.maxed{background:linear-gradient(180deg,#e9ecef,#dee2e6)}

/* seed rows */
.seed-row{
  display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;
  width:100%;text-align:left;
  padding:9px 11px;margin-bottom:9px;
  background:linear-gradient(180deg,#fff,#fff5e2);
  border:3px solid var(--ink);border-radius:var(--r-md);
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.24);
  transition:transform .09s ease, box-shadow .09s ease;
}
.seed-row:active{transform:translateY(4px);box-shadow:0 0 0 var(--ink-2), 0 4px 8px rgba(44,26,16,.22)}
.seed-row[disabled]{filter:grayscale(.6) opacity(.6)}
/* A LOCKED SEED ROW is drained, not deleted. The old gated row was
   grayscale(.7) opacity(.72) with a "Level N" chip, and it did two wrong
   things: the level stopped being the real gate when the Garden Year priced
   seeds in gold, and a row you cannot read is a poor advert for the thing you
   are saving 150K for. It takes the drained-paper family every other "not now"
   state wears — and it restates its own lip, because a box-shadow modifier
   that forgets to is how the lip gets deleted in exactly the states a player
   is looking at. */
.seed-row.locked{
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  box-shadow:0 4px 0 var(--paper-dim-edge), 0 8px 14px rgba(44,26,16,.18);
}
.seed-row.locked:active{transform:translateY(4px);box-shadow:0 0 0 var(--paper-dim-edge), 0 4px 8px rgba(44,26,16,.18)}
.seed-row.locked .seed-art{filter:saturate(.35)}
.seed-row.locked .stat{
  background:linear-gradient(180deg,#f6f2ea,var(--paper-dim-2));
  box-shadow:0 2px 0 var(--paper-dim-edge);
}
.seed-row.locked .verb-note,
.seed-row.locked .verb-chip{filter:saturate(.35)}
/* The unlock price sits in the slot the go button uses on every other row. */
.seed-lock{
  display:inline-flex;align-items:center;gap:4px;flex:none;
  padding:6px 9px;
  border:2.5px solid var(--ink);border-radius:999px;
  background:linear-gradient(180deg,#fff,var(--paper-3));
  box-shadow:0 2px 0 var(--ink-2);
  font-size:12px;font-weight:800;line-height:1;
  font-variant-numeric:tabular-nums;
}
.seed-lock svg{width:15px;height:15px}
.seed-lock.ok{background:linear-gradient(180deg,#d3f9d8,#8ce99a);animation:affordPulse 1.4s ease-in-out infinite}
.seed-lock.no{background:linear-gradient(180deg,#f6f2ea,var(--paper-dim-2));box-shadow:0 2px 0 var(--paper-dim-edge)}
/* The row you just paid for keeps its place in the list and lights for a beat,
   so the thing you bought is the thing that moves. */
.seed-row.fresh{
  background:linear-gradient(180deg,#fff6c2,#ffe066);
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.24), 0 0 0 4px rgba(255,201,60,.5);
}

/* The unlock question. One-time, permanent, unrefundable, and the game has no
   undo — so it asks, as a whole panel rather than a card floating over a
   scrolling list that a `panels` event can rebuild underneath it. */
.unlock-ask{
  display:grid;justify-items:center;gap:7px;text-align:center;
  margin:10px 2px;padding:18px 14px 16px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  border:3.5px solid var(--ink);border-radius:var(--r-lg);
  box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.22);
}
.unlock-art{
  width:100px;height:100px;border-radius:50%;
  display:grid;place-items:center;margin-bottom:2px;
  background:
    radial-gradient(circle at 34% 28%, rgba(255,255,255,.95), rgba(255,255,255,.6)),
    var(--art,#e8f7e3);
  border:4px solid var(--ink);
  box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.22);
}
.unlock-ask h3{margin:0;font-size:22px;font-weight:800}
.unlock-ask p{margin:0;font-size:13.5px;font-weight:700;color:var(--ink-soft);line-height:1.4;max-width:32ch}
.unlock-row{display:grid;grid-template-columns:1fr 1fr;gap:9px;width:100%;margin-top:10px}
.unlock-row .big-btn{margin-top:0}
.unlock-row .big-btn[disabled]{filter:grayscale(.55) opacity(.6)}
.unlock-short{font-size:12.5px;font-weight:800;color:var(--ink-soft)}

.quest-card{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  width:100%;text-align:left;
  padding:10px 12px;margin-bottom:9px;
  background:linear-gradient(180deg,#fff,#fff5e2);
  border:3px solid var(--ink);border-radius:var(--r-md);
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.24);
}
.quest-card.ready{
  background:linear-gradient(180deg,#fff6c2,#ffe066);
}
.quest-card .q-meta{min-width:0}
.quest-card .q-meta .seed-name{display:block}
.quest-card .q-prog{flex:none;font-weight:900;font-variant-numeric:tabular-nums;font-size:13px}
/* Tinted from the seed's own c1, so nineteen badges down the left edge run the
   hue wheel instead of being nineteen identical mint discs. The white radial is
   a veil rather than a fill: it pales any petal colour to a legible disc while
   keeping the highlight where it was, so no seed needs a second hex. */
.seed-art{
  width:54px;height:54px;flex:none;border-radius:50%;
  display:grid;place-items:center;
  background:
    radial-gradient(circle at 34% 28%, rgba(255,255,255,.95), rgba(255,255,255,.6)),
    var(--art,#e8f7e3);
  border:3px solid var(--ink);box-shadow:0 3px 0 var(--ink-2);
}
.seed-name{font-size:15px;font-weight:800;line-height:1.15}
.seed-stats{display:flex;flex-wrap:wrap;gap:5px;margin-top:3px}
/* Every other number in the game sits in a cream pill with a contour and a lip.
   These were 7% ink on cream with neither, three or four to a row and eight rows
   deep, which is most of why the sheet read as a spec table. */
.stat{
  display:inline-flex;align-items:center;gap:3px;
  padding:2px 7px;border-radius:999px;
  border:2px solid var(--ink);
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 2px 0 var(--ink-2);
  font-size:10.5px;font-weight:800;line-height:1.3;
}
.stat svg{width:12px;height:12px}
.stat.good{background:linear-gradient(180deg,#d3f9d8,#8ce99a)}
.stat.gem{background:linear-gradient(180deg,#cdeeff,var(--gem))}
/* A number that is WORSE, in the same red the prices use for "no" — green means
   yes in both pill families already, and a hive slowed by its neighbour must not
   read as a benefit because it is written in the same cream as one. */
.stat.bad{background:linear-gradient(180deg,#ffe3e3,#ffc9c9)}
/* The multiplier the garden is adding, riding inside the pill it changed. A
   second pill would wrap the row on a phone, and the fact belongs to the
   number rather than beside it. */
.stat .mx{font-style:normal;font-size:9.5px;font-weight:900;opacity:.72;margin-left:1px}
/* A multiplier BELOW one, in the same red the prices use for "no". Colour only,
   so the pill keeps its own body and lip. */
.stat .mx.low{color:#c92a2a;opacity:.9}
/* Flavour, and it lives in the Almanac rather than the picker — a line that
   carries no decision is weight on a screen where every row is a decision. */
.seed-desc{display:block;margin-top:2px;font-size:11px;font-weight:600;line-height:1.3;color:var(--ink-soft)}

/* A verb is a different kind of fact from cost and yield, so it gets its own colour and never
   sits inside .seed-stats. */
.verb-chip{
  display:inline-block;vertical-align:2px;margin-left:6px;
  padding:1px 7px;border-radius:999px;
  background:var(--verb,#8fd6ff);
  border:2px solid rgba(44,26,16,.55);
  color:var(--ink);font-size:9.5px;font-weight:900;letter-spacing:.02em;
  text-transform:uppercase;line-height:1.5;
}
/* The trait is the most interesting fact in the row, so it is an object rather
   than a grey band with a coloured rule. The verb's own colour is the body,
   veiled to a pale wash so ink stays legible on every tint in DATA.verbs. */
.verb-note{
  display:block;margin-top:5px;
  padding:4px 9px;border-radius:12px;
  border:2px solid var(--ink);
  background:linear-gradient(180deg, rgba(255,255,255,.86), rgba(255,255,255,.58)), var(--verb,#8fd6ff);
  box-shadow:0 2px 0 var(--ink-2);
  font-size:11px;font-weight:700;line-height:1.3;color:var(--ink);
}
.seed-go{
  width:40px;height:40px;border-radius:50%;flex:none;
  display:grid;place-items:center;
  background:linear-gradient(180deg,#8ce99a,#51cf66);
  border:3px solid var(--ink);box-shadow:0 3px 0 var(--ink-2);
}
.seed-go svg{width:22px;height:22px}
/* A row you cannot afford YET drains its go button rather than swapping it: an
   empty disc reads as a bug, and a padlock would claim the one-time wall. It
   takes the --paper-dim family the locked row above it wears, and restates its
   own lip, because a box-shadow modifier that forgets to is how the lip gets
   deleted in exactly the states a player is looking at. */
.seed-row[disabled] .seed-go{
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  box-shadow:0 3px 0 var(--paper-dim-edge);
}

/* stats list (bonuses screen) */
.stat-block{
  background:linear-gradient(180deg,#fff,#fff5e2);
  border:3px solid var(--ink);border-radius:var(--r-md);
  box-shadow:0 4px 0 var(--ink-2);
  padding:11px 12px;margin-bottom:11px;
}
.stat-block h3{
  margin:0 0 8px;font-size:14px;font-weight:800;
  text-transform:uppercase;letter-spacing:.06em;opacity:.75;
  display:flex;align-items:center;gap:6px;
}
.stat-block h3 svg{width:18px;height:18px}
.stat-line{
  display:flex;justify-content:space-between;align-items:baseline;gap:10px;
  padding:5px 0;border-bottom:2px dashed rgba(44,26,16,.12);
}
.stat-line:last-child{border-bottom:0}
.stat-line .k{font-size:13px;font-weight:800}
.stat-line .v{font-size:13px;font-weight:800;font-variant-numeric:tabular-nums;color:#2b8a3e;text-align:right}
.stat-line .d{font-size:11px;font-weight:600;opacity:.6;display:block;margin-top:1px}
.stat-line .kk{flex:1}
.stat-line.dim{opacity:.45}
.almanac-bloom{display:inline-flex;vertical-align:middle;margin-right:6px}

/* almanac seed rows — three columns, plus the mastery goal underneath */
.almanac-row{padding:6px 0;border-bottom:2px dashed rgba(44,26,16,.12)}
.almanac-row:last-child{border-bottom:0}
.almanac-row.dim{opacity:.45}
.almanac-row-top{display:flex;align-items:baseline;gap:8px}
.almanac-row-top .n{flex:1;min-width:0;font-size:13px;font-weight:800;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.almanac-row-top .r{flex:0 0 auto;font-size:11px;font-weight:800;opacity:.7;
  text-transform:uppercase;letter-spacing:.04em}
.almanac-row-top .r-rare{color:#1c7ed6;opacity:1}
.almanac-row-top .r-epic{color:#9775fa;opacity:1}
.almanac-row-top .r-legend{color:#f08c00;opacity:1}
.almanac-row-top .c{flex:0 0 46px;font-size:13px;font-weight:800;
  font-variant-numeric:tabular-nums;color:#2b8a3e;text-align:right}
/* A petal track: name, pips, price. The pips are the game's existing level
   vocabulary in the seed's own green, so a player reads "3 of 5" without a
   fraction. The price is the ordinary .price family in the three states they
   already know — green affordable, drained short, grey MAX. */
/* Four columns and two rows: the track reads left to right as it always did,
   and the value the pips are counting sits on its own line underneath, where a
   long one cannot squeeze the buy button. */
.petal-track{display:grid;grid-template-columns:auto auto 1fr auto;align-items:center;
  column-gap:8px;row-gap:1px;margin-top:10px;min-height:40px}
.petal-track .pl{width:88px;flex:none;font-size:11.5px;font-weight:800}
.petal-track .sp{flex:1}
/* Five dots are one object and never wrap into two rows — a 4-and-1 stack
   reads as a broken meter rather than as a ladder half filled. `.pips` wraps by
   default because the badge cards use eight of them on a wider card. */
.petal-track .pips{flex:none;gap:3px;flex-wrap:nowrap}
/* The number beside the dots. Quiet — the pips are the feel and this is the
   fact, so it sits under them in the row's own ink rather than in a pill of
   its own, which at this size would be a third object per track. */
.petal-track .pv{
  grid-column:1 / -1;
  font-size:10px;font-weight:800;line-height:1.25;color:var(--ink-soft);
  font-variant-numeric:tabular-nums;
}
.petal-track .pv.full{color:#2f9e44}
/* The third row: what the skill IS, in words, at any pip count. Same token as
   the seed's own description line under the Almanac heading — descriptive text
   on paper is `--ink-soft`, never an opacity on ink. */
.petal-track .pd{
  grid-column:1 / -1;
  font-size:11px;font-weight:600;line-height:1.3;color:var(--ink-soft);
}
.petal-track .pip{width:10px;height:10px;background:var(--seed);
  box-shadow:inset 0 2px 0 rgba(255,255,255,.4)}
.petal-track .pip.off{background:var(--paper-dim-2);box-shadow:none}
.petal-buy{font:inherit;color:var(--ink);cursor:pointer;
  min-height:40px;padding:0 12px;
  font-size:12.5px;font-weight:800;font-variant-numeric:tabular-nums}
.petal-buy:active{transform:translateY(2px);box-shadow:0 0 0 var(--ink-2)}
.petal-buy[disabled]{cursor:default}
.pouch-note,.bench-note{display:flex;align-items:center;gap:7px;opacity:1}

.almanac-row-goal{display:flex;align-items:center;gap:8px;margin-top:3px}
.almanac-row-goal .g{flex:0 0 auto;font-size:11px;font-weight:700;opacity:.6;
  display:inline-flex;align-items:center;gap:3px}
.almanac-row-goal .gem-pip svg{width:11px;height:11px;vertical-align:-1px}
.almanac-row-goal .y{flex:0 0 46px;font-size:11px;font-weight:800;
  font-variant-numeric:tabular-nums;color:var(--coin-d);text-align:right}
.mastery-bar{
  flex:1;min-width:24px;height:6px;border-radius:999px;overflow:hidden;
  background:rgba(44,26,16,.12);border:1.5px solid rgba(44,26,16,.35);
}
.mastery-bar b{
  display:block;height:100%;width:100%;
  background:linear-gradient(90deg,#ffd43b,#f08c00);
  transform:scaleX(0);transform-origin:left center;
}
.almanac-meter{
  position:relative;height:24px;margin:4px 0 10px;
  border-radius:999px;overflow:hidden;
  background:rgba(44,26,16,.1);border:2.5px solid var(--ink);
}
.almanac-meter i{
  position:absolute;inset:0;width:100%;
  background:linear-gradient(90deg,#8ce99a,#ffe066);
  clip-path:inset(0 calc(100% - var(--fill,0%)) 0 0);
}
.almanac-meter span{
  position:relative;z-index:1;
  display:flex;align-items:center;justify-content:center;
  height:100%;font-size:12px;font-weight:800;
}

/* settings rows */
.set-row{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:11px 12px;margin-bottom:10px;
  background:linear-gradient(180deg,#fff,#fff5e2);
  border:3px solid var(--ink);border-radius:var(--r-md);
  box-shadow:0 4px 0 var(--ink-2);
  font-weight:800;font-size:14px;
}
.set-row .lbl{display:flex;align-items:center;gap:8px}
.set-row .lbl svg{width:22px;height:22px}
.toggle{
  width:60px;height:34px;border-radius:999px;flex:none;
  border:3px solid var(--ink);background:#dee2e6;
  position:relative;transition:background .2s ease;
  box-shadow:inset 0 2px 0 rgba(0,0,0,.12);
}
.toggle i{
  position:absolute;top:2px;left:2px;width:24px;height:24px;border-radius:50%;
  background:#fff;border:2.5px solid var(--ink);
  transition:transform .22s cubic-bezier(.34,1.56,.64,1);
}
.toggle[aria-pressed="true"]{background:#8ce99a}
.toggle[aria-pressed="true"] i{transform:translateX(26px)}

.big-btn{
  width:100%;padding:13px;margin-top:6px;
  border:3px solid var(--ink);border-radius:var(--r-md);
  font-size:15px;font-weight:800;
  background:linear-gradient(180deg,#fff,var(--paper-3));
  box-shadow:0 5px 0 var(--ink-2);
  display:flex;align-items:center;justify-content:center;gap:8px;
  transition:transform .08s ease, box-shadow .08s ease;
}
.big-btn:active{transform:translateY(5px);box-shadow:0 0 0 var(--ink-2)}
.big-btn svg{width:20px;height:20px}
.big-btn.danger{background:linear-gradient(180deg,#ffe3e3,#ffa8a8)}
.big-btn.magic{background:linear-gradient(180deg,#e5dbff,#b197fc)}
/* Confirming a purchase is a yes, and green is what yes looks like on every
   price in this game. The magic purple next door is the Epic rarity's family
   and is not available for a buy button. */
.big-btn.yes{background:linear-gradient(180deg,#d3f9d8,#8ce99a)}
.big-btn[disabled]{filter:grayscale(.6) opacity(.55)}


/* =========================================================
   THE TURN — the ceremony
   Five beats on one sheet. The plate runs the garden's own four value tiers
   indoors: ink outline, a DARK body, cream pills for every number, .outlined
   for the big ones. That is why an arcade scoreboard can be this loud without
   leaving the house style.
   ========================================================= */
.sheet.cere-sheet{height:min(94dvh, 800px)}
.sheet.cere-sheet .sheet-body{display:flex;flex-direction:column}
.cere{flex:1;display:flex;flex-direction:column;justify-content:center}
/* The picker starts at the top instead: with every flower unlocked the grid is
   seven rows and fills the sheet, and a list that centres itself when short and
   fills when long jumps around as the garden grows. */
.cere.top{justify-content:flex-start}
.cere .btn-row{padding-top:16px;display:grid;gap:8px}
.centred{text-align:center}

/* The flower stands IN the body, not in #sheetArt: a sheet this tall has no
   room above its own top edge and the breakout art clips off the screen. */
.cere-flower{display:grid;place-items:center;margin:0 0 10px}
.cere-flower svg{width:132px;height:132px;filter:drop-shadow(0 6px 0 rgba(44,26,16,.22))}

.speech-block{
  position:relative;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  border:3px solid var(--ink);border-radius:var(--r-md);
  padding:11px 13px;
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.2);
  font-size:16px;font-weight:800;line-height:1.3;
}
.speech-block::after{
  content:'';position:absolute;left:calc(50% - 7px);top:-9px;width:14px;height:14px;
  background:#fffdf7;border-left:3px solid var(--ink);border-top:3px solid var(--ink);
  border-radius:3px 0 0 0;transform:rotate(45deg);
}

.plate{
  position:relative;margin-top:12px;
  border:4px solid var(--ink);border-radius:var(--r-lg);
  padding:14px 14px 12px;
  background:
    repeating-linear-gradient(96deg, rgba(255,255,255,.06) 0 3px, transparent 3px 14px),
    linear-gradient(180deg,#8a5a33 0%, #71472a 46%, #5c3a22 100%);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.2),
    inset 0 -8px 0 rgba(0,0,0,.22),
    0 5px 0 #4a2e1a,
    0 9px 14px rgba(44,26,16,.22);
}
/* The won state adds a rim and restates the whole stack — a box-shadow modifier
   that forgets to is how the lip gets deleted in the one state everybody looks at. */
.plate.won{
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.2),
    inset 0 -8px 0 rgba(0,0,0,.22),
    0 5px 0 #4a2e1a,
    0 9px 14px rgba(44,26,16,.22),
    0 0 0 4px var(--coin);
}
.plate-cap{margin:0 0 4px;font-size:12px;font-weight:800;letter-spacing:.08em;
  text-transform:uppercase;color:#f6e3c4;opacity:.85;text-align:center}
.plate-big{display:flex;align-items:center;justify-content:center;gap:8px;
  font-size:clamp(36px,11vw,46px);font-weight:800;line-height:1;font-variant-numeric:tabular-nums}
.plate-big svg{width:34px;height:34px}

/* A Tally line. The counter is a cream pill and the bonus is the same green
   every affordable price wears, because a bonus is the biggest yes the game has. */
.tline{display:flex;align-items:center;gap:8px;margin-top:8px}
.tline.just{animation:tlineIn .3s cubic-bezier(.34,1.56,.64,1)}
@keyframes tlineIn{0%{transform:translateX(26px);opacity:0}100%{transform:none;opacity:1}}
.tline .lab{flex:1;min-width:0;font-size:13.5px;font-weight:800;color:#fff4de;line-height:1.15;
  text-shadow:0 2px 0 rgba(44,26,16,.5)}
.tline .cnt{font-variant-numeric:tabular-nums}
.tline .bon{background:linear-gradient(180deg,#d3f9d8,#8ce99a)}
.tfoot{display:flex;justify-content:flex-end;align-items:baseline;gap:8px;margin-top:12px;padding-right:4px}
.tfoot.centred{justify-content:center}
.tfoot .plate-cap{margin:0}
.tmult{font-size:34px;font-weight:800;line-height:1}

.cost-line{
  display:flex;align-items:center;gap:8px;margin-top:11px;padding:9px 11px;
  border:3px solid var(--ink);border-radius:var(--r-sm);
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  box-shadow:0 3px 0 var(--paper-dim-edge), 0 6px 10px rgba(44,26,16,.14);
  font-size:12.5px;font-weight:700;line-height:1.35;
}
.cost-line svg{width:22px;height:22px;flex:none}
/* Same geometry, opposite meaning. Drained paper is the game's word for
   "asleep, out of food, stopped working" (doc 05), so a grant and a
   reassurance may not wear it — the blessing that just landed was reading as
   the deadest object on the screen. */
.cost-line.good{
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 3px 0 var(--ink-2), 0 6px 10px rgba(44,26,16,.18);
}
/* The ladder, restated where the ceremony uses it: 3px pairs with
   0 6px 10px, 4px with 0 8px 14px, 5px with the dock's 0 9px 14px, 9px with
   0 18px 26px. Lip and contact shadow scale together or the object looks like
   it is hovering at the wrong height. */
.keep-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
.cere-lab{margin:12px 0 0;font-size:11px;font-weight:800;letter-spacing:.07em;
  text-transform:uppercase;color:var(--ink-soft)}
/* What the year takes wears the drained paper every "not any more" state in
   this game wears — not red, because a cost stated in advance is not an error.
   It restates its own lip, like every other modifier that touches box-shadow. */
.chip.gone{
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  box-shadow:0 2px 0 var(--paper-dim-edge);
}
.chip.gone svg{filter:saturate(.45)}

/* The blessing. A grid of blooms, not a list of rows: the only decision here is
   WHICH flower, so cost, yield and verb would all be noise. */
.bless-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-top:4px}
.bless-tile{
  display:grid;justify-items:center;gap:3px;padding:9px 4px 7px;
  border:3px solid var(--ink);border-radius:var(--r-md);
  background:linear-gradient(180deg,#fff,#fff5e2);
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.2);
  transition:transform .08s ease, box-shadow .08s ease;
}
.bless-tile:active{transform:translateY(4px);box-shadow:0 0 0 var(--ink-2), 0 4px 8px rgba(44,26,16,.2)}
.bless-tile.on{
  background:linear-gradient(180deg,#fff6c2,#ffe066);
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.2), 0 0 0 4px rgba(255,201,60,.55);
}
.bless-art{
  width:52px;height:52px;border-radius:50%;display:grid;place-items:center;
  background:
    radial-gradient(circle at 34% 28%, rgba(255,255,255,.95), rgba(255,255,255,.6)),
    var(--art,#e8f7e3);
  border:3px solid var(--ink);box-shadow:0 3px 0 var(--ink-2);
}
.bless-name{font-size:11.5px;font-weight:800;line-height:1.1;text-align:center}
.bless-tile .pip{width:9px;height:9px;background:var(--seed)}
.bless-tile .pip.off{background:var(--paper-dim-2)}
/* What this flower is already sitting on, under its pips — the same quiet
   caption the petal tracks use, so the two screens read as one fact. */
.bless-val{
  font-size:9.5px;font-weight:800;line-height:1.1;color:var(--ink-soft);
  font-variant-numeric:tabular-nums;
}
.bless-tile.on .bless-val{color:#7a4d10}

/* The gate. The reset pays in PLACES first and numbers second, so the place
   gets the picture and the seeds get a chip. */
.gate-card{
  position:relative;overflow:hidden;margin-top:12px;
  border:4px solid var(--ink);border-radius:var(--r-lg);
  box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.22);
}
.gate-scene{
  position:relative;height:126px;overflow:hidden;
  background:linear-gradient(180deg,#ffd9a1,#ffb570 62%,#e88f4e);
}
/* Ground, inside the container, because doc 05 check 3 is "is anything
   floating?" and a sky with a flower in it is the answer. The hedges stand on
   this and the bloom gets a pad on top of it. */
.gate-scene::after{
  content:'';position:absolute;left:-2%;right:-2%;bottom:0;height:26px;z-index:1;
  border-top:3px solid var(--ink);
  background:linear-gradient(180deg,#9a7a3f,#7d6132);
}
.gate-scene .hedge{position:absolute;bottom:14px;width:42%;height:106px;line-height:0;z-index:2}
.hedge-svg{width:100%;height:100%;display:block;overflow:visible}
.gate-scene .hedge.l{left:0;transform:translateX(-30%) rotate(-7deg);transform-origin:bottom left;
  animation:gateL .9s cubic-bezier(.22,1,.28,1) both}
.gate-scene .hedge.r{right:0;transform:translateX(30%) rotate(7deg);transform-origin:bottom right;
  animation:gateR .9s cubic-bezier(.22,1,.28,1) both}
/* A gate with nowhere to go stays shut. The card may not draw an open gate
   while the strip that honours it is still unbuilt. */
.gate-scene.shut .hedge.l{transform:none;animation:none}
.gate-scene.shut .hedge.r{transform:none;animation:none}
@keyframes gateL{0%{transform:none}100%{transform:translateX(-30%) rotate(-7deg)}}
@keyframes gateR{0%{transform:none}100%{transform:translateX(30%) rotate(7deg)}}
.gate-bloom{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);width:62px;
  line-height:0;z-index:2}
.gate-bloom svg{width:100%;height:auto;filter:drop-shadow(0 4px 0 rgba(44,26,16,.22))}
/* The pad is the ground and the shadow is the contact — a dark ellipse on a
   dark band would be invisible, which is the meadow's lesson. */
.gate-bloom::after{
  content:'';position:absolute;left:50%;bottom:-9px;transform:translateX(-50%);
  width:46px;height:12px;border-radius:50%;z-index:-1;
  background:radial-gradient(ellipse at 50% 40%, #b08c4c 0 60%, rgba(176,140,76,0) 72%);
}
.gate-foot{
  display:flex;align-items:center;justify-content:space-between;gap:8px;
  padding:9px 12px;background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  border-top:3px solid var(--ink);font-size:15px;font-weight:800;
}

/* =========================================================
   OVERLAYS
   ========================================================= */
.float-layer{position:absolute;inset:0;pointer-events:none;z-index:44;overflow:hidden}
.float{
  position:absolute;transform:translate(-50%,-50%);
  font-size:19px;font-weight:800;white-space:nowrap;
  color:#fff;
  text-shadow:-2px 0 var(--ink),2px 0 var(--ink),0 -2px var(--ink),0 2px var(--ink),
              -2px -2px var(--ink),2px -2px var(--ink),-2px 2px var(--ink),2px 2px var(--ink);
  animation:floatUp .85s cubic-bezier(.2,.8,.3,1) forwards;
}
@keyframes floatUp{
  0%{opacity:0;transform:translate(-50%,-50%) scale(.6)}
  22%{opacity:1;transform:translate(calc(-50% + var(--dx)/3),-115%) scale(1.14)}
  100%{opacity:0;transform:translate(calc(-50% + var(--dx)),-260%) scale(.95)}
}
.float.crit{font-size:27px;color:#ffe066;animation-name:floatCrit}
@keyframes floatCrit{
  0%{opacity:0;transform:translate(-50%,-50%) scale(.4) rotate(-12deg)}
  20%{opacity:1;transform:translate(-50%,-120%) scale(1.5) rotate(6deg)}
  55%{transform:translate(-50%,-165%) scale(1.15) rotate(-3deg)}
  100%{opacity:0;transform:translate(calc(-50% + var(--dx)),-300%) scale(1) rotate(0)}
}
.float.big{font-size:24px;color:#b2f2bb;animation-duration:1.1s}
.float.gem{color:var(--gem)}
.float.ticket{color:#ffc9de}
.float.legend{font-size:26px;color:var(--legend)}
.float.epic{color:#d0bfff}
.float.rare{color:#a5d8ff}
.float.water{color:#74c0fc}
.float.bee{color:#ffc93c}
.float.lucky{color:#fa5252}

/* toasts */
.toasts{
  position:absolute;top:calc(var(--sat) + 98px);left:0;right:0;
  z-index:60;display:flex;flex-direction:column;align-items:center;gap:8px;
  pointer-events:none;padding:0 12px;
}
.toast{
  display:flex;align-items:center;gap:9px;
  max-width:min(94vw,420px);
  padding:8px 14px 8px 9px;
  background:linear-gradient(180deg,#fff,var(--paper-2));
  border:3px solid var(--ink);border-radius:999px;
  box-shadow:0 5px 0 var(--ink-2), var(--shadow-pop);
  animation:toastIn .4s cubic-bezier(.34,1.56,.64,1);
  transition:opacity .3s ease, transform .3s ease;
}
.toast .t-art{
  width:38px;height:38px;flex:none;border-radius:50%;
  display:grid;place-items:center;
  background:radial-gradient(circle at 34% 28%, #fff, #ffe9a8);
  border:2.5px solid var(--ink);
}
.toast .t-art svg,.toast .t-art .bloom{width:26px;height:26px}
.toast .t-title{font-size:13.5px;font-weight:800;line-height:1.15}
.toast .t-body{font-size:11.5px;font-weight:700;opacity:.72;line-height:1.2}
.toast.legend{background:linear-gradient(180deg,#fff8d6,var(--legend))}
.toast.epic{background:linear-gradient(180deg,#f3ecff,#d0bfff)}
.toast.rare{background:linear-gradient(180deg,#e7f5ff,#a5d8ff)}
.toast.out{opacity:0;transform:translateY(-14px) scale(.94)}
@keyframes toastIn{0%{opacity:0;transform:translateY(-24px) scale(.8)}100%{opacity:1;transform:none}}

/* wonder banner */
.banner{
  position:absolute;left:0;right:0;top:34%;z-index:70;
  display:grid;place-items:center;pointer-events:none;
  opacity:0;
}
.banner.show{opacity:1}
.banner .bg{
  padding:10px 26px;border-radius:999px;
  border:4px solid var(--ink);
  background:linear-gradient(100deg,#ff6b6b,#ffd43b,#69db7c,#4dabf7,#b197fc);
  background-size:300% 100%;
  box-shadow:0 8px 0 var(--ink-2), 0 16px 34px rgba(44,26,16,.34);
  animation:bannerIn .55s cubic-bezier(.34,1.7,.64,1), rainbowSlide 2.4s linear infinite;
}
.banner .bg h2{
  margin:0;font-size:clamp(26px,8vw,40px);font-weight:800;letter-spacing:.04em;
  color:#fff;
  text-shadow:-3px 0 var(--ink),3px 0 var(--ink),0 -3px var(--ink),0 3px var(--ink),
              -3px -3px var(--ink),3px -3px var(--ink),-3px 3px var(--ink),3px 3px var(--ink);
}
.banner .bg p{margin:1px 0 0;font-size:13px;font-weight:800;color:#fff;text-shadow:0 2px 0 var(--ink)}
@keyframes bannerIn{0%{transform:scale(.3) rotate(-14deg);opacity:0}60%{transform:scale(1.12) rotate(3deg);opacity:1}100%{transform:scale(1) rotate(0)}}
@keyframes rainbowSlide{from{background-position:0 0}to{background-position:300% 0}}
.banner.out .bg{animation:bannerOut .4s ease forwards}
@keyframes bannerOut{to{transform:scale(.7);opacity:0}}

/* coach marks */
.coach[hidden]{display:none}
.coach{
  position:absolute;z-index:65;pointer-events:none;
  transform:translate(-50%,0);
  display:flex;flex-direction:column;align-items:center;gap:2px;
}
.coach .arrow{
  width:0;height:0;
  border:11px solid transparent;border-top-color:var(--ink);
  animation:coachBob .9s ease-in-out infinite;
}
.coach .tip{
  background:var(--paper);border:3px solid var(--ink);border-radius:14px;
  padding:6px 12px;font-size:13px;font-weight:800;white-space:nowrap;
  box-shadow:0 4px 0 var(--ink-2);
  /* Set by placeCoach() when the target is near a screen edge: the bubble
     slides, the arrow underneath does not. Zero for every centred mark. */
  display:flex;align-items:center;gap:7px;
  transform:translateX(var(--tip-shift,0px));
}
/* The finger and its wake. Drawn travelling left, mirrored for the way back —
   one glyph, two directions, and the little slide is the whole instruction. */
.c-swipe{display:block;line-height:0;animation:coachDrag 1.5s ease-in-out infinite}
.c-swipe svg{width:21px;height:21px;display:block;overflow:visible}
.c-swipe.right svg{transform:scaleX(-1)}
/* DIRECTIONAL, or `reverse` does nothing. A symmetric ease-in-out keyframe is
   its own reverse, so the mirrored glyph was travelling both ways identically
   and the slide — the half of the mark that says "drag" — carried no direction
   at all. */
@keyframes coachDrag{0%{transform:translateX(4px)}70%,100%{transform:translateX(-4px)}}
.c-swipe.right{animation-direction:reverse}
/* Beside the tab, not above it. `side-l` means the TAB is on the left, so the
   mark stands to its right with the arrow pointing back at it. The base
   `translate(-50%,0)` has to go: placeCoach() writes the exact corner here. */
.coach.side-l,.coach.side-r{transform:none;flex-direction:row;align-items:center;gap:5px}
.coach.side-l .arrow,.coach.side-r .arrow{
  border:11px solid transparent;margin:0;
  animation:coachBobX .9s ease-in-out infinite;
}
.coach.side-l .arrow{border-right-color:var(--ink);--nudge:-6px}
.coach.side-r .arrow{border-left-color:var(--ink);--nudge:6px}
@keyframes coachBobX{0%,100%{transform:translateX(0)}50%{transform:translateX(var(--nudge,-6px))}}
@keyframes coachBob{0%,100%{transform:translateY(0)}50%{transform:translateY(6px)}}
/* A coach mark points at something in the garden, so an open sheet has covered
   the thing it is pointing at — it was floating over the panel's own title.
   Declarative and keyed off the sheet being open, like .sheet-art, so no close
   path can forget it. The coach follows the sheet in the DOM, hence `~`. */
.sheet.open ~ .coach{display:none}

/* =========================================================
   RESPONSIVE
   ========================================================= */
@media (max-height:700px){
  .dock-btn{min-height:50px;padding:5px 4px}
  .dock.five{grid-template-rows:50px}
  .dock.five .dock-btn{height:50px}
  .dock.five .dock-btn.home{height:64px}
  /* The pedestal's rise drops with the dock, so the raised Garden button still
     clips a creature's foot rather than its shin. */
  .critter-yard{--pet-lift:4px}
  .dock-ico{width:22px;height:22px}
  .dock-label{font-size:11px}
  .dock.five .dock-label{font-size:9.5px}
  .wallet{font-size:14px;padding:4px 9px 4px 5px}
  .w-icon{width:19px;height:19px}
}
@media (max-height:600px){
  .rail{display:none}
}
/* The HUD tightens on a narrow screen, and it has to: with the year meter beside
   them, three wallets and three round buttons do not fit 390px at full size —
   they wrap, and the wrap comes and goes as the numbers grow. Same values the
   short-screen block above uses, promoted to apply on narrow WIDTH too. */
/* The HUD tightens on a narrow screen. The wallets still compress — a "880.2K"
   coin pill is the widest thing up here — but the ROUND BUTTONS ARE BACK TO
   44px as of phase 3.5, and that is a debt this phase collected rather than a
   new decision.

   What stood here before: with the year meter beside them, three wallets
   (222px) and three 44px buttons (144px) plus the 8px gap came to 374px of the
   370 available. Four pixels over. The buttons dropped to 40 — under the touch
   minimum — and the note said in prose that THE REAL FIX was one fewer HUD
   button, and that it was the owner's call to make.

   The owner made it. The meter pill moved into the dock's Turn button and the
   album star moved to Cards, so the row is two wallets and two buttons: about
   234px of the 340 available at 360px wide, with the worst realistic numbers in
   it. There is nothing left to buy the four pixels from, and nothing left that
   needs them. */
@media (max-width:430px){
  .wallet{font-size:14px;padding:4px 9px 4px 5px}
  .w-icon{width:19px;height:19px}
}
@media (min-width:600px) and (min-height:760px){
  .ui{padding-left:calc(var(--sal) + 16px);padding-right:calc(var(--sar) + 16px)}
}
@media (orientation:landscape) and (max-height:560px){
  .rail{display:none}
  .sheet{height:94dvh;border-radius:22px 22px 0 0}
  .dock-btn{min-height:44px}
  .dock-label{font-size:10px}
}

/* ---------- apiary + apothecary ---------- */
.card.wide{width:100%;margin-bottom:10px}

.wide-btn{
  display:block;width:100%;margin:0 0 12px;padding:11px;
  background:linear-gradient(180deg,#ffe9a8,#ffc93c);
  border:3px solid var(--ink);border-radius:var(--r-md);
  box-shadow:0 4px 0 var(--ink-2), 0 7px 12px rgba(44,26,16,.16);
  font-weight:900;font-size:14px;
  transition:transform .09s ease, box-shadow .09s ease;
}
.wide-btn:active{transform:translateY(4px);box-shadow:0 0 0 var(--ink-2)}

.hive{
  padding:10px 11px 9px;margin-bottom:10px;
  background:linear-gradient(180deg,#fff,#fff5e2);
  border:3px solid var(--ink);border-radius:var(--r-md);
  box-shadow:0 4px 0 var(--ink-2), 0 7px 12px rgba(44,26,16,.14);
}
.hive.has{background:linear-gradient(180deg,#fffdf0,#ffefc4)}
.hive-top{display:flex;align-items:center;gap:9px}
.hive-ico{width:38px;height:38px;flex:none}
.hive-ico svg{width:100%;height:100%;display:block}
.hive-info{flex:1;min-width:0}
.hive-info .card-sub{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hive-count{font-weight:900;font-size:15px;font-variant-numeric:tabular-nums;opacity:.75}

.jars{display:flex;gap:6px;margin:9px 0 7px}
.jar{
  flex:1;aspect-ratio:1;max-width:42px;
  display:grid;place-items:center;
  border:2.5px dashed rgba(44,26,16,.28);border-radius:11px;
  background:rgba(44,26,16,.04);
}
.jar.on{
  border-style:solid;border-color:var(--ink);
  background:linear-gradient(180deg,#ffe9a8,#ffc93c);
  box-shadow:0 2px 0 var(--ink-2);
  animation:jarPop .3s ease-out;
}
.jar svg{width:78%;height:78%;display:block}
@keyframes jarPop{from{transform:scale(.4)}to{transform:scale(1)}}

.hive-foot{display:flex;align-items:center;justify-content:space-between;gap:8px}
.hive-foot .card-sub{margin:0}

.mini{
  flex:none;min-height:42px;padding:0 14px;gap:5px;
  display:inline-flex;align-items:center;justify-content:center;
  border:3px solid var(--ink);border-radius:999px;
  background:#fff;font-weight:800;font-size:13px;line-height:1;
  font-variant-numeric:tabular-nums;
  box-shadow:0 3px 0 var(--ink-2);
  transition:transform .08s ease, box-shadow .08s ease;
}
/* The button is already a cream capsule with a contour and a lip, so the gold it
   hands over is in a pill without needing one of its own. */
.mini svg{width:15px;height:15px;flex:none}
.mini:active{transform:translateY(3px);box-shadow:0 0 0 var(--ink-2)}
.mini.go{background:linear-gradient(180deg,#b2f2bb,#69db7c)}
.mini[disabled]{filter:grayscale(.6) opacity(.55);box-shadow:0 2px 0 var(--ink-2)}

.stock{
  display:flex;align-items:center;gap:9px;
  padding:8px 10px;margin-bottom:7px;
  background:#fff;border:3px solid var(--ink);border-radius:var(--r-md);
  box-shadow:0 3px 0 var(--ink-2);
}
.stock-ico{width:30px;height:30px;flex:none;display:grid;place-items:center}
.stock-ico svg,.stock-ico .bloom{width:100%;height:100%;display:block}
.stock-name{flex:1;min-width:0;font-weight:800;font-size:13.5px;line-height:1.15}
.stock-sub{display:block;font-size:11px;font-weight:700;opacity:.6;margin-top:1px}
.stock-sub svg{width:11px;height:11px;vertical-align:-1px;margin-right:2px}
.stock-qty{font-weight:900;font-size:14px;font-variant-numeric:tabular-nums;opacity:.7}

.brew{
  display:flex;align-items:center;gap:9px;
  padding:8px 10px;margin-bottom:7px;
  background:linear-gradient(180deg,#fff,#eaf6ff);
  border:3px solid var(--ink);border-radius:var(--r-md);
  box-shadow:0 3px 0 var(--ink-2);
}

.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.chip{
  display:inline-flex;align-items:center;gap:4px;
  padding:4px 9px 4px 5px;
  background:#fff;border:2.5px solid var(--ink);border-radius:999px;
  box-shadow:0 2px 0 var(--ink-2);
  font-weight:800;font-size:12.5px;font-variant-numeric:tabular-nums;
}
.chip svg,.chip .bloom{width:20px;height:20px;display:block}

.recipe .needs{display:flex;flex-wrap:wrap;gap:5px;margin:2px 0 4px}
.need{
  padding:3px 8px;border-radius:999px;
  font-size:11.5px;font-weight:800;
  background:rgba(255,107,107,.16);color:#a33;
}
.need.ok{background:rgba(81,207,102,.2);color:#2b7a3d}
.recipe .mini{align-self:flex-start}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.08s !important}
  .cloud{display:none}
}

/* ============ THE CARD ALBUM ============
   Card art is a slot: .card-motif composes a placeholder from the icon vocabulary, .card-img would
   carry a real illustration. Both sit in the same box, so swapping one for the other changes
   nothing about the layout.

   BROUGHT ONTO THE HOUSE MATERIAL 2026-08-30. The album shipped on 15 August and the standard
   hardened on the 26th, so these screens were the last place in the game still drawn the old way:
   flat pastel tiles on cream paper — no figure against ground, no lip on anything, numbers set
   loose on coloured bars. The fix is doc 05's own first check, applied literally: the tiles and the
   card faces become the DARK BODY tier, in the Tally plate's ramp, because that ramp exists for
   exactly this case — a dark body standing on cream inside a sheet. The set's identity moves onto
   its ring, where a tint belongs: a badge, not a wall. */
.album-head{margin:2px 0 12px}
.album-lede{font-size:12.5px;font-weight:700;line-height:1.4;margin:0 0 8px;color:var(--ink-soft)}
/* The Almanac's meter, at album scale: an ink contour, a sunk track, a fill that
   runs green into gold, and the count in a cream capsule rather than painted on
   the bar. */
.album-bar,.set-bar{
  position:relative;display:block;height:24px;border-radius:999px;
  background:rgba(44,26,16,.1);border:2.5px solid var(--ink);overflow:hidden;
}
/* THE RAMP BELONGS TO THE TRACK, NOT THE FILL — and CLIPPING is what delivers
   that, not scaling. A narrow `width` redraws the whole green-to-gold run inside
   the sliver, so 8% looks as finished as 100%; `transform:scaleX()` does exactly
   the same thing, because a transform scales an element's painted output. Only
   a clip leaves the gradient where it was and hides the rest, so a part-full bar
   shows its green end and nothing else. `.q-bar i` and `.almanac-meter i` clip
   the same way, under the name `--fill`. */
.album-bar i,.set-bar i{
  position:absolute;inset:0;width:100%;border-radius:999px;
  background:linear-gradient(90deg,#8ce99a,#ffe066);
  clip-path:inset(0 calc(100% - var(--p,0%)) 0 0);
}
.album-bar b,.set-bar b{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  padding:0 7px;border-radius:999px;
  border:2px solid var(--ink);
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 2px 0 var(--ink-2);
  font-size:9.5px;font-weight:900;color:var(--ink);letter-spacing:.02em;line-height:1.25;
}

.set-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin-top:12px}
/* THE DARK BODY, and the whole reason this screen now reads. Twelve pale tints
   on pale cream had no figure and no ground; the same twelve tints on a dark
   body do, and the tint is still there — it moved to the ring. The ramp is the
   Tally plate's, already documented in doc 05, because a dark body standing on
   cream inside a sheet is the same problem the plate solved. */
.set-tile{
  display:flex;flex-direction:column;align-items:center;gap:6px;
  padding:11px 9px;border-radius:var(--r-md);
  border:3px solid var(--ink);
  background:
    repeating-linear-gradient(96deg, rgba(255,255,255,.06) 0 3px, transparent 3px 14px),
    radial-gradient(circle at 26% 22%, rgba(255,255,255,.07) 0 9%, transparent 10%),
    radial-gradient(circle at 72% 62%, rgba(0,0,0,.07) 0 7%, transparent 8%),
    linear-gradient(180deg,#8a5a33 0%, #71472a 46%, #5c3a22 100%);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.18),
    inset 0 -7px 0 rgba(0,0,0,.2),
    0 4px 0 #4a2e1a,
    0 8px 14px rgba(44,26,16,.24);
  color:#fff4de;
  touch-action:manipulation;
}
.set-tile:active{
  transform:translateY(4px);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.18),
    inset 0 -7px 0 rgba(0,0,0,.2),
    0 0 0 #4a2e1a,
    0 4px 8px rgba(44,26,16,.22);
}
/* A finished set wears a ring, and the ring RESTATES the whole stack — an
   `outline` cannot carry a lip, and a box-shadow modifier that forgets one
   deletes it in the state everybody is looking at. Green because green already
   means yes everywhere in this game. */
.set-tile.done{
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.18),
    inset 0 -7px 0 rgba(0,0,0,.2),
    0 4px 0 #4a2e1a,
    0 8px 14px rgba(44,26,16,.24),
    0 0 0 4px #51cf66;
}
/* …and it still presses. `.done` is later than `:active` and the same
   specificity, so without this the finished tile slid down 4px with its full
   lip still under it — the one tile a player has actually completed. */
.set-tile.done:active{
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.18),
    inset 0 -7px 0 rgba(0,0,0,.2),
    0 0 0 #4a2e1a,
    0 4px 8px rgba(44,26,16,.22),
    0 0 0 4px #51cf66;
}
/* The set's tint, under the white veil that lets a saturated token be used as a
   pale surface — the same move .seed-art makes for nineteen seed colours. */
.set-ring{
  width:44px;height:44px;border-radius:50%;flex:none;
  display:grid;place-items:center;
  /* A HIGHLIGHT, NOT A WASH. `.seed-art`'s full veil is tuned for saturated seed
     colours; four of the twelve set tints are already near-cream, and veiling
     them made four identical white discs — doc 05's own named failure, on the
     one badge that now carries the set's identity. */
  background:
    radial-gradient(circle at 34% 28%, rgba(255,255,255,.5), transparent 62%),
    var(--set);
  border:3px solid var(--ink);box-shadow:0 3px 0 var(--ink-2);
}
.set-ring svg{width:22px;height:22px}
.set-name{font-size:12px;font-weight:900;text-align:center;line-height:1.2;min-height:2.4em;display:grid;align-items:center}
.set-tile .set-bar{width:100%}

/* The set's own header. The tint is the BODY, veiled, the way .verb-note wears a
   verb's colour — a saturated token used as a pale surface, so ink stays legible
   across all twelve. */
.setbar{
  display:flex;align-items:center;gap:10px;
  padding:9px 11px;border-radius:var(--r-sm);margin-bottom:12px;
  border:3px solid var(--ink);
  background:linear-gradient(180deg, rgba(255,255,255,.7), rgba(255,255,255,.46)), var(--set);
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.24);
}
.setbar small{font-size:11px;font-weight:700;color:var(--ink-soft)}

.card-grid9{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:14px}
.cardcell{position:relative;display:flex;flex-direction:column;align-items:center;gap:3px;touch-action:manipulation}
.cstars{display:flex;gap:1px;justify-content:center}
/* An unearned star is DRAINED, not faded — the value comes out of the glyph the
   way the meadow's locked land desaturates. Its gold is painted into the path,
   so `color` never reached it; the old rule setting one had never applied. */
.cstar svg{width:12px;height:12px;filter:saturate(.15) brightness(1.06)}
.cstar.on svg{filter:none}
/* A card face is the dark body tier, like its tile. Unowned it is the same
   object drained, so the grid reads as nine cards of which some are filled —
   never as nine holes. */
.cardface{
  position:relative;width:100%;aspect-ratio:3/4;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
  padding:6px 4px;border-radius:var(--r-sm);
  border:3px solid var(--ink);
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  box-shadow:
    inset 0 4px 0 rgba(255,255,255,.4),
    inset 0 -6px 0 rgba(44,26,16,.07),
    0 4px 0 var(--paper-dim-edge),
    0 8px 14px rgba(44,26,16,.14);
  color:var(--ink-soft);
}
/* box-shadow is ONE property, so every line of the base stack is restated here.
   Writing the lip alone deleted the lit top edge of every card the player
   actually owns — half the recipe gone in the only state that matters. */
.cardcell.have .cardface{
  background:
    repeating-linear-gradient(96deg, rgba(255,255,255,.06) 0 3px, transparent 3px 14px),
    radial-gradient(circle at 26% 22%, rgba(255,255,255,.07) 0 9%, transparent 10%),
    radial-gradient(circle at 72% 62%, rgba(0,0,0,.07) 0 7%, transparent 8%),
    linear-gradient(180deg,#8a5a33 0%, #71472a 46%, #5c3a22 100%);
  box-shadow:
    inset 0 4px 0 rgba(255,255,255,.18),
    inset 0 -6px 0 rgba(0,0,0,.2),
    0 4px 0 #4a2e1a,
    0 8px 14px rgba(44,26,16,.24);
  color:#fff4de;
}
.cardcell:active .cardface{
  transform:translateY(3px);
  box-shadow:
    inset 0 4px 0 rgba(255,255,255,.18),
    inset 0 -6px 0 rgba(0,0,0,.2),
    0 0 0 #4a2e1a,
    0 4px 8px rgba(44,26,16,.22);
}
.cardname{font-size:9px;font-weight:800;text-align:center;line-height:1.15;padding:0 2px}
/* The motif disc is the card's one piece of colour, and it stands on the face
   rather than floating: veil, ink contour, lip. */
.card-motif{
  display:grid;place-items:center;border-radius:50%;
  background:
    radial-gradient(circle at 34% 28%, rgba(255,255,255,.8), rgba(255,255,255,.34)),
    var(--motif);
  border:3px solid var(--ink);box-shadow:0 2px 0 var(--ink-2);
}
.card-motif svg{width:58%;height:58%}
.card-img{border-radius:var(--r-sm);object-fit:cover;border:3px solid var(--ink);box-shadow:0 2px 0 var(--ink-2)}
/* Every number in a cream pill with a contour and a lip — this one counts your
   spares. */
.cdupe{
  position:absolute;top:14px;right:-2px;
  padding:1px 6px;border-radius:999px;
  border:2px solid var(--ink);
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 2px 0 var(--ink-2);
  font-size:9px;font-weight:900;color:var(--ink);
}

/* The opening. Rarity is telegraphed by the frame before the name is legible —
   and since 2026-08-30 in the RARITY COLOURS the player already knows. It used
   to glow gem-cyan for rare and coin-gold for legendary, which are the two
   currencies, and Wonderstruck pink for mythical. Blue, purple and gold are a
   vocabulary learned once in the garden; spending them on anything else, or
   spending something else on them, is the one thing doc 05 forbids twice. */
.pack-reveal{display:flex;flex-direction:column;align-items:center;gap:8px;padding:6px 0 14px}
.pack-card{
  width:min(62%,210px);aspect-ratio:3/4;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;
  padding:14px;border-radius:var(--r-md);
  border:4px solid var(--ink);
  background:
    repeating-linear-gradient(96deg, rgba(255,255,255,.06) 0 3px, transparent 3px 14px),
    radial-gradient(circle at 26% 22%, rgba(255,255,255,.07) 0 9%, transparent 10%),
    radial-gradient(circle at 72% 62%, rgba(0,0,0,.07) 0 7%, transparent 8%),
    linear-gradient(180deg,#8a5a33 0%, #71472a 46%, #5c3a22 100%);
  /* THE RING IS ITS OWN PROPERTY, not another line in this stack. `box-shadow`
     is one property and `.pack-reveal:not(.is-new) .pack-card` and
     `.pack-reveal.r-legend .pack-card` are the same specificity — so the later
     rule won outright and a spare Legendary wore the DARK body's lip and bruise
     on a cream body. The two facts are independent; they get two properties. */
  --pc-ring:0 0 0 0 transparent;
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.2),
    inset 0 -8px 0 rgba(0,0,0,.22),
    0 5px 0 #4a2e1a,
    0 9px 14px rgba(44,26,16,.22),
    var(--pc-ring);
  color:#fff4de;
  animation:packIn .42s cubic-bezier(.2,1.5,.4,1) both;
}
.pack-name{font-size:14px;font-weight:900;text-align:center;line-height:1.2}
.pack-tag{font-size:12px;font-weight:800;margin:0;color:var(--ink)}
/* A spare is DRAINED, not dimmed — the same paper the idle upgrade card and the
   locked seed row wear, with its lip restated so the card keeps its material. */
.pack-reveal:not(.is-new) .pack-card{
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  /* A spare keeps its star row and loses its ring — the rung is still readable,
     the celebration is not repeated. */
  --pc-ring:0 0 0 0 transparent;
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.4),
    inset 0 -8px 0 rgba(44,26,16,.07),
    0 5px 0 var(--paper-dim-edge),
    0 9px 14px rgba(44,26,16,.14),
    var(--pc-ring);
  color:var(--ink);
}
.pack-reveal:not(.is-new) .pack-tag{color:var(--ink-soft)}
/* Every rarity ring restates the whole stack, lip included. Common and Uncommon
   deliberately get none: the ladder says a common thing does not announce
   itself. Mythical is the top of a FIVE-rung ladder where the garden has four,
   so rather than borrow purple — which a player has already learned means Epic,
   one rung DOWN — it wears legendary gold said twice: a solid ring, a wider
   glow, and the saturate breath. */
.pack-reveal.is-new.r-rare .pack-card{--pc-ring:0 0 0 4px var(--rare), 0 0 22px 3px rgba(77,171,247,.7)}
.pack-reveal.is-new.r-legend .pack-card{--pc-ring:0 0 0 4px var(--legend), 0 0 26px 5px rgba(255,212,59,.8)}
.pack-reveal.is-new.r-mythic .pack-card{
  --pc-ring:0 0 0 5px var(--legend), 0 0 40px 10px rgba(255,212,59,.85);
  animation:mythicPulse 1.6s ease-in-out infinite;
}
@keyframes packIn{0%{transform:scale(.7) rotate(-4deg);opacity:0}100%{transform:none;opacity:1}}
@keyframes mythicPulse{0%,100%{filter:saturate(1)}50%{filter:saturate(1.45)}}
@media (prefers-reduced-motion:reduce){
  /* The mythic cancel has to match the selector that sets the animation, or it
     loses on specificity and the only thing stopping the pulse is the global
     clamp — which is the "a collapsed animation is not a guard" mistake this
     round went to the trouble of writing down. */
  .pack-card{animation:none}
  .pack-reveal.is-new.r-mythic .pack-card{animation:none}
}
.setbar-txt{flex:1;display:flex;flex-direction:column;gap:5px;font-size:12px;font-weight:800}
.setbar-txt .set-bar{width:100%}

/* A pack that turned up in the garden. Borrows the Lucky Ladybug's arrival beat — something lands
   and asks to be collected — but is tappable, because collecting it is the point. */
.pack-drop{
  position:absolute;top:-9px;left:50%;z-index:5;
  width:40px;height:40px;padding:4px;
  transform:translateX(-50%);
  display:none;place-items:center;
  border-radius:50%;border:3px solid var(--ink);
  background:radial-gradient(circle at 34% 28%, #fffdf7, var(--paper-3));
  box-shadow:0 3px 0 var(--ink-2), 0 6px 10px rgba(44,26,16,.18), 0 0 16px 3px rgba(255,224,102,.85);
  touch-action:manipulation;pointer-events:none;
}
/* Visibility comes from `display`, never from the animation — a badge that only exists once a
   keyframe has run is invisible and uncollectable anywhere the animation does not play. */
.plot.has-pack .pack-drop{
  display:grid;
  pointer-events:auto;
  animation:packLand .5s cubic-bezier(.2,1.6,.4,1) both, packBob 1.9s ease-in-out .5s infinite;
}
.pack-drop svg{width:100%;height:100%}
.pack-drop:active{filter:brightness(.94)}
@keyframes packLand{
  0%{transform:translateX(-50%) translateY(-26px) scale(.3) rotate(-20deg);opacity:0}
  100%{transform:translateX(-50%) translateY(0) scale(1) rotate(0);opacity:1}
}
@keyframes packBob{
  0%,100%{transform:translateX(-50%) translateY(0) rotate(-4deg)}
  50%{transform:translateX(-50%) translateY(-5px) rotate(4deg)}
}
@media (prefers-reduced-motion:reduce){
  .plot.has-pack .pack-drop{animation:none;transform:translateX(-50%) scale(1)}
}

/* ============ creatures ============ */
/* They live on the lawn under the garden board, not on it — plots are for
   planting, and a creature standing on one would read as something to harvest.
   Absolute, so the yard overlays the lawn instead of adding a row that pushes
   the whole stage down past the dock. */
.critter-yard {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--yard-h);
  pointer-events: none;
  z-index: 3;
}
/* Phase 3.5: the creatures keep their feet on the lawn. Lifting them clear of
   the band's two buttons was tried and it broke doc 05's anchoring rule — a
   creature 64px up reads as floating, because the lawn it belongs to is still
   down there. So they stand where they stood, the two floating buttons paint
   over them at z-index 4, and a creature tucked behind a button is depth rather
   than a collision — the same thing the burrow mouth did before it retired.
   The 6px is the pedestal's rise, so the raised Garden button clips a foot
   rather than a shin. */
.critter-yard { --pet-lift: 6px; }
.critter {
  position: absolute;
  left: var(--x, 50%);
  bottom: var(--pet-lift, 2px);
  /* Trimmed from 19vw when the band took the ends of the yard (phase 3.5).
     Four creatures and two floating buttons share 370px; at 19vw the outer two
     were half behind a button and each adjacent pair overlapped by 30px. */
  width: clamp(60px, 17vw, 88px);
  height: clamp(60px, 17vw, 88px);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  transform: translateX(-50%) scale(0.6);
  opacity: 0;
  transition: transform .5s cubic-bezier(.34,1.56,.64,1), opacity .3s ease-out;
  pointer-events: auto;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* Presence comes from a class toggle, never from a keyframe finishing — an
   element that only appears once an animation has run is uncollectable
   wherever that animation does not play. */
.critter.here { transform: translateX(-50%) scale(1); opacity: 1; }

.critter-svg {
  width: 100%;
  height: 100%;
  overflow: visible;
  display: block;
  animation: critter-float 3.6s ease-in-out infinite;
}
/* The head-tilt is the whole personality. Slow, irregular, and never while the
   player is touching it. */
.critter .cr-body, .hollow-pet .cr-body { transform-origin: 50% 88%; animation: critter-tilt 5.2s ease-in-out infinite; }
.critter .cr-sprout, .hollow-pet .cr-sprout { transform-origin: 50% 90%; animation: critter-tilt 5.2s ease-in-out infinite; }
.critter .cr-glow, .hollow-pet .cr-glow { animation: critter-glow 2.8s ease-in-out infinite; }
.critter .cr-eye, .hollow-pet .cr-eye { transform-origin: center; animation: critter-blink 5.9s infinite; }

@keyframes critter-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6%); }
}
@keyframes critter-tilt {
  0%, 44%, 100% { transform: rotate(0deg); }
  52% { transform: rotate(-7deg); }
  60% { transform: rotate(6deg); }
  68% { transform: rotate(-3deg); }
  76% { transform: rotate(0deg); }
}
@keyframes critter-glow {
  0%, 100% { opacity: .28; transform: scale(1); }
  50% { opacity: .46; transform: scale(1.06); }
}
@keyframes critter-blink {
  0%, 92%, 100% { transform: scaleY(1); }
  95% { transform: scaleY(0.08); }
}

.critter.bop .critter-svg { animation: critter-bop .42s cubic-bezier(.34,1.56,.64,1); }
@keyframes critter-bop {
  0% { transform: translateY(0) scale(1, 1); }
  30% { transform: translateY(-18%) scale(0.92, 1.12); }
  60% { transform: translateY(0) scale(1.1, 0.9); }
  100% { transform: translateY(0) scale(1, 1); }
}


.critter-gift {
  position: absolute;
  top: -4px;
  right: -2px;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border: 3px solid var(--ink, var(--ink));
  border-radius: 999px;
  background: #ffd43b;
  box-shadow: 0 2px 0 var(--ink-2);
  animation: critter-gift-bob 1.5s ease-in-out infinite;
}
.critter-gift[hidden] { display: none; }
.critter-gift svg { width: 14px; height: 14px; }
@keyframes critter-gift-bob {
  0%, 100% { transform: translateY(0) rotate(-6deg); }
  50% { transform: translateY(-4px) rotate(6deg); }
}
.critter.has-gift .critter-svg { animation: critter-float 1.9s ease-in-out infinite; }

.critter-motes { position: absolute; inset: 0; pointer-events: none; }
.cr-mote {
  position: absolute;
  left: 50%;
  bottom: 24%;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  opacity: 0;
  animation: critter-mote 3.4s ease-out infinite;
  animation-delay: calc(var(--i) * 1.1s);
}
@keyframes critter-mote {
  0% { opacity: 0; transform: translate(-50%, 0) scale(.4); }
  25% { opacity: .9; }
  100% { opacity: 0; transform: translate(calc(-50% + (var(--i) - 1) * 26px), -46px) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .critter-svg, .critter .cr-body, .critter .cr-sprout,
  .critter .cr-glow, .critter .cr-eye, .cr-mote, .critter-gift,
  .hollow-pet .cr-body, .hollow-pet .cr-sprout, .hollow-pet .cr-glow, .hollow-pet .cr-eye {
    animation: none;
  }
  /* Loadout mode still dims a resting creature; it just stops fading into it. */
  .in-loadout .hollow-pet { transition: none; }
}

/* ---------- the habitat, in the Almanac ---------- */
.stat-note{margin:0 0 8px;font-size:12px;color:var(--ink-soft)}
.critter-row{
  display:flex;align-items:center;gap:10px;
  padding:8px;margin-bottom:6px;
  border:3px solid var(--ink);border-radius:14px;
  background:var(--paper-2);
}
.critter-row.tending{background:#e6f7e2;box-shadow:inset 0 0 0 2px #8ce99a}
.critter-row.dim{opacity:.62}
.critter-row.dim .critter-face svg{filter:grayscale(1)}
.critter-face{flex:0 0 46px;width:46px;height:46px}
.critter-face svg{width:100%;height:100%;overflow:visible}
/* Nothing in a list row should be animating — the yard is where they are alive. */
.critter-row .critter-svg,
.critter-row .cr-body,
.critter-row .cr-sprout,
.critter-row .cr-glow,
.critter-row .cr-eye{animation:none}
.critter-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.critter-who{font-size:14px;font-weight:800;color:var(--ink)}
.critter-who em{font-style:normal;font-weight:700;font-size:12px;color:var(--ink-soft)}
.critter-note{font-size:11.5px;color:var(--ink-soft);line-height:1.35}
.critter-trait{display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--ink);line-height:1.3}
.critter-trait svg{width:13px;height:13px;flex:0 0 13px}
.critter-trait b{white-space:nowrap}
.critter-toggle{
  flex:0 0 auto;
  padding:6px 11px;
  border:3px solid var(--ink);border-radius:999px;
  background:var(--paper);color:var(--ink);
  font-family:inherit;font-weight:800;font-size:12px;
  cursor:pointer;box-shadow:0 3px 0 var(--ink-2);
}
.critter-toggle[data-on="1"]{background:#8ce99a}
.critter-toggle:active{transform:translateY(2px);box-shadow:0 1px 0 var(--ink-2)}
.critter-toggle:disabled{opacity:.45;cursor:default;box-shadow:none}

/* A tending creature wears a leaf in the yard, so the state is visible without
   opening a panel. */
.critter-leaf{
  position:absolute;top:-2px;left:-2px;
  width:22px;height:22px;display:grid;place-items:center;
  border:3px solid var(--ink);border-radius:999px;
  background:#8ce99a;box-shadow:0 2px 0 var(--ink-2);
}
.critter-leaf[hidden]{display:none}
.critter-leaf svg{width:12px;height:12px}

/* ---------- sleeping ----------
   A pet that has run out of food is asleep, not gone. That distinction is the
   whole reason an upkeep timer is survivable in a cosy game, so it has to READ
   as sleeping: shut eyes, drifting Zs, and the creature still right there. */
.cr-eyes-shut{display:none}
.cr-zzz{display:none}
.asleep .cr-eyes{display:none}
.asleep .cr-eyes-shut{display:block}
.asleep .cr-zzz{display:block}
/* The idle personality stops; only the breath and the Zs keep moving. */
.asleep .cr-body,.asleep .cr-sprout,.asleep .cr-eye,.asleep .cr-wings,.asleep .cr-tail{
  animation:none !important
}
.asleep .cr-glow{animation:none;opacity:.16}
.asleep .cr-mouth{opacity:.35}
.asleep .critter-svg{animation:cr-breathe 3.6s ease-in-out infinite;transform-origin:50% 92%}
@keyframes cr-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.028)}}
/* Visible at rest, drifting as a flourish on top. A Z that only exists once a
   keyframe has run is invisible anywhere the animation does not play — the same
   rule the pack badge is on, and it caught this feature too. */
.cr-z{
  opacity:.72;transform-origin:50% 50%;
  animation:cr-zfloat 4.8s ease-in-out infinite;
  animation-delay:calc(var(--i) * -1.6s);
}
/* A zigzag rise, and no scaling. The first pass slid each Z out to one side and
   grew it, which reads as a graphic being pushed rather than as something
   drifting off a sleeping animal. Sway one way, then the other, then away. */
@keyframes cr-zfloat{
  0%{opacity:0;transform:translate(0,3px)}
  14%{opacity:.75}
  32%{transform:translate(-3.5px,-6px)}
  56%{transform:translate(3.5px,-13px)}
  80%{opacity:.5;transform:translate(-2.5px,-20px)}
  100%{opacity:0;transform:translate(1px,-27px)}
}
/* Sleeping is a state, never something a keyframe has to finish to establish —
   the same rule the pack badge is on. */
@media (prefers-reduced-motion:reduce){
  .cr-z{animation:none;opacity:.8}
  .asleep .critter-svg{animation:none}
}

/* ---------- the sheet's breakout art ----------
   Whoever the panel is about stands ON the sheet rather than being filed inside
   it. Absolutely positioned on `.sheet` and not in the scrolling body, so it
   rides the open/close transform and then holds still while the body scrolls. */
.sheet-art{
  /* Sunk well into the sheet, not perched on it. A creature that only touches
     the top edge reads as a sticker; one whose body disappears behind the panel
     reads as standing there. The creature art also leaves ~7% empty below the
     body inside its own viewBox, so the overlap has to cover that first. */
  position:absolute;left:0;right:0;bottom:calc(100% - 52px);
  height:168px;z-index:1;
  display:grid;place-items:end center;
  pointer-events:none;
}
.sheet-art[hidden]{display:none}
/* Tied to the sheet being OPEN, not to a JS call. The art rides the sheet's
   transform, and a closed sheet parks just below the bottom edge — which put a
   creature's head over the dock until this existed. Declarative, so no close
   path can forget it. */
.sheet-art{opacity:1;transition:opacity .16s ease}
.sheet:not(.open) .sheet-art{
  opacity:0;visibility:hidden;
  transition:opacity .14s ease, visibility 0s .14s;
}
.sheet-art::before{
  content:'';position:absolute;left:50%;bottom:14px;transform:translateX(-50%);
  width:290px;height:220px;border-radius:50%;
  background:radial-gradient(ellipse at 50% 58%, var(--cg,#b6f2c8) 0%, transparent 64%);
  opacity:.72;
}
.sheet-art svg{
  position:relative;width:182px;height:182px;overflow:visible;
  filter:drop-shadow(0 8px 0 rgba(44,26,16,.2));
  animation:cp-hover 4.4s ease-in-out infinite;
}
@keyframes cp-hover{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
/* Asleep is asleep everywhere. It should not be hovering, and the Zs belong to
   the room, not to a portrait the size of a face. */
.sheet-art.asleep svg{animation:none}
.sheet-art.asleep .cr-zzz{display:none}
@media (prefers-reduced-motion:reduce){ .sheet-art svg{animation:none} }
@media (max-height:700px){
  .sheet-art{height:124px;bottom:calc(100% - 40px)}
  .sheet-art svg{width:136px;height:136px}
  .sheet-art::before{width:210px;height:165px;bottom:10px}
}

/* ---------- one creature, and everything you can do to it ---------- */
.cp-plate{text-align:center;margin:0 0 8px;padding-top:4px}
.cp-name{
  margin:0;font-size:clamp(26px,7.4vw,34px);font-weight:800;letter-spacing:.02em;line-height:1.05;
}
.cp-species{
  margin:1px 0 0;font-size:12.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink-soft);
}
.cp-plate .critter-stars{margin:4px 0 0;gap:3px}
.cp-plate .cr-star svg{width:19px;height:19px}
.cp-plate.asleep .cp-name{opacity:.75}

/* The level, laid out as the owner sketched it: a rounded-square bloom token,
   a bar tall enough to stack the count AND its caption inside, and the star with
   its number IN it rather than beside it. The caption filling the bar's own
   height is the point — beside a short bar it would have cost another row. */
.cp-grow{
  display:flex;align-items:center;gap:9px;
  margin:0;padding:8px;
  border:3px solid var(--ink);border-radius:18px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-3));
  box-shadow:0 3px 0 var(--ink-2);
}
.cp-token{
  flex:0 0 50px;width:50px;height:50px;position:relative;
  display:grid;place-items:center;
  border:3px solid var(--ink);border-radius:14px;
  background:radial-gradient(circle at 36% 28%, #fffdf7, var(--paper-3));
  box-shadow:0 3px 0 var(--ink-2);
}
.cp-token .bloom{width:34px;height:34px;display:block}
/* The number lives inside the star. Two glyphs at most, so it always fits. */
/* The star wants a big flat middle for the number to sit in. The shared icon is
   drawn with a 2px stroke in a 24-unit box, which at this size is a fat band
   that the digit ran straight into — thinned here, and the number carries a
   white halo so it separates from the gold as well as from the outline. */
.cp-goal{background:linear-gradient(180deg,#fff8d6,#ffe9a8)}
.cp-goal svg{width:46px;height:46px;display:block;stroke-width:1.3}
.cp-goal b{
  position:absolute;inset:0;display:grid;place-items:center;
  padding-top:3px;font-size:16px;font-weight:800;line-height:1;color:var(--ink);
  text-shadow:0 1px 0 rgba(255,255,255,.9),0 -1px 0 rgba(255,255,255,.75),
              1px 0 0 rgba(255,255,255,.75),-1px 0 0 rgba(255,255,255,.75);
}
.cp-bar{
  position:relative;flex:1;min-width:0;
  height:50px;border:3px solid var(--ink);border-radius:14px;
  background:#e3d6bd;overflow:hidden;
}
.cp-bar i{
  display:block;height:100%;
  background:linear-gradient(180deg,#b2f2bb,#51cf66);
  box-shadow:inset 0 -6px 0 rgba(44,26,16,.13);
  transition:width .4s cubic-bezier(.22,1,.28,1);
}
.cp-bar-text{
  position:absolute;inset:0;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:1px;
  pointer-events:none;text-align:center;padding:0 6px;
}
/* Dark ink with a white halo, not the house white-on-ink outline: at this size
   the eight offset copies crowd the glyphs into mud, and a halo stays clean
   over both the filled and the empty half. */
.cp-bar-text b{
  font-size:15px;font-weight:800;line-height:1;color:var(--ink);letter-spacing:.02em;
  text-shadow:0 1px 0 rgba(255,255,255,.95),0 -1px 0 rgba(255,255,255,.8),
              1px 0 0 rgba(255,255,255,.8),-1px 0 0 rgba(255,255,255,.8);
}
.cp-bar-text em{
  font-style:normal;font-size:9.5px;font-weight:800;line-height:1.1;
  letter-spacing:.05em;text-transform:uppercase;color:var(--ink);opacity:.66;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;
  text-shadow:0 1px 0 rgba(255,255,255,.8);
}
.cp-grow.maxed .cp-bar i{background:linear-gradient(180deg,#ffe9a8,#ffc93c)}

/* ONE meter, because there is one clock, with a pip where the star lapses.
   Above the pip: well fed and working a star up. Below it: awake but hungry, and
   sleep is coming. The pip sits low by design — a warning line, not a climb. */
.cp-fuel{
  margin-bottom:9px;padding:9px 11px 10px;
  border:3px solid var(--ink);border-radius:16px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-3));
  box-shadow:0 3px 0 var(--ink-2);
}
.cp-fuel-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:16px}
.cp-fuel-k{
  display:inline-flex;align-items:center;gap:6px;
  font-size:12px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--ink);
}
.cp-fuel-k svg{width:15px;height:15px;flex:0 0 15px}
.cp-fuel-v{font-size:11.5px;font-weight:800;color:var(--ink-soft);white-space:nowrap}
.cp-fuel-bar{
  position:relative;height:22px;
  border:3px solid var(--ink);border-radius:999px;background:#e3d6bd;
}
.cp-fuel-bar i{
  display:block;height:100%;border-radius:999px;
  background:linear-gradient(180deg,#b2f2bb,#51cf66);
  transition:width .4s cubic-bezier(.22,1,.28,1);
}
.cp-fuel.on .cp-fuel-bar i{background:linear-gradient(180deg,#ffe9a8,#ffc93c)}
.cp-fuel.out{background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2))}
.cp-fuel.out .cp-fuel-v{color:var(--ink)}
/* The line, marked with the thing it grants rather than with a word. The header
   already names the state — repeating "Well fed" under the bar said it twice and
   read as a caption for the whole bar instead of for the line. */
.cp-pip{position:absolute;top:-3px;bottom:-3px;width:3px;background:var(--ink);border-radius:2px}
.cp-pip svg{
  position:absolute;left:50%;bottom:calc(100% + 2px);transform:translateX(-50%);
  width:15px;height:15px;
  filter:drop-shadow(0 1px 0 rgba(255,255,255,.9));
}

/* Skill and level in one box, because the star IS the trait getting stronger. */
.cp-skill{
  margin-bottom:10px;padding:10px 11px;
  border:3px solid var(--ink);border-radius:18px;
  background:var(--paper-2);
  box-shadow:0 3px 0 var(--ink-2);
}
.cp-skill .cp-card-v{margin-bottom:9px}
.cp-skill .cp-grow{margin:0;border-width:3px}

/* Each fact in its own chip, so the panel reads as a character card rather than
   as a paragraph of state. */
.cp-cards{display:flex;flex-direction:column;gap:6px;margin-bottom:4px}
.cp-card{
  padding:9px 11px;border:3px solid var(--ink);border-radius:14px;
  background:var(--paper-2);
  box-shadow:0 3px 0 var(--ink-2);
}
.cp-card.bad{background:var(--paper-dim);box-shadow:inset 0 0 0 2px var(--paper-dim-edge), 0 3px 0 var(--ink-2)}
.cp-card-k{
  display:flex;align-items:center;gap:6px;
  font-size:12px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--ink);
}
.cp-card-k svg{width:15px;height:15px;flex:0 0 15px}
.cp-card-v{display:block;margin-top:3px;font-size:12.5px;line-height:1.4;color:var(--ink-soft)}
.cp-card .critter-memento,.cp-card .food-state{margin:0}

.critter-panel h3{
  display:flex;align-items:center;gap:7px;
  margin:12px 0 6px;font-size:14px;font-weight:800;color:var(--ink);
}
.critter-panel h3 svg{width:16px;height:16px;flex:0 0 16px}
.cp-head{
  display:flex;align-items:center;gap:12px;
  padding:10px;margin-bottom:8px;
  border:3px solid var(--ink);border-radius:16px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 3px 0 var(--ink-2);
}
.cp-head.asleep{background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2))}
.cp-face{flex:0 0 64px;width:64px;height:64px}
.cp-face svg{width:100%;height:100%;overflow:visible}
/* A portrait is not the yard — it holds still until you pet it. */
.cp-face .cr-body,.cp-face .cr-sprout,.cp-face .cr-glow,.cp-face .cr-eye,
.cp-face .cr-wings,.cp-face .cr-tail,.cp-face .cr-z{animation:none}
.cp-face.asleep .cr-zzz{display:none}
.cp-face.bop .critter-svg{animation:critter-bop .42s cubic-bezier(.34,1.56,.64,1)}
.cp-who{display:flex;flex-direction:column;gap:1px;min-width:0}
.cp-who b{font-size:19px;font-weight:800;line-height:1.1;color:var(--ink)}
.cp-who em{font-style:normal;font-size:12px;font-weight:700;color:var(--ink-soft)}
.cp-who .critter-stars{margin:3px 0 0}
.cp-about{margin:0 0 8px;font-size:12.5px;line-height:1.45;color:var(--ink-soft)}
.cp-said{
  margin:8px 0 0;padding:9px 13px;
  border:3px solid var(--ink);border-radius:16px;
  background:var(--paper);
  font-size:12.5px;font-weight:700;line-height:1.4;color:var(--ink);
  box-shadow:0 3px 0 var(--ink-2);
}
.cp-said.pop{animation:cpSaid .38s cubic-bezier(.34,1.56,.64,1)}
@keyframes cpSaid{0%{transform:scale(.94);opacity:.4}100%{transform:none;opacity:1}}
.critter-panel .critter-trait,
.critter-panel .critter-grow,
.critter-panel .critter-memento,
.critter-panel .food-state{margin-bottom:6px}
@media (prefers-reduced-motion:reduce){
  .cp-said.pop,.cp-face.bop .critter-svg{animation:none}
}

/* ---------- feeding ---------- */
.feed-alert{
  display:flex;align-items:flex-start;gap:7px;margin:0 0 10px;padding:9px 11px;
  border:3px solid var(--ink);border-radius:14px;
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  font-size:12px;font-weight:700;line-height:1.35;color:var(--ink);
  box-shadow:0 3px 0 var(--ink-2);
}
.feed-alert svg{width:15px;height:15px;flex:0 0 15px;margin-top:1px}
.feed-row{flex-direction:column;align-items:stretch;gap:0}
.feed-row.napping{background:var(--paper-dim);box-shadow:inset 0 0 0 2px var(--paper-dim-edge), 0 3px 0 var(--ink-2)}
.food-state.out{color:var(--ink);font-weight:800}
/* A sleeping portrait in a list row gets the shut eyes and nothing else. The Zs
   rise well above the art on purpose, which in a 46px row means drifting up into
   the row above — and nothing in a list should be animating anyway, since the
   yard is where they are alive. The purple row and the copy carry it here. */
.critter-face.asleep .critter-svg{animation:none}
.critter-face.asleep .cr-zzz{display:none}
.feed-top{display:flex;align-items:center;gap:10px}
.feed-row.fed{background:#fff2e0;box-shadow:inset 0 0 0 2px #ffa94d, 0 3px 0 var(--ink-2)}
/* The copy is one flex item, not three. Left as bare text nodes beside the
   icon, each fragment became its own column and the line broke into a grid on
   a narrow screen. */
.food-state{display:flex;align-items:flex-start;gap:5px;font-size:11.5px;line-height:1.35;
  color:var(--ink-soft)}
.food-state svg{width:12px;height:12px;flex:0 0 12px;margin-top:1px}
.food-state > span{flex:1;min-width:0}
.food-state.on{color:var(--ink);font-weight:700}
/* A grid rather than flex-wrap, so three always sit in one row at any width. */
.food-row{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;margin-top:7px}
/* The only buy buttons on this panel, so they get the whole house recipe rather
   than a border and a fill: body gradient, two blemishes, two inset rims, the
   lip, the contact shadow, the ink. On cream the blemish and rim alphas are the
   paper set — black at .10 over cream is a grey smudge, which is the exact
   material mistake this recipe exists to prevent. */
.food-btn{
  min-width:0;
  display:flex;flex-direction:column;align-items:center;gap:5px;
  padding:9px 6px 8px;
  border:3px solid var(--ink);border-radius:14px;
  background:
    radial-gradient(circle at 26% 22%, rgba(255,255,255,.55) 0 12%, transparent 13%),
    radial-gradient(circle at 72% 62%, rgba(44,26,16,.05) 0 9%, transparent 10%),
    linear-gradient(180deg,#fffdf7 0%,var(--paper) 72%,var(--paper-2) 100%);
  color:var(--ink);
  font-family:inherit;cursor:pointer;
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.5),
    inset 0 -7px 0 rgba(44,26,16,.07),
    0 3px 0 var(--ink-2),
    0 6px 10px rgba(44,26,16,.18);
  transition:transform .08s ease, box-shadow .08s ease;
}
.food-btn.affordable{
  background:
    radial-gradient(circle at 26% 22%, rgba(255,255,255,.55) 0 12%, transparent 13%),
    radial-gradient(circle at 72% 62%, rgba(44,26,16,.05) 0 9%, transparent 10%),
    linear-gradient(180deg,#fff8e8 0%,var(--paper-2) 72%,#ffe9c4 100%);
}
.food-btn:active{
  transform:translateY(3px);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.5),
    inset 0 -7px 0 rgba(44,26,16,.07),
    0 0 0 var(--ink-2),
    0 3px 6px rgba(44,26,16,.25);
}
.food-btn:disabled{opacity:.42;cursor:default;box-shadow:none}
/* The food IS the button. A big token with what it gives you stamped on the
   corner, then the name, then the price — icon first, words second. */
.food-ico{
  position:relative;width:52px;height:52px;
  display:grid;place-items:center;
  border:3px solid var(--ink);border-radius:14px;
  background:radial-gradient(circle at 34% 28%, #fffdf7, var(--paper-3));
}
.food-ico svg{width:34px;height:34px;display:block}
.food-ico b{
  position:absolute;right:-7px;bottom:-6px;
  padding:2px 6px;border:2.5px solid var(--ink);border-radius:999px;
  background:linear-gradient(180deg,#fff8d6,#ffd43b);
  font-size:10.5px;font-weight:800;line-height:1.15;color:var(--ink);white-space:nowrap;
}
.food-name{font-size:11px;font-weight:800;line-height:1.1;text-align:center}
/* What the clock reads once the food is in. Quiet ink under the name rather than
   a pill of its own — the button is already a cream capsule, and a third object
   in a 105px column would not fit. */
.food-after{
  font-size:9.5px;font-weight:800;line-height:1.1;text-align:center;
  color:var(--ink-soft);font-variant-numeric:tabular-nums;
}
.food-after.capped{color:#b2601f}
.food-btn .price{margin-top:2px;align-self:center;padding:3px 7px;font-size:11px}

/* ---------- creature stars and growth ---------- */
.critter-stars{display:inline-flex;gap:1px;margin-left:6px;vertical-align:middle}
.cr-star svg{width:11px;height:11px;display:block;color:#ffc94a}
.cr-star.off{opacity:.26}
/* The star a creature is working at while fed, which it has not been raised to. */
.cr-star.fed svg{color:#ff922b}
.cr-star.fed{filter:drop-shadow(0 0 3px rgba(255,169,77,.9))}
.critter-grow{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--ink-soft)}
.critter-grow i{
  flex:0 0 62px;height:7px;border-radius:999px;
  background:#8ce99a;border:2px solid var(--ink);
  transform-origin:left center;
}
.critter-grow span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The glow scales with the creature's star, set from renderCritters(). */
.critter .cr-glow{opacity:calc(0.32 * var(--lvl, 1))}
/* Wings beat; a tail sways. Both are decoration on top of the float. */
.critter .cr-wings,.hollow-pet .cr-wings{transform-origin:50% 58%;animation:critter-wing 0.9s ease-in-out infinite}
.critter .cr-tail,.hollow-pet .cr-tail{transform-origin:74% 80%;animation:critter-tail 3.1s ease-in-out infinite}
@keyframes critter-wing{0%,100%{transform:scaleX(1)}50%{transform:scaleX(.82)}}
@keyframes critter-tail{0%,100%{transform:rotate(0deg)}50%{transform:rotate(-11deg)}}
@media (prefers-reduced-motion: reduce){
  .critter .cr-wings,.critter .cr-tail{animation:none}
}

/* ---------- companions ---------- */
.pair-row{
  display:flex;align-items:center;gap:10px;
  padding:8px;margin-bottom:6px;
  border:3px solid var(--ink);border-radius:14px;
  background:var(--paper-2);
}
.pair-row.on{background:#e6f7e2;box-shadow:inset 0 0 0 2px #8ce99a}
.pair-row.dim{opacity:.6}
.pair-faces{flex:0 0 auto;display:flex;align-items:center}
.pair-face{width:34px;height:34px;display:block}
.pair-face + .pair-face{margin-left:-9px}
.pair-face svg{width:100%;height:100%;overflow:visible}
.pair-face.unmet svg{filter:grayscale(1);opacity:.55}
.pair-row .critter-svg,.pair-row .cr-body,.pair-row .cr-sprout,
.pair-row .cr-glow,.pair-row .cr-eye,.pair-row .cr-wings,.pair-row .cr-tail{animation:none}
.pair-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.pair-name{font-size:13.5px;font-weight:800;color:var(--ink)}
.pair-name em{
  font-style:normal;font-size:10px;font-weight:800;margin-left:6px;
  padding:1px 6px;border-radius:999px;background:#8ce99a;
  border:2px solid var(--ink);vertical-align:middle;
}
.pair-note{font-size:11.5px;color:var(--ink-soft);line-height:1.35}
.pair-who{font-size:10.5px;color:var(--ink-soft);opacity:.8}
.critter-memento{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--ink);line-height:1.3}
.critter-memento svg{width:12px;height:12px;flex:0 0 12px}
.critter-memento.none{color:var(--ink-soft);opacity:.7}

/* ============ the way down to the Hollow ============ */
/* THE BURROW MOUTH IS GONE, 2026-08-30 (the owner's call at the phase-3.5 gate).
   A room is reached by the vertical swipe now, and learning that swipe is the
   point: DOWN goes under to the Hollow, UP goes out to the Wild Meadow. The two
   labelled mouths on the lawn were competing with the gesture they were meant
   to teach, and the band needs the foot of the yard for the floating pair. */
.in-hollow .stage,.in-hollow #dock,.in-hollow .rail,.in-hollow .quest-strip{display:none}

/* ============ the Hollow ============ */
.hollow{position:absolute;inset:0;z-index:5;overflow:hidden;background:#b8834c}
.hollow[hidden]{display:none}
.hollow-scene-wrap{position:absolute;inset:0}
.hollow-scene{width:100%;height:100%;display:block}

.hollow-tenants{position:absolute;inset:0;pointer-events:none}
.hollow-pet{
  position:absolute;transform:translate(-50%,-50%);
  margin:0;padding:0;border:0;background:none;
  pointer-events:auto;cursor:pointer;line-height:0;
}
.hollow-pet svg{width:100%;height:100%;overflow:visible}
.hollow-pet .cr-glow{opacity:.42}
.hollow-pet:active{transform:translate(-50%,-50%) scale(.94)}
.hollow-name{
  position:absolute;left:50%;bottom:-13px;transform:translateX(-50%);
  font-size:10px;font-weight:800;color:#fff;white-space:nowrap;
  text-shadow:0 2px 0 rgba(44,26,16,.9);pointer-events:none;
}
.hollow-leaf,.hollow-gift,.hollow-fed{
  position:absolute;width:22px;height:22px;display:grid;place-items:center;
  border:3px solid var(--ink);border-radius:999px;box-shadow:0 2px 0 var(--ink-2);
}
.hollow-leaf{top:-4px;left:-4px;background:#8ce99a}
.hollow-gift{top:-4px;right:-4px;background:#ffd43b}
.hollow-fed{bottom:-2px;right:-4px;background:#ffa94d}
.hollow-leaf[hidden],.hollow-gift[hidden],.hollow-fed[hidden]{display:none}
.hollow-leaf svg,.hollow-gift svg,.hollow-fed svg{width:12px;height:12px}

/* Clears the HUD, which sits above every place layer since 2026-08-25. */
.hollow-exit{
  position:absolute;top:calc(62px + var(--sat));left:50%;transform:translateX(-50%);
  z-index:3;display:flex;align-items:center;gap:7px;
  padding:5px 13px 5px 9px;border:3px solid var(--ink);border-radius:999px;
  background:rgba(255,247,225,.94);font-family:inherit;font-weight:700;font-size:11.5px;
  color:var(--ink);box-shadow:0 3px 0 var(--ink-2);cursor:pointer;
}
/* Points and nudges DOWN, because since the owner re-ruled the axis that is the
   way out of a room you entered by dragging the world up. The margin moves with
   it: a down triangle paints the top half of its box and an up triangle the
   bottom, so the 4px has to swap sides or the mark sits wrong. */
.hollow-exit i{width:0;height:0;border:5px solid transparent;border-top-color:var(--ink);
  margin-top:4px;animation:coachBob .9s ease-in-out infinite}
@keyframes hl-nudge{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
.hollow-exit:active{transform:translateX(-50%) translateY(2px);box-shadow:0 1px 0 var(--ink-2)}

.hollow-count{
  /* Above the dock, not over the crack — the top of the scene is the one place
     with art busy enough to swallow small white text. */
  position:absolute;bottom:calc(98px + var(--bottom-gap));left:0;right:0;margin:0;z-index:3;
  text-align:center;font-size:11.5px;color:#fff;text-shadow:0 2px 0 rgba(44,26,16,.9);
}
.hollow-empty{
  position:absolute;top:44%;left:24px;right:24px;margin:0;z-index:3;text-align:center;
  font-size:13px;line-height:1.6;color:#fff;text-shadow:0 2px 0 rgba(44,26,16,.85);
}
.hollow-empty[hidden]{display:none}

.hollow-dock{
  position:absolute;left:0;right:0;bottom:0;z-index:3;
  padding:9px 9px calc(6px + var(--bottom-gap));
  background:linear-gradient(180deg,rgba(44,26,16,0),rgba(44,26,16,.2) 40%,rgba(44,26,16,.32));
}
/* Which verb a tap on a creature is currently spending. */
.hollow-dock .dock-btn.on{background:linear-gradient(180deg,#fff8d6,#ffe066)}

/* Loadout mode: who is out reads off the room rather than out of a panel, so a
   resting creature steps back without leaving. */
.in-loadout .hollow-pet{transition:opacity .2s ease, filter .2s ease}
.in-loadout .hollow-pet:not(.tending){opacity:.5;filter:saturate(.45)}
.in-loadout .hollow-leaf{box-shadow:0 0 0 3px rgba(140,233,154,.5), 0 2px 0 var(--ink-2)}

/* Motion emitted by hollow.js, named so it cannot collide with the garden's. */
.hl-wisp{animation:hl-drift var(--dur,7s) ease-in-out infinite;animation-delay:var(--delay,0s);
  transform-box:fill-box;transform-origin:center}
@keyframes hl-drift{
  0%,100%{transform:translate(0,0);opacity:.22}25%{opacity:.95}
  50%{transform:translate(var(--dx,10px),var(--dy,-14px));opacity:.7}75%{opacity:.3}
}
.hl-shaft{animation:hl-breathe 9s ease-in-out infinite}
@keyframes hl-breathe{0%,100%{opacity:.55}50%{opacity:.78}}
.hl-dust{animation:hl-fall var(--dur,11s) linear infinite;animation-delay:var(--delay,0s);
  transform-box:fill-box}
@keyframes hl-fall{
  0%{transform:translate(0,-10px);opacity:0}15%{opacity:.8}
  85%{opacity:.45}100%{transform:translate(var(--dx,6px),430px);opacity:0}
}
.hl-glow{animation:hl-pulse 4.5s ease-in-out infinite;transform-box:fill-box;transform-origin:center}
@keyframes hl-pulse{0%,100%{opacity:.35}50%{opacity:.7}}
@media (prefers-reduced-motion: reduce){
  .hl-wisp,.hl-shaft,.hl-dust,.hl-glow,.hollow-exit i{animation:none}
}

/* ---------------- customers ----------------
   Three expressions are always in the markup and these rules pick one, the same
   contract the sleeping creatures use. Base state is the greeting face. */
.cu-eyes-happy, .cu-mouth-happy, .cu-eyes-wait, .cu-mouth-wait { display: none; }
.is-waiting .cu-eyes, .is-waiting .cu-mouth { display: none; }
.is-waiting .cu-eyes-wait, .is-waiting .cu-mouth-wait { display: block; }
.is-happy .cu-eyes, .is-happy .cu-mouth { display: none; }
.is-happy .cu-eyes-happy, .is-happy .cu-mouth-happy { display: block; }

/* ---------------- the Garden Stand ----------------
   A queue of people, not a list of orders. The face is the biggest thing on a
   row on purpose: the customer is the story and the goods are the token. */
.on-head{display:flex;align-items:center;gap:10px;margin:0 0 10px}
.on-head-copy{flex:1;min-width:0}
.on-head-copy b{display:block;font-size:19px;line-height:1.1}
.on-head-copy span{display:block;font-size:12px;color:var(--ink-soft)}
.on-tier{
  display:inline-flex;align-items:center;gap:4px;padding:5px 11px;
  border:3px solid var(--ink,var(--ink));border-radius:999px;background:#fff7e1;
  font-size:12px;font-weight:800;box-shadow:0 3px 0 var(--ink-2);white-space:nowrap;
}
.on-tier svg{width:15px;height:15px}

.on-list{display:flex;flex-direction:column;gap:10px}
.on-row{
  display:flex;align-items:stretch;gap:9px;width:100%;text-align:left;
  padding:8px 9px;border:4px solid var(--ink,var(--ink));border-radius:18px;
  background:#fff7e1;box-shadow:0 4px 0 var(--ink-2);
  font-family:inherit;color:inherit;cursor:pointer;
}
.on-row:active{transform:translateY(2px);box-shadow:0 2px 0 var(--ink-2)}
/* Ready is carried by the card lighting up AND by the customer's face, so it
   reads before any text does. */
.on-row.ready{background:#fff3c4;border-color:var(--ink)}
.on-row.empty{opacity:.72;cursor:default;align-items:center;gap:12px}
.on-row.empty:active{transform:none;box-shadow:0 4px 0 var(--ink-2)}
.on-empty-art svg{width:30px;height:30px;opacity:.7}
.on-empty-copy b{display:block;font-size:14px}
.on-empty-copy span{font-size:12px;color:var(--ink-soft)}

.on-face{
  width:54px;flex:0 0 54px;align-self:flex-start;
  border:3px solid var(--ink,var(--ink));border-radius:14px;overflow:hidden;
  background:#e8f4ff;line-height:0;
}
.on-face svg{width:100%;height:auto;display:block}
.on-row.ready .on-face{background:#fff0c9}

.on-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
.on-who{font-size:14.5px;font-weight:800;line-height:1.1}
.on-good{display:flex;align-items:center;gap:5px;font-size:11.5px;
  color:var(--ink-soft);text-transform:uppercase;letter-spacing:.05em}
.on-good svg{width:14px;height:14px}

.on-chips{display:flex;flex-wrap:wrap;gap:7px;margin-top:3px}
/* THE ORDER TOKEN — rebuilt 2026-08-30 on the owner's note: "the numbers overlay
   the icon of the flower they need to deliver."

   They did, literally: the count was `position:absolute; top:30px` on a 38px
   art tile, so the pill sat across the bottom eight pixels of the bloom. Two
   things were fighting for the same square, and the bloom is the one that has
   to read — it is what the player has to go and grow.

   So the token is one object with two bands: the art on top at 44px, the count
   in its own strip underneath with a rule between them. Bigger, and nothing
   overlapping anything. */
.on-chip{
  position:relative;width:64px;text-align:center;overflow:hidden;
  border:3px solid var(--ink,var(--ink));border-radius:13px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2,var(--paper-2)));
  box-shadow:0 3px 0 var(--ink-2,var(--ink-2)), 0 6px 9px rgba(44,26,16,.16);
}
.on-chips.big .on-chip{width:78px;border-radius:17px;border-width:3.5px}
.on-chip-art{
  width:100%;height:52px;display:grid;place-items:center;line-height:0;
  background:#f7ead1;
}
.on-chips.big .on-chip-art{height:62px}
.on-chip-art svg{width:76%;height:76%;overflow:visible}
.on-chip.done .on-chip-art{background:#d3f9d8}
.on-chip-count{
  display:flex;align-items:center;justify-content:center;gap:2px;
  border-top:3px solid var(--ink,var(--ink));
  background:linear-gradient(180deg,var(--paper-2,var(--paper-2)),var(--paper-3,var(--paper-3)));
  font-size:12.5px;font-weight:800;line-height:1.6;white-space:nowrap;
}
.on-chips.big .on-chip-count{font-size:14px;line-height:1.6;border-top-width:3.5px}
.on-chip-count svg{width:13px;height:13px}
.on-chips.big .on-chip-count svg{width:15px;height:15px}
.on-chip.done .on-chip-count{background:linear-gradient(180deg,#d3f9d8,#8ce99a)}
.on-chip-name{display:none}
.on-chips.big .on-chip-name{
  display:block;padding:4px 3px 5px;border-top:2px solid var(--ink-2);
  font-size:10px;line-height:1.15;color:var(--ink-soft);
}
.on-chip-qty{display:none}

.on-bar{
  height:8px;border:2.5px solid var(--ink,var(--ink));border-radius:999px;
  background:#e8d5b0;overflow:hidden;margin-top:2px;
}
.on-bar.big{height:13px;border-width:3px;margin-top:8px}
.on-bar i{display:block;height:100%;background:#8ce99a;transition:width .25s ease}
.on-row.ready .on-bar i{background:#ffd23f}

.on-pay{
  flex:0 0 auto;display:flex;flex-direction:column;align-items:flex-end;justify-content:center;gap:3px;
}
.on-coins,.on-rep{display:inline-flex;align-items:center;gap:3px;font-size:12.5px;font-weight:800}
.on-coins svg,.on-rep svg{width:15px;height:15px}
.on-rep{color:var(--ink-soft);font-size:11.5px}
.on-go{
  margin-top:2px;padding:2px 8px;border:2.5px solid var(--ink,var(--ink));border-radius:999px;
  background:#8ce99a;font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;
}
.on-foot{margin:12px 2px 0;font-size:11.5px;color:var(--ink-soft);line-height:1.5}

/* ---------- one customer ---------- */
.on-plate{text-align:center;margin:0 0 10px;padding-top:4px}
.on-name{margin:0;font-size:clamp(24px,6.8vw,31px);font-weight:800;line-height:1.05}
.on-said{
  display:inline-block;margin:7px 0 0;padding:7px 13px;
  border:3px solid var(--ink,var(--ink));border-radius:16px;background:#fff7e1;
  font-size:13px;line-height:1.35;box-shadow:0 3px 0 var(--ink-2);
}
.on-said.pop{animation:on-pop .3s ease}
@keyframes on-pop{0%{transform:scale(.9)}60%{transform:scale(1.05)}100%{transform:scale(1)}}

.on-want{
  border:4px solid var(--ink,var(--ink));border-radius:18px;background:#fff7e1;
  padding:10px 12px 12px;box-shadow:0 4px 0 var(--ink-2);margin-bottom:10px;
}
.on-want-head{display:flex;align-items:center;gap:7px;font-size:16px}
.on-want-head svg{width:20px;height:20px}
.on-line{margin:4px 0 9px;font-size:12.5px;font-style:italic;color:var(--ink-soft);line-height:1.4}

.on-pays{display:flex;justify-content:center;gap:14px;margin:0 0 10px}
.on-pays .on-coins{font-size:16px}
.on-pays .on-coins svg{width:19px;height:19px}
.on-pays .on-rep{font-size:14px}

.big-btn.go{background:#8ce99a}
.big-btn.off{opacity:.55}
.ghost-btn{
  display:block;width:100%;margin-top:8px;padding:9px;
  border:3px dashed var(--ink,var(--ink));border-radius:14px;background:transparent;
  font-family:inherit;font-weight:800;font-size:12.5px;color:var(--ink-soft);cursor:pointer;
}
.ghost-btn:active{transform:translateY(2px)}

/* The customer standing on the sheet is a bust — taller than it is wide, where a
   creature is square. It has to be sized by HEIGHT and kept inside the box: an
   SVG taller than its container overflows downward past the sheet's top edge,
   and its shoulders land on the customer's own name. Measured, not guessed. */
/* Both dimensions stated. `width:auto` on an SVG resolves to 100% of its
   container rather than to the viewBox aspect, which drew a head three times the
   size of the panel. 100:154 is the customer viewBox. */
.sheet-art.guest{height:196px;place-items:end center}
.sheet-art.guest svg{width:127px;height:196px;animation:none}
.sheet-art.guest::before{width:210px;height:165px}
@media (max-height:700px){
  .sheet-art.guest{height:140px}
  .sheet-art.guest svg{width:91px;height:140px}
}

/* `.ui` is painted above the meadow so the HUD shows through. It also covers
   the whole screen, so while a room is up it has to stop eating that room's
   taps — everything else in it is hidden by the rule below. */
.in-meadow .ui{pointer-events:none}
.in-meadow .hud{pointer-events:auto}

/* THE OVERWORLD IS GONE, 2026-08-30 (phase 3.5). The map, its camera, its place
   cards and its parcels retired with the World dock button; `overworld.js` and
   `ui-map.js` are deleted. Everything it was the only door to has a new one:
   the Stand is the Orders & Quests button, the Wild Meadow is the swipe DOWN,
   the Hollow is the swipe UP, and the garden is the GARDEN button. The land
   parcels were the one thing with nowhere to go, and they went with it
   deliberately — see docs/25-world-map.md. */

/* =========================================================
   THE WILD MEADOW
   The quiet place. The garden is the tapping and the combo; nothing in here
   flashes or counts down, and the only motion is drift. See meadow.js.
   ========================================================= */
.meadow-layer{
  position:absolute;inset:0;z-index:12;overflow:hidden;background:#8ed36b;
  /* The meadow's own material palette, and the reason this screen finally
     separates from the world it stands in. The garden reads because it puts a
     BROWN board on a green field; the meadow put a green board on a green field
     and no amount of detail could rescue that. So the terrace is stone and the
     cells are cobbles — see docs/05-art-direction.md, "The material recipe". */
  --mw-stone-1:#d8c8a8;
  --mw-stone-2:#b09c7c;
  --mw-stone-3:#8c775c;
  --mw-stone-lip:#6b5942;
  --cob-joint:#3d352c;
  --cob-1:#726554;
  --cob-2:#80735f;
  --cob-3:#615647;
  --cob-lit:rgba(255,250,238,.16);
  --cob-moss:#5c7f3e;
  --cob-socket:#40352b;
  --cob-pad:#a2978a;
  --cob-lip:#241e18;
  --mw-tread:#8a7550;
  --mw-tuft:#4a8b3b;
}
/* Night takes the warmth out of the stone rather than only the light. */
.meadow-layer[data-sky="moon"]{
  --mw-stone-1:#8e8878;
  --mw-stone-2:#6f6a5d;
  --mw-stone-3:#575246;
  --mw-stone-lip:#413d34;
  --cob-joint:#2a2520;
  --cob-1:#4e4a42;
  --cob-2:#57534a;
  --cob-3:#433f38;
  --cob-lit:rgba(226,236,255,.1);
  --cob-moss:#415c33;
  --cob-socket:#2c261f;
  --cob-pad:#6b6459;
  --cob-lip:#171310;
  --mw-tread:#4e442f;
  --mw-tuft:#2c5230;
}
.meadow-layer[hidden]{display:none}
.meadow-scene-host{position:absolute;inset:0}
.meadow-scene-host svg{position:absolute;inset:0;width:100%;height:100%;display:block}
.in-meadow .stage,.in-meadow #dock,.in-meadow .rail,.in-meadow .quest-strip{display:none}
.in-meadow .coach{display:none}

/* ---- the column ----
   The place layers sit OUTSIDE `.ui`, which is the one thing that caps the game
   at 560px and centres it. So a room has to re-declare that column itself or it
   is the only screen in the game that runs the full width of a desktop window —
   which is exactly how the meadow's dock ended up three times the garden's.
   Same rows, same padding, same cap: this is the garden's grid, re-stated. */
.mw-ui{
  position:absolute;inset:0;
  display:grid;
  grid-template-rows:auto minmax(0,1fr) auto auto;
  grid-template-columns:minmax(0,1fr);
  padding:
    calc(var(--sat,0px) + 70px)
    calc(var(--sar,0px) + 10px)
    var(--bottom-gap,16px)
    calc(var(--sal,0px) + 10px);
  gap:6px;
  max-width:560px;margin:0 auto;
  pointer-events:none;
}

/* ---- the board ----
   Same grammar as the garden: a square frame floating in the scene with the
   flower in the middle, built from the same five layers. Different skin — an
   old stone terrace rather than a wooden planter. */
.mw-stage{
  --yard-h:clamp(70px,13vh,108px);
  position:relative;display:grid;place-items:center;
  min-height:0;min-width:0;padding:2px 0 var(--yard-h);
  overflow:visible;pointer-events:none;
}
.mw-frame{width:100%;height:100%;display:grid;place-items:center;min-height:0;min-width:0}
.mw-board{
  --gap:clamp(6px,2.2vw,13px);
  position:relative;display:grid;pointer-events:auto;
  grid-template-columns:repeat(3,minmax(0,1fr));
  grid-template-rows:repeat(3,minmax(0,1fr));
  gap:var(--gap);padding:calc(var(--gap) * .85);
  border:4px solid var(--ink);border-radius:26px;
  background:
    repeating-linear-gradient(96deg, rgba(255,255,255,.035) 0 3px, transparent 3px 21px),
    linear-gradient(180deg,var(--mw-stone-1) 0%,var(--mw-stone-2) 44%,var(--mw-stone-3) 100%);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.3),
    inset 0 -9px 0 rgba(0,0,0,.18),
    0 9px 0 var(--mw-stone-lip),
    0 18px 26px rgba(44,26,16,.3);
}
/* Long grass pushing up over the terrace, where the garden's planter gets a
   neat mown fringe. Same device, wilder hand. */
.mw-board::before{
  content:'';position:absolute;left:4%;right:4%;top:-12px;height:15px;
  background:
    radial-gradient(ellipse 52% 100% at 50% 100%, #4a8b3b 0 86%, transparent 92%),
    radial-gradient(ellipse 62% 64% at 50% 100%, var(--grass-d) 0 86%, transparent 92%);
  background-size:21px 15px, 30px 10px;
  background-position:0 0, 10px 5px;
  background-repeat:repeat-x;
  opacity:.92;pointer-events:none;
}

/* The house material recipe, from docs/05-art-direction.md: body gradient, lit
   top edge, shaded bottom edge, an UNBLURRED lip, soft contact shadow. The lip
   is what makes a shape read as moulded rather than drawn, and the meadow's
   cells had a flat rgba() fill and none of it. */
.mw-cell{
  position:relative;padding:0;border:3px solid var(--ink);border-radius:var(--r-md);
  background:linear-gradient(180deg,var(--cob-joint) 0%,var(--cob-lip) 100%);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.14),
    inset 0 -7px 0 rgba(0,0,0,.22),
    0 4px 0 var(--cob-lip),
    0 8px 14px rgba(44,26,16,.26);
  line-height:0;cursor:pointer;font-family:inherit;overflow:visible;isolation:isolate;
  transition:transform .1s ease, box-shadow .1s ease;
}
.mw-cell:active{
  transform:translateY(3px);
  box-shadow:inset 0 5px 0 rgba(255,255,255,.14), inset 0 -7px 0 rgba(0,0,0,.22),
    0 1px 0 var(--cob-lip), 0 4px 8px rgba(44,26,16,.22);
}
.mw-cell svg{width:100%;height:100%;display:block;overflow:visible}
.mw-cell-floor{
  position:absolute;inset:0;z-index:0;
  border-radius:calc(var(--r-md) - 3px);overflow:hidden;
}
.mw-cell-floor svg{overflow:hidden}
.mw-cell-obj{position:absolute;inset:0;z-index:1}
.mw-cell::after{
  content:'';position:absolute;inset:0;z-index:2;pointer-events:none;
  border-radius:calc(var(--r-md) - 3px);
  background:
    radial-gradient(circle at 26% 20%, rgba(255,255,255,.13) 0 13%, transparent 14%),
    radial-gradient(circle at 74% 66%, rgba(0,0,0,.13) 0 10%, transparent 11%);
  box-shadow:inset 0 5px 0 rgba(255,255,255,.15), inset 0 -8px 0 rgba(0,0,0,.28);
}

/* ---- locked land ----
   The garden's locked plot, restated: the body desaturates to cold stone and a
   padlock sits over a chip that names either the level or the price. Same two
   stages, same read, so the rule is learned once across both boards. */
.mw-cell.locked{
  filter:saturate(.3);
  background:linear-gradient(180deg,#6f7a70 0%,#4b544c 100%);
}
.mw-cell.locked .mw-cell-floor{opacity:.42;filter:grayscale(1)}
.mw-lock{
  position:absolute;inset:0;z-index:2;display:none;
  flex-direction:column;align-items:center;justify-content:center;gap:4px;
  pointer-events:none;
}
.mw-cell.locked .mw-lock{display:flex}
.mw-lock > svg{width:26%;height:26%;max-width:34px;filter:drop-shadow(0 2px 0 rgba(0,0,0,.3))}
.mw-lock-cost svg{width:13px;height:13px;max-width:none;filter:none}
.mw-lock-cost{
  display:flex;align-items:center;gap:3px;
  background:var(--paper);border:2.5px solid var(--ink);border-radius:999px;
  padding:2px 8px;font-size:11px;font-weight:800;line-height:1;
  box-shadow:0 2px 0 var(--ink-2);
}
/* Affordable land pulses, exactly as an affordable plot does. Gated land never
   does — a level you have not reached is not an invitation. */
.mw-cell.locked.can .mw-lock-cost{background:#d3f9d8;animation:affordPulse 1.4s ease-in-out infinite}
.mw-cell.locked.gated .mw-lock-cost{background:var(--paper);animation:none}

/* A cell is a PLACE, not a status light, so the cobbles read the same in every
   state and what changes is the thing standing on them. */
.mw-cell.empty.can .mw-socket-ring{animation:mwInvite 2.6s ease-in-out infinite}
@keyframes mwInvite{0%,100%{opacity:.5}50%{opacity:1}}
.mw-cell.ready{
  box-shadow:
    inset 0 0 0 2px rgba(255,201,74,.5),
    inset 0 5px 0 rgba(255,255,255,.14),
    inset 0 -7px 0 rgba(0,0,0,.22),
    0 4px 0 var(--cob-lip),
    0 8px 14px rgba(44,26,16,.26);
}
.mw-cell.ready .mw-cell-obj{animation:mwReady 2.2s ease-in-out infinite}
@keyframes mwReady{0%,100%{transform:translateY(0) rotate(0)}
  25%{transform:translateY(-3px) rotate(-1.5deg)}75%{transform:translateY(-3px) rotate(1.5deg)}}
.mw-cell.movable{outline:3px dashed rgba(255,247,225,.85);outline-offset:-9px}
.mw-cell.picked{outline:4px solid #ffd23f;outline-offset:-8px;transform:translateY(3px)}

.mw-flower{
  grid-column:2;grid-row:2;position:relative;pointer-events:auto;
  padding:0;border:0;background:none;display:grid;place-items:center;cursor:pointer;
}
.mw-flower svg{width:100%;height:auto;overflow:visible}

.mw-jar-badge{
  position:absolute;top:-9px;right:-9px;min-width:26px;height:26px;z-index:3;
  display:grid;place-items:center;border:3.5px solid var(--ink,var(--ink));border-radius:999px;
  background:#ffc94a;font-size:13px;font-weight:800;line-height:1;
  box-shadow:0 3px 0 var(--ink-2);pointer-events:none;
}
.mw-jar-badge[hidden]{display:none}
.mw-cell.full .mw-jar-badge{background:#ff8fa3}

/* ---- keepers ---- */
/* They stand in the YARD under the board, which is the garden's own device —
   `.stage` reserves it as padding so a creature never ends up standing on a
   plot. Positioning them in scene coordinates was only right while the scene
   and the layer were the same box; once the board became a centred column they
   belonged to the column. */
.mw-keeper-bank{
  position:absolute;left:0;right:0;bottom:0;height:var(--yard-h,92px);
  display:flex;align-items:center;justify-content:center;
  gap:clamp(48px,22vw,150px);
  pointer-events:none;z-index:2;
}
.mw-keeper{
  position:relative;width:var(--keeper-size,74px);height:var(--keeper-size,74px);
  padding:0;border:0;background:none;line-height:0;
  pointer-events:auto;cursor:pointer;font-family:inherit;
  transition:transform .12s cubic-bezier(.34,1.56,.64,1);
}
.mw-keeper svg{width:100%;height:100%;overflow:visible;display:block}
.mw-keeper:active{transform:scale(.93)}
/* A creature with nothing under it floats, and a soft ellipse is a shadow with
   no floor to fall on. Every keeper gets a flagstone to stand on. */
.mw-keeper-ground{position:absolute;inset:0;z-index:0}
.mw-keeper-obj{position:absolute;inset:-8% 0 12%;z-index:1}
.mw-keeper-tag{
  position:absolute;left:50%;bottom:-19px;transform:translateX(-50%);
  padding:2px 9px;border:2.5px solid var(--ink,var(--ink));border-radius:999px;
  background:rgba(255,247,225,.94);font-size:10.5px;font-weight:800;white-space:nowrap;
  line-height:1.35;box-shadow:0 2px 0 var(--ink-2);pointer-events:none;
}
.mw-keeper.empty::after{
  content:'';position:absolute;left:15%;right:15%;top:63%;height:29%;
  border:3px dashed rgba(255,247,225,.62);border-radius:999px;pointer-events:none;
}
.mw-keeper.empty .mw-keeper-obj{display:grid;place-items:end center;padding-bottom:26%;opacity:.8}
.mw-keeper.empty .mw-keeper-obj svg{width:34%;height:34%}
.mw-keeper.asleep{opacity:.72}

/* ---- bees ---- */
/* Behind the board on purpose: a bee that passes behind the terrace reads as
   depth, where one drawn over a cell just hides what is in it. */
.mw-bees{position:absolute;inset:0;pointer-events:none;overflow:hidden}
.mw-bee{
  position:absolute;left:var(--x,14%);top:var(--y,46%);width:30px;height:24px;display:block;
  animation:mwBee var(--dur,9s) linear infinite;animation-delay:calc(var(--i,0) * -1.7s);
}
.mw-bee svg{width:100%;height:100%;display:block;overflow:visible}
/* A long lazy loop across the bank. Never a straight line — a bee that flies
   straight reads as a projectile. */
@keyframes mwBee{
  0%{transform:translate(0,0) rotate(0)}
  20%{transform:translate(90px,-42px) rotate(6deg)}
  42%{transform:translate(186px,10px) rotate(-4deg)}
  60%{transform:translate(232px,-34px) rotate(5deg)}
  80%{transform:translate(126px,-70px) rotate(-6deg)}
  100%{transform:translate(0,0) rotate(0)}
}

.mw-note{
  display:flex;flex-wrap:wrap;justify-content:center;gap:6px;pointer-events:none;
}
.mw-note span{
  display:inline-flex;align-items:center;gap:5px;padding:4px 11px;
  border:3px solid var(--ink,var(--ink));border-radius:999px;background:rgba(255,247,225,.94);
  font-size:11.5px;font-weight:800;box-shadow:0 3px 0 var(--ink-2);
}
.mw-note svg{width:14px;height:14px}

.mw-exit{
  justify-self:center;
  display:flex;align-items:center;gap:7px;padding:5px 13px;margin-bottom:2px;
  border:3px solid var(--ink,var(--ink));border-radius:999px;background:rgba(255,247,225,.9);
  font-size:11.5px;font-weight:800;box-shadow:0 3px 0 var(--ink-2);pointer-events:auto;
  margin:0;font:inherit;color:inherit;cursor:pointer;
}
/* Up, since the meadow is now entered by dragging the world down and left by
   dragging it back. Its twin in the Hollow points the other way for the same
   reason. */
.mw-exit i{width:0;height:0;border:5px solid transparent;border-bottom-color:var(--ink);
  margin-bottom:4px;animation:hl-nudge 2.4s ease-in-out infinite}

.mw-dock{pointer-events:auto}
.mw-dock .dock-btn.on{background:linear-gradient(180deg,#fff3c4,#ffe9a8)}

/* ---- the panels ---- */
.keeper-row{
  display:flex;align-items:center;gap:10px;width:100%;text-align:left;margin-bottom:8px;
  padding:8px 11px;border:4px solid var(--ink,var(--ink));border-radius:18px;background:#fff7e1;
  box-shadow:0 4px 0 var(--ink-2);font-family:inherit;color:inherit;cursor:pointer;
}
.keeper-row:active{transform:translateY(2px);box-shadow:0 2px 0 var(--ink-2)}
.keeper-row.on{background:#fff3c4}
.keeper-face{width:52px;flex:0 0 52px;line-height:0}
.keeper-face svg{width:100%;height:auto;overflow:visible}
.keeper-face.asleep{opacity:.6}
.keeper-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.keeper-bee{font-style:normal;font-size:10px;padding:1px 7px;border-radius:999px;
  border:2px solid var(--ink,var(--ink));background:#ffd23f;margin-left:5px;vertical-align:middle}
.keeper-pick{width:26px;flex:0 0 26px}
.keeper-pick svg{width:100%;height:100%}

.shelf-bar{height:12px;border:3px solid var(--ink,var(--ink));border-radius:999px;
  background:#e8d5b0;overflow:hidden;margin:0 0 12px}
.shelf-bar i{display:block;height:100%;background:#ffc94a;transition:width .3s ease}
.shelf-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
.shelf-slot{
  position:relative;display:flex;flex-direction:column;align-items:center;gap:3px;
  padding:8px 4px 6px;border:3px solid var(--ink,var(--ink));border-radius:14px;
  background:#f2e3c6;opacity:.55;
}
.shelf-slot.has{background:#fff7e1;opacity:1}
.shelf-jar{width:38px;height:44px;line-height:0}
.shelf-jar svg{width:100%;height:100%;display:block}
/* Unfilled slots show the jar shape only — the silhouette pull. */
.shelf-slot:not(.has) .shelf-jar svg{filter:grayscale(1) brightness(.75);opacity:.5}
.shelf-name{font-size:9.5px;font-weight:800;line-height:1.15;text-align:center}
.shelf-n{position:absolute;top:-6px;right:-4px;min-width:20px;height:20px;
  display:grid;place-items:center;border:2.5px solid var(--ink,var(--ink));border-radius:999px;
  background:#ffc94a;font-size:10px;font-weight:800}

/* Scene drift — slow enough to read as air moving, never as motion. */
/* Drift in user units: `vw` inside an SVG resolves against the page rather than
   the viewBox, so the scenery keyframe cannot be reused here. */
@keyframes mwDrift{from{transform:translateX(-150px)}to{transform:translateX(540px)}}
.mw-cloud{animation:mwDrift var(--dur,58s) linear infinite;animation-delay:var(--delay,0s)}
.mw-blade{transform-box:fill-box;transform-origin:bottom center;
  animation:mwSway 5.2s ease-in-out infinite;animation-delay:calc(var(--i,0) * -.6s)}
@keyframes mwSway{0%,100%{transform:rotate(-2.5deg)}50%{transform:rotate(2.5deg)}}
.mw-flower{transform-box:fill-box;transform-origin:bottom center;
  animation:mwSway 6.4s ease-in-out infinite;animation-delay:calc(var(--i,0) * -.9s)}
.mw-frond{transform-box:fill-box;transform-origin:top center;
  animation:mwSway 7.6s ease-in-out infinite;animation-delay:calc(var(--i,0) * -.5s)}

/* A media query adds no specificity, so this has to come AFTER every rule it
   cancels. Sitting above them it lost the cascade, and the clouds, blades and
   fronds kept moving with reduced motion on. */
@media (prefers-reduced-motion:reduce){
  .mw-bee,.mw-socket-ring,.mw-cell.ready .mw-cell-obj,.mw-exit i,
  .mw-cell.locked.can .mw-lock-cost,
  .mw-cloud,.mw-blade,.mw-flower,.mw-frond{animation:none}
}

/* ---- the build picker ---- */
.build-row{
  display:flex;align-items:center;gap:11px;width:100%;text-align:left;margin-bottom:8px;
  padding:9px 12px;border:4px solid var(--ink,var(--ink));border-radius:18px;background:#fff7e1;
  box-shadow:0 4px 0 var(--ink-2);font-family:inherit;color:inherit;cursor:pointer;
}
.build-row:active{transform:translateY(2px);box-shadow:0 2px 0 var(--ink-2)}
.build-row.affordable{background:#fff3c4}
.build-art{width:54px;flex:0 0 54px;line-height:0}
.build-art svg{width:100%;height:auto;overflow:visible}
.build-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.build-helps{
  font-size:10.5px;font-weight:800;color:var(--ink-soft);
  text-transform:uppercase;letter-spacing:.04em;
}
.build-helps.on{color:#3f9950}
/* What the thing would actually do, in the same cream pills the seed picker
   uses — the adjectives stay above them, the numbers live here. */
.build-stats{display:flex;flex-wrap:wrap;gap:4px;margin:2px 0 1px}
.build-buy{display:flex;flex-direction:column;align-items:flex-end;gap:3px;flex:none}
/* .price is written for a card column, where it pushes itself to the bottom. */
.build-buy .price{margin-top:0;align-self:auto}
.build-owned{
  font-size:9.5px;font-weight:800;color:var(--ink-soft);
  text-transform:uppercase;letter-spacing:.03em;white-space:nowrap;
}

/* =========================================================
   FALL — the second season, and the strip it sits on

   Fall is NOT a place layer. It is the same room with a different board in it:
   `.stage` swaps its frame and the scenery swaps behind it, while the HUD, the
   quest strip, the rail and the dock never move. That is why none of this
   re-states the 560px column — it never leaves `.ui`.

   The board is the garden's construction line for line — same 3x3, same gap,
   same radius, same lip ladder, same grass fringe — in a different MATERIAL. A
   woven trug on damp autumn earth, because Fall's verb is "fill it and carry
   the whole thing in" where Summer's is "dig it, plant it, clear it".
   ========================================================= */

/* The scene sits above the CSS scenery and below `.ui`, so the HUD stays up and
   the dock stays tappable through it. */
.fall-layer{position:absolute;inset:0;z-index:2;pointer-events:none}
.fall-layer[hidden]{display:none}
.fall-scene-host{position:absolute;inset:0;overflow:hidden}
.fall-scene{width:100%;height:100%;display:block}

.fall-frame{display:none;width:100%;height:100%;place-items:center;min-height:0;min-width:0}
.in-fall .fall-frame{display:grid}
.in-fall .garden-frame{display:none}
/* The creatures live in Summer. Their yard's reserved height goes back to the
   board rather than sitting empty — a season is allowed to use its own room. */
.in-fall .critter-yard{display:none}
/* A coach mark points at something in the GARDEN, and .in-fall has hidden it —
   so the bubble measured a 0x0 rect and parked in the top-left corner over the
   coin wallet. Every other room declares this; Fall was built and never
   registered as one. */
/* …except the one that teaches the way back OUT of Fall, which points at the
   season tab — a real, measured, visible node in this room. Narrowed rather
   than removed: every other coach mark still targets something in the garden
   and would still park in the corner. */
.in-fall .coach:not(.season){display:none}
:root{--fl-noop:0}

.fl-board{
  --gap:clamp(6px, 2.2vw, 13px);
  position:relative;
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  grid-template-rows:repeat(3,minmax(0,1fr));
  gap:var(--gap);
  padding:calc(var(--gap) * .85);
  border:4px solid var(--ink);
  border-radius:26px;
  background:
    repeating-linear-gradient(52deg, rgba(255,255,255,.09) 0 4px, transparent 4px 11px),
    repeating-linear-gradient(-52deg, rgba(0,0,0,.10) 0 4px, transparent 4px 11px),
    linear-gradient(180deg,#c98a4e 0%, #a76b38 44%, #8a5528 100%);
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.24),
    inset 0 -9px 0 rgba(0,0,0,.22),
    0 9px 0 #5f3a1a,
    0 18px 26px rgba(44,26,16,.32);
}
/* Armed. The board itself is the announcement — you can tell from across the
   room, which is what a screen you come back to at dinner needs. It restates
   the WHOLE stack, because a box-shadow modifier that adds a rim and forgets
   the lip deletes the lip in exactly the state everybody is looking at. */
.fl-board.armed{
  box-shadow:
    inset 0 5px 0 rgba(255,255,255,.24),
    inset 0 -9px 0 rgba(0,0,0,.22),
    0 9px 0 #5f3a1a,
    0 18px 26px rgba(44,26,16,.32),
    0 0 0 4px var(--coin),
    0 0 30px 10px rgba(255,201,60,.42);
}
/* The garden's grass fringe, in stubble. */
.fl-board::before{
  content:'';position:absolute;left:5%;right:5%;top:-10px;height:12px;
  background:radial-gradient(ellipse 52% 100% at 50% 100%, #7d8b40 0 92%, transparent 94%);
  background-size:22px 12px;background-repeat:repeat-x;
  opacity:.9;pointer-events:none;
}

.fl-plot{
  position:relative;
  border-radius:var(--r-md);
  border:3px solid var(--ink);
  background:linear-gradient(180deg,#7e5e42 0%, #5f4630 72%, #453221 100%);
  box-shadow:0 4px 0 #392a1c, 0 8px 14px rgba(44,26,16,.24);
  display:block;overflow:visible;isolation:isolate;
  transition:transform .1s ease, box-shadow .1s ease;
}
.fl-plot:active{transform:translateY(3px);box-shadow:0 1px 0 #392a1c, 0 4px 8px rgba(44,26,16,.22)}
/* The lit top edge and the shaded bottom ride an OVERLAY, because an inset
   box-shadow paints under an element's content and the cell has an opaque
   floor. Half the material recipe vanished from the meadow's cells this way. */
.fl-plot::after{
  content:'';position:absolute;inset:0;border-radius:15px;pointer-events:none;z-index:3;
  box-shadow:inset 0 5px 0 rgba(255,255,255,.13), inset 0 -7px 0 rgba(0,0,0,.2);
}
.fl-floor{position:absolute;inset:0;border-radius:15px;overflow:hidden;line-height:0}
.fl-floor svg{width:100%;height:100%;display:block}
.fl-slot{position:absolute;inset:0;display:grid;place-items:center;z-index:2;line-height:0}
.fl-slot svg{width:88%;height:88%;overflow:visible}
.fl-empty{position:absolute;inset:0;display:none;place-items:center;z-index:2;opacity:.85}
.fl-empty svg{width:46%;height:46%;filter:drop-shadow(0 2px 0 rgba(0,0,0,.35))}
.fl-plot[data-state="empty"] .fl-empty{display:grid}
.fl-plot[data-state="empty"]{filter:saturate(.85)}

/* Ready. The same "!" the garden uses, so the state is learned once. */
.fl-ready{
  position:absolute;top:-9px;right:-6px;z-index:5;
  width:24px;height:24px;border-radius:50%;
  display:none;place-items:center;
  background:linear-gradient(180deg,#ffe066,var(--coin));
  border:3px solid var(--ink);box-shadow:0 3px 0 var(--ink-2);
  font-size:13px;font-weight:800;line-height:1;
  animation:readyBob 1.5s ease-in-out infinite;
}
.fl-plot[data-state="ready"] .fl-ready{display:grid}
.fl-plot[data-state="ready"]{box-shadow:0 4px 0 #392a1c, 0 8px 14px rgba(44,26,16,.24), 0 0 0 3px var(--coin)}
@keyframes readyBob{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}

.fl-wait{
  position:absolute;left:50%;top:6px;transform:translateX(-50%);z-index:5;
  padding:2px 7px;border-radius:999px;
  border:2px solid var(--ink);
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 2px 0 var(--ink-2);
  font-size:9.5px;font-weight:800;line-height:1.3;white-space:nowrap;
}
.fl-wait[hidden]{display:none}
.fl-bar{
  /* The garden's geometry and the garden's dark track: at 0% a cream well
     drew a bright line across the plant, and an empty cell read as MORE
     planted than a planted one. */
  position:absolute;left:10%;right:10%;bottom:6px;height:7px;z-index:4;
  border-radius:999px;border:2px solid var(--ink);
  background:rgba(35,20,10,.5);overflow:hidden;
}
.fl-bar{display:none}
.fl-plot[data-state="grow"] .fl-bar{display:block}
.fl-bar i{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(180deg,#8ce99a,#51cf66)}

/* The fortnight plant. A body colour of its own, so the one cell that is NOT
   part of the bed looks like it is not part of the bed — the rule is visible
   before it is explained. */
/* Still owed a windfall. The gold ring is the same "you can have this" gold the
   armed board and every affordable price wear, and it restates the plot's own
   lip and contact shadow rather than replacing them. */
.fl-plot.marked{
  box-shadow:0 4px 0 #392a1c, 0 8px 14px rgba(44,26,16,.24), 0 0 0 3px var(--coin);
}
.fl-plot.century{
  background:linear-gradient(180deg,#5b4a7a 0%, #463962 72%, #332a49 100%);
  box-shadow:0 4px 0 #2a2240, 0 8px 14px rgba(44,26,16,.28);
}
.fl-plot.century .fl-floor{opacity:.25}
.fl-plot.century .fl-wait{background:linear-gradient(180deg,#efe7ff,#d8ccff)}
.fl-plot.century .fl-bar i{background:linear-gradient(180deg,#e5dbff,#b197fc)}

.fl-flower-cell{display:grid;place-items:center;position:relative}
.fl-flower{background:none;border:0;padding:0;width:86%;line-height:0;pointer-events:auto}
.fl-flower svg{width:100%;height:auto;overflow:visible}

/* THE BED CHIP — Fall's whole rule as one object, UNDER the board, on screen
   from the first second. Everything else about the windfall is this chip
   changing state. */
/* The margin is UNDER the board now, not over it: it is the chip's room, and
   the frame centres board-plus-room so the reserve cannot be squeezed out. */
.fl-wrap{position:relative;display:grid;place-items:center;margin-bottom:46px}
.fl-chip{
  position:absolute;left:50%;transform:translateX(-50%);
  /* MOVED BELOW THE BOARD, 2026-08-30, on the owner's word that it intrudes.
     Above it, the chip's last 2px sat inside the board and its lower third lay
     across the stubble fringe — so Fall's one rule was drawn on top of Fall's
     one picture, and on a notched phone the board fills the frame and the chip
     was pushed off the top of it entirely. Below, it sits in the strip the
     yard already reserves and Fall deliberately empties: no creatures stand
     there in this season, and it reads as the board's caption rather than as
     something laid over it. Anchored to `.fl-wrap`, which IS the board's box,
     so it tracks the board at every viewport instead of being measured from a
     frame the board does not fill. */
  bottom:-42px;z-index:6;pointer-events:none;
  display:flex;align-items:center;gap:7px;
  max-width:96%;padding:6px 13px;white-space:nowrap;
  border:3px solid var(--ink);border-radius:999px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 3px 0 var(--ink-2), 0 6px 10px rgba(44,26,16,.2);
  font-size:12.5px;font-weight:800;line-height:1;
}
.fl-chip svg{width:18px;height:18px;flex:none}
.fl-chip.close{background:linear-gradient(180deg,#fff6c2,#ffe066)}
/* The pulse lives on a pseudo-element, never on the chip: affordPulse writes
   `transform`, which would silently throw away the chip's own centring — the
   same collision that eats a lip when a state modifier writes box-shadow. */
.fl-chip.armed{background:linear-gradient(180deg,#ffe98a,var(--coin))}
.fl-chip.armed::before{
  content:'';position:absolute;inset:-6px;border-radius:999px;
  background:rgba(255,201,60,.35);z-index:-1;
  animation:affordPulse 1.4s ease-in-out infinite;
}

/* ---------- the season edges ----------
   How a player finds out sideways exists. The horizontal answer to the burrow
   door: a labelled thing you can tap, beside a gesture that does the same. They
   sit low, over the lawn — centred vertically they land ON the board, and the
   board is the thing this game is. */
/* NOT a grid item. `.ui` has one explicit column, and an item with a definite
   row but an auto column forces the next auto-placed item into an IMPLICIT
   second column — which narrowed the whole interface to half the screen and
   squashed the dock into a corner. Absolute against `.ui` instead, clear of the
   dock, so it can never touch the row layout. */
.season-edges{position:absolute;left:0;right:0;top:0;
  bottom:calc(var(--bottom-gap) + 104px);pointer-events:none;z-index:5}
.in-hollow .season-edges,
.in-meadow .season-edges{display:none}
.s-edge{
  position:absolute;bottom:0;pointer-events:auto;
  display:flex;flex-direction:column;align-items:center;gap:3px;
  width:38px;padding:9px 4px;
  border:3px solid var(--ink);
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.24);
  font-size:9.5px;font-weight:800;line-height:1.05;text-align:center;
}
.s-edge svg{width:19px;height:19px}
.s-edge .w{writing-mode:vertical-rl;text-orientation:mixed;letter-spacing:.06em}
.s-edge.r{right:0;border-right:0;border-radius:16px 0 0 16px}
.s-edge.l{left:0;border-left:0;border-radius:0 16px 16px 0}
.s-edge:active{transform:translateY(3px);box-shadow:0 1px 0 var(--ink-2)}
/* A locked edge is a promise you can read from here — drained paper and the
   turn that opens it, so nobody has to walk to a gate to find out. */
.s-edge.locked{
  background:linear-gradient(180deg,var(--paper-dim),var(--paper-dim-2));
  box-shadow:0 4px 0 var(--paper-dim-edge), 0 8px 14px rgba(44,26,16,.24);
}
.s-edge .turn{font-size:8.5px;opacity:.75;letter-spacing:0}
/* The dock's attention dot, on a season. Same object, same meaning: there is
   something in there worth opening. */
.s-edge .s-dot{
  position:absolute;top:-6px;right:-4px;
  width:16px;height:16px;border-radius:50%;
  background:#ff6b6b;border:3px solid var(--ink);
  box-shadow:0 2px 0 var(--ink-2);
}
.s-edge{position:absolute;bottom:0;pointer-events:auto}

/* ---------- a locked season, seen from its gate ----------
   A gate is a promise, not a wall: a real backdrop, a hedge across it, a
   padlock, the turn that opens it, and one thing drifting. The HUD stays up —
   that is the rule for every place in this game — and everything else goes. */
.gate-layer{position:absolute;inset:0;z-index:3;overflow:hidden}
/* Same rule as the scene above it: nothing may leave its own colour against the
   page's lawn at the bottom edge of .game. */
.gate-layer::after{
  content:'';position:absolute;left:0;right:0;bottom:0;height:44px;pointer-events:none;z-index:5;
  background:linear-gradient(180deg,rgba(79,174,84,0),#4fae54);
}
.gate-layer[hidden]{display:none}
.in-gate .stage,
.in-gate #dock,
.in-gate .rail,
.in-gate .quest-strip,
.in-gate .season-edges{display:none}
.in-gate .coach{display:none}
/* `.ui` is a stacking context covering the whole screen, so it eats every tap
   meant for a layer beneath it — the gate's "Back to the garden" was the only
   visible control on that screen and it was dead. `.in-meadow` has carried this
   pair since the meadow shipped; the gate was built without it. The HUD opts back in, because the wallets and the settings are reachable
   from every room. */
.in-gate .ui{pointer-events:none}
.in-gate .hud{pointer-events:auto}
.g-sky{position:absolute;inset:0}
.g-hedge{position:absolute;left:-6%;right:-6%;bottom:0;height:26%;line-height:0;display:flex}
/* TILED, not stretched. One hedge stretched across a phone turns its lumps into
   a green hill and its flowers into ellipses — the same failure as slicing a
   backdrop composed at 390 into a wider window. Each copy keeps its own
   proportions and the row repeats to fill. */
.g-hedge .hedge-svg{flex:1 1 0;min-width:0;height:100%}
.g-plate{
  position:absolute;left:50%;top:52%;transform:translate(-50%,-50%);z-index:3;
  display:grid;justify-items:center;gap:10px;text-align:center;
}
.g-lock{
  width:74px;height:74px;border-radius:50%;display:grid;place-items:center;
  border:4px solid var(--ink);
  background:linear-gradient(180deg,#fffdf7,var(--paper-3));
  box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.22);
}
.g-lock svg{width:40px;height:40px}
.g-name{font-size:34px;font-weight:800;letter-spacing:.06em}
.g-leaf{position:absolute;width:22px;opacity:.85;z-index:2;
  animation:gateDrift 11s linear infinite}
@keyframes gateDrift{
  0%{transform:translate(0,-6%) rotate(0)}
  100%{transform:translate(-70px,120vh) rotate(220deg)}
}
.g-back{
  position:absolute;left:50%;bottom:calc(var(--sab) + 24px);transform:translateX(-50%);z-index:4;
  display:flex;align-items:center;gap:7px;padding:9px 16px;
  border:3px solid var(--ink);border-radius:999px;
  background:linear-gradient(180deg,#fffdf7,var(--paper-2));
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.22);
  font-size:14px;font-weight:800;
}
.g-back svg{width:20px;height:20px}
@media (prefers-reduced-motion:reduce){
  .g-leaf{animation:none}
  .fl-chip.armed::before{animation:none}
  .fl-ready{animation:none}
}

/* =========================================================
   WHAT'S NEW — the announcement dialog
   =========================================================
   Never fullscreen: a card in the middle of the screen with the house
   material on it, the scrim doing the rest. It brings its own scrim rather
   than borrowing the sheet's, because the sheet's belongs to a thing that can
   be dragged away and this cannot — its one button is the only way out.
   Above everything else the game can put up (banner 70, coach 65, toasts 60),
   since it is the first thing a player sees on a new build. */
.news[hidden]{display:none}
.news{
  position:absolute;inset:0;z-index:90;display:grid;place-items:center;
  background:rgba(30,18,10,.5);
  backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);
}
/* THE DIM IS THE LAYER ITSELF, not a child of it. An absolutely-positioned
   child of a grid container inherits `place-items`, so `inset: 0` with
   `align-self: center` sizes the box to its content — a separate scrim element
   measured 0x0 and the game behind it stayed bright, with no error anywhere.
   The layer is already the full screen; it carries its own colour. */
.news-card{
  position:relative;z-index:1;
  width:min(340px, calc(100% - 40px));
  max-height:calc(100dvh - var(--sat) - var(--sab) - 48px);
  overflow:auto;-webkit-overflow-scrolling:touch;
  margin:calc(var(--sat) + 12px) 0 calc(var(--sab) + 12px);
  padding:12px 14px 14px;
  background:linear-gradient(180deg,#fffdf7 0%, var(--paper-2) 72%, var(--paper-3) 100%);
  border:4px solid var(--ink);border-radius:var(--r-lg);
  /* A card's lip is 4px and its contact shadow is the 8/14 that goes with it. */
  box-shadow:0 4px 0 var(--ink-2), 0 8px 14px rgba(44,26,16,.24);
  transform:translateY(14px) scale(.96);
  transition:transform .28s cubic-bezier(.34,1.4,.64,1);
}
.news.show .news-card{transform:none}
/* The art is owner-supplied and portrait. It gets a landscape window with the
   picture's middle in it — the flower and the bees — rather than its own
   aspect, which on a phone would be the whole dialog and then some. */
.news-art{
  margin:-2px -2px 10px;
  aspect-ratio:4 / 3;overflow:hidden;
  border:3px solid var(--ink);border-radius:var(--r-md);
  background:linear-gradient(180deg,#bfe9ff,#e9f8ff);
  box-shadow:0 3px 0 var(--ink-2);
}
.news-art img{display:block;width:100%;height:100%;object-fit:cover;object-position:50% 42%}
.news h2{margin:0 0 6px;font-size:21px;font-weight:800;text-align:center;line-height:1.15}
.news-list{margin:0;padding:0;list-style:none;display:grid;gap:7px}
.news-list li{
  position:relative;padding-left:20px;
  font-size:13px;font-weight:600;line-height:1.4;color:var(--ink);
}
/* The bullet is a seed-green dot rather than a disc glyph, so it belongs to
   this game rather than to a document. */
.news-list li::before{
  content:'';position:absolute;left:4px;top:6px;
  width:8px;height:8px;border-radius:50%;
  background:var(--seed,#57c15b);border:2px solid var(--ink);
}
.news-note{
  margin:10px 0 0;text-align:center;
  font-size:12px;font-weight:800;font-style:italic;color:var(--ink-soft);
}
.news .big-btn{margin-top:12px}
@media (prefers-reduced-motion:reduce){
  .news-card{transition:none;transform:none}
}

/* =========================================================
   THE SKY PASS — the staged weather layers
   =========================================================
   The approved motion is `tools/sky-spike.html`; this is that stylesheet moved
   onto the game's own root. Three systematic differences and nothing else: the
   stage's `#frame[data-sky][data-phase]` is `#game[data-weather][data-wx-phase]`
   here, every tuned number arrives as a `--wx-<section>-<key>` custom property
   written onto `#game` from `DATA.weatherStage` (so each `var()` carries the
   approved value as its fallback and a sheet that loads before the first write
   still paints), and the stage's reduced-motion toggle is the media query that
   closes this section.

   Markup, inside `#world` between `.scenery` and `.fall-layer`:

     <div class="wx" id="wx" aria-hidden="true">
       <div class="wx-wash"></div>
       <div class="wx-front"><i class="wx-front-cloud"></i> x6</div>
       <div class="wx-bolt"></div>
       <div class="wx-dusk"></div>
       <div class="wx-ribbons"><i class="wx-ribbon" style="--i:0"></i> x3</div>
       <div class="wx-ground"></div>
       <div class="wx-veil"></div>
       <div class="wx-takeover"></div>
       <div class="wx-sunbreak"><i class="wx-ray"></i> x4</div>
       <div class="wx-flash-under"></div>
     </div>

   and, as a direct child of `#world` AFTER `.ui`, because a flash that does not
   silhouette the garden is a grey rectangle:

     <div class="wx-flash" id="wxFlash"></div>

   Opt-in hooks the world wears, so this section never has to know what the
   garden is called: `.wx-wet` on plot interiors, the meadow layer and the fence
   · `.wx-glint`, `.wx-lean` and `.wx-lit` on plants · `.wx-shelter` on tending
   creatures · `.wx-splashed` for ~340ms where a drop lands on one.
   ========================================================= */

#game{
  /* Storm grey and a sun shaft have no name in the palette, and inventing one in
     `--wx-*` space would collide with the knob mirror `DATA.weatherStage` writes. */
  --wxr-wash-a:#6d829a;
  --wxr-wash-b:#93a5b6;
  --wxr-wash-c:#b3c0cc;
  --wxr-cloud:#94a4b4;
  --wxr-cloud-d:#7e8fa1;
  --wxr-ray:#fff6d8;
  --wxr-bolt:#eaf6ff;
  --wxr-pulse:#bcd3e6;
  --wxr-leaf:#57c15b;
  /* iOS paints the strip below a short window with the flat lawn, so anything
     tinting the whole screen stops short of the bottom exactly as `.season-tint`
     does, or it draws the join three rounds of layout work went into hiding. */
  --wxr-mask:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);

  /* Blue, purple and gold are the rarity vocabulary and cyan is the gem, so a
     sky-wide band borrowing any of them would read as a tier. */
  --wx-aurora-1:#5ff0b6;
  --wx-aurora-2:#49c9d8;
  --wx-aurora-3:#a86ee0;
}
#game[data-weather="storm"]{
  --wxr-wash-a:#41506a;
  --wxr-wash-b:#5f6f88;
  --wxr-wash-c:#8390a4;
  --wxr-cloud:#7a8798;
  --wxr-cloud-d:#5d6b7d;
}

/* THE WEATHER LAYER. Above the living sky, below the interface.
   NO z-index, and nothing here may grow one. A positioned element with a
   z-index is a stacking context, and a stacking context is an isolated blending
   group — every multiply, screen and overlay inside would blend against
   transparency instead of against the sky, which is the one thing the
   compositing order exists to prevent. `isolation`, `opacity` under 1, `filter`
   and `transform` on this box are the same bug spelled differently. DOM order
   already puts it above `.scenery` and below `.fall-layer`, `.ui` and
   `.fx-canvas`, each of which carries its own. */
.wx{position:absolute;inset:0;pointer-events:none}

/* ---------------------------------------------------------
   THE FRONT — the announce channel. One channel: clouds.
   --------------------------------------------------------- */
.wx-front{
  position:absolute;left:0;right:0;top:0;height:34%;
  pointer-events:none;
  opacity:0;
  transform:translateX(-14%);
  transition:opacity 2.4s linear, transform 2.4s ease-out;
}
.wx-front-cloud{
  position:absolute;display:block;
  left:var(--x,20%);top:var(--t,8%);
  width:var(--w,120px);height:calc(var(--w,120px) * .44);
  margin-left:calc(var(--w,120px) * -.4);
  background:var(--wxr-cloud);
  border-radius:999px;
  opacity:var(--o,.9);
  filter:drop-shadow(0 5px 0 rgba(70,92,112,.22));
  animation:wxCloudDrift var(--dur,10s) ease-in-out infinite alternate;
  animation-delay:var(--delay,0s);
}
.wx-front-cloud::before,
.wx-front-cloud::after{
  content:'';position:absolute;background:var(--wxr-cloud);border-radius:50%;
}
.wx-front-cloud::before{width:56%;height:112%;left:8%;top:-54%}
.wx-front-cloud::after{width:44%;height:94%;right:10%;top:-40%}
.wx-front-cloud:nth-child(1){--x:6%;--t:3%;--w:132px;--dur:9.2s;--o:.94;--delay:0s}
.wx-front-cloud:nth-child(2){--x:44%;--t:1%;--w:104px;--dur:11.4s;--o:.88;--delay:-3.1s}
.wx-front-cloud:nth-child(3){--x:70%;--t:9%;--w:150px;--dur:8.6s;--o:.96;--delay:-1.4s}
.wx-front-cloud:nth-child(4){--x:18%;--t:17%;--w:96px;--dur:12.8s;--o:.80;--delay:-5.2s}
.wx-front-cloud:nth-child(5){--x:56%;--t:22%;--w:120px;--dur:10.1s;--o:.74;--delay:-2.3s}
.wx-front-cloud:nth-child(6){--x:88%;--t:13%;--w:88px;--dur:13.6s;--o:.70;--delay:-6.8s}
@keyframes wxCloudDrift{
  from{transform:translateX(-22px)}
  to{transform:translateX(26px)}
}

/* The front takes the whole announce window to arrive; the sequence writes
   `--wx-front-dur` from `frontSeconds` or `calledFrontSeconds`. */
#game:is([data-weather="rain"],[data-weather="storm"]) .wx-front{
  opacity:1;
  transform:translateX(0);
  transition-duration:var(--wx-front-dur, var(--wx-front-seconds, 30s));
}
/* The storm's front is heavier: it reaches further down the sky and moves faster. */
#game[data-weather="storm"] .wx-front{height:44%}
#game[data-weather="storm"] .wx-front-cloud{
  animation-duration:calc(var(--dur,10s) * max(.15, 1 - var(--wx-storm-wind,.42) * .9));
}
/* The clouds part at the end — this is what the rays come through. */
#game:is([data-weather="rain"],[data-weather="storm"])[data-wx-phase="end"] .wx-front{
  opacity:.26;
  transform:translateX(6%);
  transition-duration:7s;
}

/* ---------------------------------------------------------
   THE WASH — weather is a tint over the living sky, never a
   replacement, so this multiplies and the season tint stays under it.
   --------------------------------------------------------- */
.wx-wash{
  position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,
    var(--wxr-wash-a) 0%,
    var(--wxr-wash-b) 58%,
    var(--wxr-wash-c) 100%);
  mix-blend-mode:multiply;
  opacity:0;
  transition:opacity 3.6s ease;
  -webkit-mask-image:var(--wxr-mask);
  mask-image:var(--wxr-mask);
}
#game[data-weather="rain"] .wx-wash{opacity:var(--wx-rain-wash,.46)}
#game[data-weather="storm"] .wx-wash{opacity:var(--wx-storm-wash,.68)}
/* Announced, not landed: the sky only half-commits during the front. */
#game[data-weather="rain"][data-wx-phase="front"] .wx-wash{
  opacity:calc(var(--wx-rain-wash,.46) * .5);
  transition-duration:var(--wx-front-dur, var(--wx-front-seconds, 30s));
}
#game[data-weather="storm"][data-wx-phase="front"] .wx-wash{
  opacity:calc(var(--wx-storm-wash,.68) * .58);
  transition-duration:var(--wx-front-dur, var(--wx-front-seconds, 30s));
}
#game:is([data-weather="rain"],[data-weather="storm"])[data-wx-phase="end"] .wx-wash{
  opacity:0;
  transition-duration:6.5s;
}

/* ---------------------------------------------------------
   WET GROUND — the trace that outlasts the rain.
   The BASE rule is the drying state, so the ground keeps drying over
   `--wx-rain-linger` even after `data-weather` has gone back to clear.
   --------------------------------------------------------- */
.wx-ground{
  position:absolute;left:0;right:0;bottom:0;height:48%;
  pointer-events:none;
  background:linear-gradient(180deg,
    transparent 0%,
    color-mix(in srgb, var(--soil-dd) 55%, #ffffff) 34%,
    var(--soil-dd) 100%);
  mix-blend-mode:multiply;
  opacity:0;
  transition:opacity var(--wx-rain-linger, 30s) linear;
  -webkit-mask-image:var(--wxr-mask);
  mask-image:var(--wxr-mask);
}
#game[data-weather="rain"] .wx-ground{opacity:var(--wx-rain-wetness,.34);transition-duration:3.2s}
#game[data-weather="storm"] .wx-ground{opacity:var(--wx-storm-wetness,.46);transition-duration:2.4s}
#game:is([data-weather="rain"],[data-weather="storm"])[data-wx-phase="front"] .wx-ground{
  opacity:0;transition-duration:1.2s;
}
#game:is([data-weather="rain"],[data-weather="storm"])[data-wx-phase="end"] .wx-ground{
  opacity:0;transition-duration:var(--wx-rain-linger, 30s);
}

/* The board and the cobbles are the world too, and they live above `.wx`, so
   they carry the wetness on themselves: darker AND richer, which is what wet
   material actually does. */
/* Two writers want this element's `filter` — the sky's wetness and the watering
   can's flash — and `filter` is one property, so the flash would snap a rain-dark
   plot back to dry for its whole 0.8s. They compose through the numbers instead:
   the sky sets these two, the flash animates them, and neither restates the other. */
.wx-wet{
  filter:brightness(var(--wx-wet-b,1)) saturate(var(--wx-wet-s,1));
  transition:filter var(--wx-rain-linger, 30s) linear;
}
#game[data-weather="rain"] .wx-wet{
  --wx-wet-b:calc(1 - var(--wx-rain-wetness,.34) * .40);
  --wx-wet-s:calc(1 + var(--wx-rain-wetness,.34) * .55);
  transition-duration:3.2s;
}
#game[data-weather="storm"] .wx-wet{
  --wx-wet-b:calc(1 - var(--wx-storm-wetness,.46) * .40);
  --wx-wet-s:calc(1 + var(--wx-storm-wetness,.46) * .55);
  transition-duration:2.4s;
}
#game:is([data-weather="rain"],[data-weather="storm"])[data-wx-phase="front"] .wx-wet{
  --wx-wet-b:1;--wx-wet-s:1;transition-duration:1.2s;
}
#game:is([data-weather="rain"],[data-weather="storm"])[data-wx-phase="end"] .wx-wet{
  --wx-wet-b:1;--wx-wet-s:1;transition-duration:var(--wx-rain-linger, 30s);
}

/* ---------------------------------------------------------
   GLISTEN — a wet rim on the plants. The declared filter is the state;
   the keyframe only breathes it, so a collapsed animation loses nothing.
   --------------------------------------------------------- */
/* `filter` is one property, so a rim written flat deletes whatever the plant was
   already wearing — and `.plant.has-glow` is the bloom a sheen should flatter
   most. The plant's own glow composes in through a variable rather than the rim
   being restated for every combination: a restatement would need its own rule
   and its own keyframe, and both would out-rank `.wx-lean` and quietly take the
   storm's sway off exactly the plants that glow. `opacity(1)` is the no-op that
   stands in its place. */
.wx-glint{transition:filter 2.4s ease;--wx-own-glow:opacity(1)}
.plant.has-glow.wx-glint{--wx-own-glow:drop-shadow(0 0 7px var(--glow))}
/* A mutated bloom carries its own glow for the whole time it is in the ground,
   and that permanence is the mechanic rather than a flourish. It composes in the
   same way, and a bloom that is both mutated and a glower keeps both. */
.plot[data-mutation] .plant.wx-glint{--wx-own-glow:drop-shadow(0 0 6px var(--mut-glow,#cfeeff))}
.plot[data-mutation] .plant.has-glow.wx-glint{
  --wx-own-glow:drop-shadow(0 0 7px var(--glow)) drop-shadow(0 0 6px var(--mut-glow,#cfeeff));
}
/* `animation` is one property too, and these hooks out-rank the ripe wiggle at
   style.css's `.plot[data-state="ready"] .plant`. Listing the wiggle alongside
   keeps a ready bloom inviting the tap through a shower — it is the only thing
   on the board saying "pick me", and losing it for a quarter of all slots is a
   worse bug than the sheen is a feature. A plant that is not ready simply has
   nothing to run for that name. */
#game:is([data-weather="rain"],[data-weather="storm"]):is([data-wx-phase="transform"],[data-wx-phase="linger"]) .wx-glint{
  filter:var(--wx-own-glow) drop-shadow(0 -1.5px 0 rgba(255,255,255,.62)) saturate(1.06);
  animation:wxGlint 3.4s ease-in-out infinite, plantWiggle .8s ease-in-out infinite;
}
#game:is([data-weather="rain"],[data-weather="storm"]):is([data-wx-phase="transform"],[data-wx-phase="linger"]) .plot:not([data-state="ready"]) .wx-glint{
  animation:wxGlint 3.4s ease-in-out infinite;
}
@keyframes wxGlint{
  0%,100%{filter:var(--wx-own-glow) drop-shadow(0 -1.5px 0 rgba(255,255,255,.62)) saturate(1.06)}
  50%{filter:var(--wx-own-glow) drop-shadow(0 -1.5px 0 rgba(255,255,255,.95))
             drop-shadow(0 0 4px rgba(220,242,255,.6)) saturate(1.13)}
}

/* A drop landing on a plant gets the house squash-and-bounce. */
.wx-splashed{
  transform-origin:50% 100%;
  animation:wxSquash .32s cubic-bezier(.34,1.56,.64,1);
}
@keyframes wxSquash{
  0%{transform:scale(1,1)}
  35%{transform:scale(1.16,.84)}
  70%{transform:scale(.92,1.1)}
  100%{transform:scale(1,1)}
}


  50%{transform:translateY(-2px) rotate(-1deg) scale(1.02)}
}

/* The flower's right leaf swings up and lies across the top of its head. */
#game:is([data-weather="rain"],[data-weather="storm"]):is([data-wx-phase="transform"],[data-wx-phase="linger"]) .tf-leaf-r{
  animation:wxLeafHold 2.8s ease-in-out infinite;
  transform:translate(-17px,-80px) rotate(31deg) scale(1.35);
  transition:transform .55s cubic-bezier(.34,1.56,.64,1);
}
@keyframes wxLeafHold{
  0%,100%{transform:translate(-17px,-80px) rotate(31deg) scale(1.35)}
  50%{transform:translate(-15px,-83px) rotate(26deg) scale(1.38)}
}

/* ---------------------------------------------------------
   THE WIND — a storm channel only. Rain's wind is the drops' lean and
   nothing else; rarity buys the layer.
   --------------------------------------------------------- */
.wx-lean{transform-origin:50% 100%;transition:transform 1.6s ease}
#game[data-weather="storm"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .wx-lean{
  transform:rotate(calc(var(--wx-storm-wind,.42) * 9deg));
  animation:wxLean 2.2s ease-in-out infinite, plantWiggle .8s ease-in-out infinite;
}
#game[data-weather="storm"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .plot:not([data-state="ready"]) .wx-lean{
  animation:wxLean 2.2s ease-in-out infinite;
}
@keyframes wxLean{
  0%,100%{transform:rotate(calc(var(--wx-storm-wind,.42) * 5deg))}
  50%{transform:rotate(calc(var(--wx-storm-wind,.42) * 13deg))}
}

/* Pets duck under the nearest leaves and peek out.

   `transform` is one property and `.critter` uses it to sit on its own anchor:
   `left: var(--x)` puts its left edge on the spot and `translateX(-50%)` slides
   it back onto it. A pose written without that centring does not just crouch a
   creature, it shifts every one of them half their own width to the right — so
   the centring is restated in the pose and in every frame of the peek. */
.wx-shelter{transform-origin:50% 100%;transition:transform .55s cubic-bezier(.34,1.56,.64,1)}
#game[data-weather="storm"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .wx-shelter{
  transform:translateX(-50%) translateY(7px) scale(.88) rotate(-4deg);
  animation:wxPeek 4.4s ease-in-out infinite;
}
@keyframes wxPeek{
  0%,100%{transform:translateX(-50%) translateY(7px) scale(.88) rotate(-4deg)}
  46%{transform:translateX(-50%) translateY(3px) scale(.92) rotate(1deg)}
  62%{transform:translateX(-50%) translateY(3px) scale(.92) rotate(1deg)}
}

/* ---------------------------------------------------------
   THE FLASH — asked for through `data-wx-flash`, never painted by the layer
   that asks, which is what leaves the ~3-in-10s ceiling something to enforce.
   Two layers: the under-flash whites out the SKY so the garden reads as a
   silhouette, the over-flash is the flicker across everything.
   --------------------------------------------------------- */
.wx-flash-under{
  position:absolute;inset:0;pointer-events:none;
  background:#fff;
  opacity:0;
  -webkit-mask-image:var(--wxr-mask);
  mask-image:var(--wxr-mask);
}
.wx-flash{
  position:absolute;inset:0;pointer-events:none;
  z-index:40;
  background:linear-gradient(180deg,
    rgba(255,255,255,1) 0%,
    rgba(255,255,255,.92) 40%,
    rgba(255,255,255,.34) 74%,
    rgba(255,255,255,.12) 100%);
  opacity:0;
  -webkit-mask-image:var(--wxr-mask);
  mask-image:var(--wxr-mask);
}
#game[data-wx-flash="1"] .wx-flash{animation:wxFlash 120ms linear}
#game[data-wx-flash="1"] .wx-flash-under{animation:wxFlashUnder 120ms linear}
@keyframes wxFlash{
  0%{opacity:0}
  10%{opacity:var(--wx-storm-flash-bright,.72)}
  34%{opacity:calc(var(--wx-storm-flash-bright,.72) * .22)}
  52%{opacity:calc(var(--wx-storm-flash-bright,.72) * .78)}
  100%{opacity:0}
}
@keyframes wxFlashUnder{
  0%{opacity:0}
  10%{opacity:min(1, calc(var(--wx-storm-flash-bright,.72) * 1.25))}
  34%{opacity:calc(var(--wx-storm-flash-bright,.72) * .3)}
  52%{opacity:min(1, calc(var(--wx-storm-flash-bright,.72) * .95))}
  100%{opacity:0}
}

/* The bolt is glimpsed, not watched: it runs from the cloud base down to the
   horizon and stops there, so it reads as being behind the hills rather than
   pasted over them. */
.wx-bolt{
  position:absolute;
  top:12%;left:var(--wx-bolt-x,62%);
  width:46px;height:60%;
  margin-left:-23px;
  pointer-events:none;
  background:linear-gradient(180deg,#fff 0%,var(--wxr-bolt) 100%);
  clip-path:polygon(62% 0%, 20% 50%, 46% 50%, 26% 100%, 80% 44%, 52% 44%, 78% 0%);
  filter:drop-shadow(0 0 7px rgba(216,240,255,.9));
  opacity:0;
  -webkit-mask-image:linear-gradient(180deg,#000 0 80%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 0 80%,transparent 100%);
}
#game[data-weather="storm"][data-wx-flash="1"] .wx-bolt{animation:wxBolt 120ms linear}
@keyframes wxBolt{
  0%{opacity:0}
  12%{opacity:.95}
  40%{opacity:.2}
  56%{opacity:.8}
  100%{opacity:0}
}

/* ---------------------------------------------------------
   THE SUNBREAK — the sun shooting through after a shower.
   Light wedges, screen blend, low opacity, slow drift. Never particles.
   Ray count responds in pure CSS: rays 3 and 4 fade in as
   `--wx-sunbreak-rays` climbs past 2 and 3, so the knob needs no JS.
   --------------------------------------------------------- */
.wx-sunbreak{
  position:absolute;inset:0;pointer-events:none;
  overflow:hidden;
  mix-blend-mode:screen;
  opacity:0;
  transition:opacity 2.8s ease;
  -webkit-mask-image:var(--wxr-mask);
  mask-image:var(--wxr-mask);
}
.wx-ray{
  position:absolute;display:block;
  left:var(--x,20%);top:-26%;
  width:var(--w,90px);height:132%;
  background:linear-gradient(90deg,
    rgba(255,246,216,0) 0%,
    var(--wxr-ray) 50%,
    rgba(255,246,216,0) 100%);
  clip-path:polygon(42% 0%, 58% 0%, 92% 100%, 8% 100%);
  /* A shaft attenuates as it travels; the container keeps the canonical bottom
     mask and this one only shapes the falloff. */
  -webkit-mask-image:linear-gradient(180deg,#000 0 32%,rgba(0,0,0,.42) 78%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 0 32%,rgba(0,0,0,.42) 78%,transparent 100%);
  filter:blur(9px);
  /* The phase animation owns opacity from here; this is the value it returns to
     if the animation is ever cancelled, so a ray is never invisible by default. */
  opacity:calc(var(--wx-sunbreak-opacity,.28) * var(--k,1));
  transform-origin:50% 0%;
  /* Two motions, deliberately on different clocks. The sweep is the shafts
     crossing the sky — off the left edge to off the right, so there is no wrap to
     hide and a ray is never seen to jump. The phase is each shaft brightening and
     dissolving on its own offset, which is what keeps three rays from reading as
     one object sliding sideways. Together: always moving, never busy. */
  transform:rotate(var(--a,17deg)) translateX(calc(var(--vw,1vw) * -45));
  animation:
    wxRaySweep calc(45s / max(.05, var(--wx-sunbreak-drift-speed,.5))) linear infinite,
    wxRayPhase calc(11s / max(.05, var(--wx-sunbreak-drift-speed,.5))) ease-in-out infinite;
}
/* Every ray is at a different point of BOTH cycles, so the sky is never
   symmetrical and never all-bright at once. The negative delays are what put
   them mid-journey the moment the sun comes out, rather than all lined up at the
   left edge waiting to start. */
.wx-ray:nth-child(1){--x:0%;--w:96px;--a:15deg;--k:1;
  animation-delay:-26s,-1s}
.wx-ray:nth-child(2){--x:0%;--w:70px;--a:19deg;--k:1;
  animation-delay:-45s,-7s}
.wx-ray:nth-child(3){--x:0%;--w:112px;--a:13deg;
  --k:clamp(0, calc(var(--wx-sunbreak-rays,3) - 2), 1);
  animation-delay:-64s,-13s}
.wx-ray:nth-child(4){--x:0%;--w:62px;--a:21deg;
  --k:clamp(0, calc(var(--wx-sunbreak-rays,3) - 3), 1);
  animation-delay:-35s,-17s}
/* Off the left edge to off the right, so the loop point is never on screen and a
   ray is never seen to jump back. At the default drift a shaft crosses about a
   third of the sky in the thirty seconds the sunbreak lives — slow enough to be
   weather, fast enough to be movement.

   The distance is a share of the VIEWPORT, never a percentage of the ray: the
   four shafts are deliberately different widths, and a percentage translate would
   give each of them a different journey and bunch them together in the middle. */
@keyframes wxRaySweep{
  from{transform:rotate(var(--a,17deg)) translateX(calc(var(--vw,1vw) * -45))}
  to{transform:rotate(calc(var(--a,17deg) + 4deg)) translateX(calc(var(--vw,1vw) * 145))}
}
/* At phase 0 a shaft simply crosses at a steady brightness; at 1 it arrives out
   of nothing and dissolves back into it. The sweep runs off both edges either
   way, so the knob only ever decides how much of the coming and going you see. */
@keyframes wxRayPhase{
  0%,100%{opacity:calc(var(--wx-sunbreak-opacity,.28) * var(--k,1)
                       * (1 - var(--wx-sunbreak-phase,.75)))}
  34%,58%{opacity:calc(var(--wx-sunbreak-opacity,.28) * var(--k,1))}
}

/* Visible is the base style; the life animation only shapes the fade so the rays
   go out as the soil dries. Remove `data-sunbreak` and the base transition takes
   them. */
#game[data-sunbreak="1"] .wx-sunbreak{
  opacity:1;
  animation:wxSunbreakLife var(--wx-sunbreak-duration,30s) ease-in-out forwards;
}
@keyframes wxSunbreakLife{
  0%{opacity:0}
  14%{opacity:1}
  62%{opacity:1}
  100%{opacity:.06}
}
/* Daytime only — the sun is not behind those clouds at night. */
#game[data-wx-night="1"][data-sunbreak="1"] .wx-sunbreak{opacity:0;animation:none}

/* =========================================================
   AURORA — dusk at noon
   ========================================================= */
/* The light rules bend: the sky dims toward dusk whatever the hour, which is
   the whole reason an aurora reads at midday. A multiply over the living sky,
   never a replacement, and masked over the last 44px like the season tint. */
.wx-dusk{
  position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,
    rgba(19,42,82,.94) 0%,
    rgba(43,74,122,.88) 52%,
    rgba(90,122,158,.74) 100%);
  mix-blend-mode:multiply;
  opacity:0;
  transition:opacity var(--wx-aurora-dusk-seconds,4s) ease;
  -webkit-mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
  mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
}
/* Both of the aurora's sky channels name the aurora as well as the dark, where
   the stage keyed them on the dark alone. On the stage `data-night` was only
   ever raised by the aurora or by a slider nobody left in the night; in the game
   it is true every night from the hour, and an ungated dusk multiplies the moon
   and the stars out of every ordinary night — a state no sky in this pass
   stages. The sunbreak still asks `data-wx-night` on its own, because "is it
   dark" is the question it needs answered. */
#game[data-weather="aurora"][data-wx-night="1"] .wx-dusk{opacity:var(--wx-aurora-dusk,.62)}

/* The stars come up with the dusk and never come down below the hour's own
   value, so a real night keeps the sky it already had. */
#game[data-weather="aurora"][data-wx-night="1"] .stars{
  opacity:max(var(--star-op), var(--wx-aurora-star-boost,.85));
  transition:opacity var(--wx-aurora-dusk-seconds,4s) linear;
}

/* =========================================================
   AURORA — the ribbons
   ========================================================= */
/* Layered gradients, not particles. The softness is in the gradient stops
   rather than a blur filter, so each band stays one cheap composited layer
   while it drifts. */
.wx-ribbons{
  position:absolute;left:0;right:0;top:0;height:58%;
  pointer-events:none;
}
.wx-ribbon{
  position:absolute;left:-34%;right:-34%;
  display:block;
  --ray:30px;
  --ray-angle:96deg;
  /* One soft ellipse of light: a band with edges reads as a stripe, and the
     stripe is what makes a coloured rectangle instead of a curtain. The
     gradient reaches transparent inside its own box, or the box clips it and
     puts the edge back. */
  background:radial-gradient(72% 58% at 50% 50%,
    color-mix(in srgb, var(--c) 95%, transparent) 0%,
    color-mix(in srgb, var(--c) 52%, transparent) 34%,
    color-mix(in srgb, var(--c) 14%, transparent) 60%,
    transparent 78%);
  /* The rays. One mask layer that dims rather than cuts, so it needs no
     mask-composite to fall back on, and all ramps, because a hard stop here
     reads as a barcode. */
  -webkit-mask-image:repeating-linear-gradient(var(--ray-angle),
    rgba(0,0,0,.34) 0px, #000 calc(var(--ray) * .46), rgba(0,0,0,.34) var(--ray));
  mask-image:repeating-linear-gradient(var(--ray-angle),
    rgba(0,0,0,.34) 0px, #000 calc(var(--ray) * .46), rgba(0,0,0,.34) var(--ray));
  mix-blend-mode:screen;
  translate:0 0;
  opacity:0;
  transition:opacity 1.6s ease;
}
/* Tilted, unevenly stacked and overlapping: three parallel bands at even
   spacing is a flag, not a sky. */
.wx-ribbon:nth-child(1){--c:var(--wx-aurora-1);top:-3%;height:38%;rotate:-7deg}
.wx-ribbon:nth-child(2){--c:var(--wx-aurora-2);top:11%;height:32%;rotate:-3deg;--ray:44px;--ray-angle:88deg}
.wx-ribbon:nth-child(3){--c:var(--wx-aurora-3);top:25%;height:28%;rotate:-12deg;--ray:23px;--ray-angle:101deg}

/* A sway rather than a loop: an alternating ease has no seam to snap at, and a
   curtain that breathes back and forth is what an aurora actually does.
   Duration is the tuned speed inverted, so the knob reads as "faster". */
@keyframes wxRibbonDrift{
  0%{transform:translate3d(-13%,0,0) skewY(-2.4deg) scaleY(1)}
  50%{transform:translate3d(2%,4%,0) skewY(1.2deg) scaleY(1.16)}
  100%{transform:translate3d(15%,-1%,0) skewY(3deg) scaleY(.94)}
}
#game[data-weather="aurora"] .wx-ribbon{
  animation:wxRibbonDrift calc(2.8s / var(--wx-aurora-ribbon-speed,.16))
    ease-in-out infinite alternate;
  animation-delay:calc(var(--i) * -9s);
  will-change:transform;
}
#game[data-weather="aurora"] .wx-ribbon:nth-child(2){
  animation-duration:calc(3.7s / var(--wx-aurora-ribbon-speed,.16));
}
#game[data-weather="aurora"] .wx-ribbon:nth-child(3){
  animation-duration:calc(4.6s / var(--wx-aurora-ribbon-speed,.16));
}

/* An aurora begins: the first ribbon alone, while the dusk is still arriving. */
#game[data-weather="aurora"][data-wx-phase="front"] .wx-ribbon{
  opacity:0;
  transition:opacity 2.6s ease 1.2s;
}
#game[data-weather="aurora"][data-wx-phase="front"] .wx-ribbon:first-child{
  opacity:calc(var(--wx-aurora-ribbon-opacity,.42) * .55);
}

/* Count and depth in one product: a band whose index the count has not reached
   resolves to zero, so the knob adds and removes ribbons without JS. */
#game[data-weather="aurora"][data-wx-phase="transform"] .wx-ribbon,
#game[data-weather="aurora"][data-wx-phase="linger"] .wx-ribbon{
  opacity:calc(var(--wx-aurora-ribbon-opacity,.42)
    * clamp(0, calc(var(--wx-aurora-ribbons,3) - var(--i)), 1));
}

/* They leave the way they came: off-edge, under their own drift. The slide is a
   transition on `translate` rather than a second animation, so it composes with
   the drift on `transform` instead of replacing it. */
#game[data-weather="aurora"][data-wx-phase="end"] .wx-ribbon{
  opacity:0;
  translate:132% -5%;
  transition:opacity 3.2s ease .8s, translate 4s ease-in;
}

/* =========================================================
   AURORA — glow rims
   ========================================================= */
/* Every plant takes the sky's light on its edge. `.wx-lit` is the hook for
   anything that is a growing thing but is not a `.plant`. */
#game[data-weather="aurora"] .plant,
#game[data-weather="aurora"] .talker,
#game[data-weather="aurora"] .wx-lit{
  transition:filter 1.4s ease;
}
/* `filter` is one property, so a rim written flat deletes whatever the plant was
   already wearing — and `.plant.has-glow` is the flower a glow rim should flatter
   most. Its own glow is restated beside the rim instead of being overwritten. */
#game[data-weather="aurora"][data-wx-phase="transform"] .plant.has-glow,
#game[data-weather="aurora"][data-wx-phase="linger"] .plant.has-glow{
  filter:drop-shadow(0 6px 0 rgba(44,26,16,.22))
         drop-shadow(0 0 7px var(--glow))
         drop-shadow(0 0 calc(var(--wx-aurora-rim-glow,.5) * 9px) var(--wx-aurora-2))
         drop-shadow(0 0 calc(var(--wx-aurora-rim-glow,.5) * 16px) var(--wx-aurora-1));
}
#game[data-weather="aurora"][data-wx-phase="transform"] .plant,
#game[data-weather="aurora"][data-wx-phase="transform"] .talker,
#game[data-weather="aurora"][data-wx-phase="transform"] .wx-lit,
#game[data-weather="aurora"][data-wx-phase="linger"] .plant,
#game[data-weather="aurora"][data-wx-phase="linger"] .talker,
#game[data-weather="aurora"][data-wx-phase="linger"] .wx-lit{
  /* The talker's own drop shadow is restated here for the same reason. */
  filter:
    drop-shadow(0 0 5px color-mix(in srgb, var(--wx-aurora-1) calc(var(--wx-aurora-rim-glow,.5) * 100%), transparent))
    drop-shadow(0 0 11px color-mix(in srgb, var(--wx-aurora-2) calc(var(--wx-aurora-rim-glow,.5) * 70%), transparent))
    drop-shadow(0 6px 0 rgba(44,26,16,.22));
}

/* =========================================================
   AURORA — looking up
   ========================================================= */
/* Creatures stop and look up, and that stillness IS the acknowledgment — the
   one sky where the world reacts by doing less. Blink and glow stay: those are
   signs of life, not work. */
#game[data-weather="aurora"][data-wx-phase="transform"] .critter-svg,
#game[data-weather="aurora"][data-wx-phase="linger"] .critter-svg{
  animation:none;
}
#game[data-weather="aurora"][data-wx-phase="transform"] .cr-body,
#game[data-weather="aurora"][data-wx-phase="transform"] .cr-sprout,
#game[data-weather="aurora"][data-wx-phase="linger"] .cr-body,
#game[data-weather="aurora"][data-wx-phase="linger"] .cr-sprout{
  animation:none;
  transform:rotate(-4deg);
  transition:transform .7s ease;
}
#game[data-weather="aurora"][data-wx-phase="transform"] .cr-eyes,
#game[data-weather="aurora"][data-wx-phase="linger"] .cr-eyes{
  transform:translate(0,-3px);
  transition:transform .7s ease;
}

/* The flower gazes up on the same hook the game already aims its eyes with. */
#game[data-weather="aurora"] .tf-head{transition:transform .9s ease}
#game[data-weather="aurora"][data-wx-phase="transform"] .flower-btn,
#game[data-weather="aurora"][data-wx-phase="linger"] .flower-btn{--py:-3.4}
#game[data-weather="aurora"][data-wx-phase="transform"] .tf-head,
#game[data-weather="aurora"][data-wx-phase="linger"] .tf-head{
  transform:translate(60px,52px);
}

/* =========================================================
   WONDERFALL — the veil it was always owed
   ========================================================= */
/* The Wonder Effect's own veil and breathing saturation, run at weather
   strength: same treatment, quieter, so it reads as a sky rather than as the
   x3 event. The warp follows the veil by the same ratio (.62 is what the
   Wonder Effect runs), so one number moves the whole dress. */
#game[data-weather="wonderfall"]{--wx-warp:calc(var(--wx-wonderfall-veil,.3) / .62)}

.wx-veil{
  position:absolute;inset:0;pointer-events:none;opacity:0;
  background:linear-gradient(125deg,var(--wonder-sweep));
  background-size:400% 400%;
  mix-blend-mode:overlay;
  transition:opacity 2s ease;
  /* The Wonder Effect's veil is a twelve-second event and never needed this;
     a sky hangs for a whole slot, so it joins the season tint's mask or the
     iOS strip below the window stops matching the garden above it. */
  -webkit-mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
  mask-image:linear-gradient(180deg,#000 0 calc(100% - 44px),transparent 100%);
}
/* The shimmer is the Wonder Effect's own `wonderShift`, unchanged. The warp is
   NOT: `wonderWarp` is the x3 event's, at its own fixed strength, and one
   `@keyframes` name has one body — redefining it here would have re-dressed the
   Wonder Effect in a variable it never sets. */
@keyframes wxWonderWarp{
  0%,100%{filter:saturate(calc(1 + var(--wx-warp) * .5)) hue-rotate(0deg)}
  50%{filter:saturate(calc(1 + var(--wx-warp) * .9)) hue-rotate(calc(var(--wx-warp) * 22deg))}
}
#game[data-weather="wonderfall"][data-wx-phase="transform"] .wx-veil,
#game[data-weather="wonderfall"][data-wx-phase="linger"] .wx-veil{
  opacity:var(--wx-wonderfall-veil,.3);
  animation:wonderShift 5s linear infinite;
}
/* The breathing saturation, on everything ABOVE the lawn rather than on the
   whole scene. A `filter` cannot carry a mask, and the strip iOS paints below a
   short window is filled with the untinted lawn colour — so a warp that reached
   the bottom edge would draw the join three rounds of layout work went into
   hiding. The veil over the top is masked and covers the lawn anyway. */
#game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .sky,
#game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .celestial,
#game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .stars,
#game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .clouds-far,
#game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .clouds-near,
#game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .hills-far,
#game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .hills-mid,
#game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .hills-near{
  animation:wxWonderWarp 7s ease-in-out infinite;
}

/* =========================================================
   WONDERFALL — ripe plants bob in rhythm
   ========================================================= */
/* One period for every ripe plant and no per-plot delay: the point is that the
   board moves together, on the bar. The default is half of the music's 3.2s bar
   — two bobs per bar, which is the beat the Wonder Effect already bobs on. */
@keyframes wxRipeBob{
  0%,100%{
    transform:translateY(calc(var(--wx-wonderfall-bob,.5) * 2px))
      rotate(calc(var(--wx-wonderfall-bob,.5) * -1.4deg));
  }
  50%{
    transform:translateY(calc(var(--wx-wonderfall-bob,.5) * -5px))
      rotate(calc(var(--wx-wonderfall-bob,.5) * 1.4deg));
  }
}
@keyframes wxRipeSettle{
  from{transform:translateY(calc(var(--wx-wonderfall-bob,.5) * -3px))}
  to{transform:none}
}
#game[data-weather="wonderfall"][data-wx-phase="transform"] .plot[data-state="ready"] .plant,
#game[data-weather="wonderfall"][data-wx-phase="linger"] .plot[data-state="ready"] .plant{
  animation:wxRipeBob var(--wx-wonderfall-bob-period,1.6s) ease-in-out infinite;
  transform-origin:50% 92%;
}
#game[data-weather="wonderfall"][data-wx-phase="end"] .plot[data-state="ready"] .plant{
  animation:wxRipeSettle .9s ease-out;
  transform-origin:50% 92%;
}

/* =========================================================
   WONDERFALL — the singing flower
   ========================================================= */
/* A mouth on the melody and eyes wide open. The head keeps the bob's period so
   the flower is singing along with the garden rather than at it. */
@keyframes wxSing{
  0%,100%{transform:translate(0,0) scale(1,1)}
  25%{transform:translate(0,1px) scale(1.1,1.55)}
  50%{transform:translate(0,0) scale(.94,.8)}
  75%{transform:translate(0,1px) scale(1.22,1.8)}
}
@keyframes wxHum{
  0%,100%{transform:translate(60px,56px) rotate(-2.2deg)}
  50%{transform:translate(60px,53px) rotate(2.2deg)}
}
#game[data-weather="wonderfall"][data-wx-phase="transform"] .tf-mouth,
#game[data-weather="wonderfall"][data-wx-phase="linger"] .tf-mouth{
  transform-origin:0 12px;
  animation:wxSing var(--wx-wonderfall-bob-period,1.6s) ease-in-out infinite;
}
#game[data-weather="wonderfall"][data-wx-phase="transform"] .tf-head,
#game[data-weather="wonderfall"][data-wx-phase="linger"] .tf-head{
  animation:wxHum var(--wx-wonderfall-bob-period,1.6s) ease-in-out infinite;
}
#game[data-weather="wonderfall"][data-wx-phase="transform"] .tf-lid,
#game[data-weather="wonderfall"][data-wx-phase="linger"] .tf-lid{
  animation:none;transform:scaleY(0);
}
#game[data-weather="wonderfall"][data-wx-phase="transform"] .tf-cheek,
#game[data-weather="wonderfall"][data-wx-phase="linger"] .tf-cheek{
  animation:none;opacity:1;
}

/* =========================================================
   WONDERFALL — the takeover cue
   ========================================================= */
/* The banner and the forced line carry the announcement; this is only the light
   around them — the Wonder fanfare's gentler cousin, one bloom, then gone.
   `.wx-cue` goes on `#game` for the length of the bloom.

   The 44px stop measures against the ELEMENT, and this one is inset -20% — 1.4x
   the screen in both axes — so the canonical stop would land ~125px BELOW the
   bottom edge and mask nothing anyone can see. 85.71% is 100/1.4: the screen's
   own bottom edge, expressed in this element's box. */
.wx-takeover{
  position:absolute;inset:-20%;pointer-events:none;opacity:0;
  /* Plain alpha, not screen: a screened white bloom is invisible over a noon
     sky, and the cue has to land at any hour. */
  background:radial-gradient(circle at 50% 44%,
    rgba(255,255,255,.55) 0%,
    rgba(255,214,245,.4) 18%,
    rgba(140,224,255,.22) 34%,
    transparent 58%);
  -webkit-mask-image:linear-gradient(180deg,#000 0 calc(85.71% - 44px),transparent 92%);
  mask-image:linear-gradient(180deg,#000 0 calc(85.71% - 44px),transparent 92%);
}
/* The Wonder halo's ring, once, on its way out — the fanfare's gentler cousin
   in the palette the effect already owns. It carries the read; the bloom behind
   it only glows where the sky is dark enough to take one. */
.wx-takeover::after{
  content:'';position:absolute;left:50%;top:44%;
  width:62%;aspect-ratio:1;
  transform:translate(-50%,-50%);
  border-radius:50%;
  background:conic-gradient(var(--wonder-sweep),#ff6b6b);
  -webkit-mask:radial-gradient(farthest-side,
    transparent calc(100% - 30px), #000 calc(100% - 20px) calc(100% - 8px), transparent 100%);
  mask:radial-gradient(farthest-side,
    transparent calc(100% - 30px), #000 calc(100% - 20px) calc(100% - 8px), transparent 100%);
  filter:blur(5px);
  opacity:.82;
}
@keyframes wxTakeover{
  0%{opacity:0;transform:scale(.3)}
  20%{opacity:.9}
  100%{opacity:0;transform:scale(1.9)}
}
#game.wx-cue .wx-takeover{
  animation:wxTakeover 1.15s cubic-bezier(.22,.9,.3,1);
}

/* =========================================================
   THE SKY PASS — THE QUIET VERSIONS
   =========================================================
   Last in this section on purpose. A media query adds no specificity, so a
   cancel written beside the rule it cancels loses the cascade and does nothing —
   the failure the meadow's clouds shipped with. Every rule below mirrors exactly
   the selector it cancels and follows it.

   The global clamp near line 2288 gets here first with
   `animation-duration:.001ms !important` and `transition-duration:.08s
   !important`, so a quiet version whose whole substance is a duration has to say
   `!important` to survive it. Two do, and both are marked.
   ========================================================= */
@media (prefers-reduced-motion:reduce){

  /* The front still arrives — it just stops drifting. */
  #game .wx-front-cloud{animation-name:none}
  #game .wx-front{transition-property:opacity;transform:translateX(0)}
  #game:is([data-weather="rain"],[data-weather="storm"]) .wx-front{transform:translateX(0)}
  #game:is([data-weather="rain"],[data-weather="storm"])[data-wx-phase="end"] .wx-front{transform:translateX(0)}

  /* Static wet ground: it arrives without a long ramp, and it still dries over
     the linger, because the drying IS the trace. Both durations out-shout the
     global clamp, which would otherwise snap the trace away in 80ms. */
  #game .wx-ground,
  #game .wx-wet{transition-duration:1.2s !important}
  #game:is([data-weather="rain"],[data-weather="storm"])[data-wx-phase="end"] .wx-ground,
  #game:is([data-weather="rain"],[data-weather="storm"])[data-wx-phase="end"] .wx-wet{
    transition-duration:var(--wx-rain-linger, 30s) !important;
  }

  /* The rim stays — it is what says "wet" — the breathing stops. */
  #game:is([data-weather="rain"],[data-weather="storm"]):is([data-wx-phase="transform"],[data-wx-phase="linger"]) .wx-glint{
    animation-name:none;
  }
  #game .wx-splashed{animation-name:none}

  /* The pose holds, the sway does not. */
  #game:is([data-weather="rain"],[data-weather="storm"]):is([data-wx-phase="transform"],[data-wx-phase="linger"]) .tf-leaf-r{
    animation-name:none;
  }
  #game[data-weather="storm"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .wx-lean,
  #game[data-weather="storm"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .wx-shelter{
    animation-name:none;
  }

  /* No strobe: one brief, soft tint pulse says "lightning" and stops there. It
     is a real pulse rather than a dead channel — the clamp would run it once for
     .001ms and leave a player with the preference on no storm signal at all,
     which is the regression the Turn button's ring already made once. */
  #game[data-wx-flash="1"] .wx-flash-under{animation-name:none;opacity:0}
  #game[data-weather="storm"][data-wx-flash="1"] .wx-bolt{animation-name:none;opacity:0}
  #game .wx-flash{background:var(--wxr-pulse)}
  #game[data-wx-flash="1"] .wx-flash{
    animation:wxFlashPulse 700ms ease-out;
    animation-duration:700ms !important;
  }
  @keyframes wxFlashPulse{
    0%{opacity:0}
    22%{opacity:min(.24, calc(var(--wx-storm-flash-bright,.72) * .3))}
    100%{opacity:0}
  }

  /* One static, faint ray. */
  #game[data-sunbreak="1"] .wx-sunbreak{animation-name:none}
  /* The base transform starts a ray off the left edge — that is where its journey
     begins — so the quiet version has to place it somewhere worth looking at
     rather than inherit a position that means "not yet". */
  #game .wx-ray{
    animation-name:none,none;
    transform:rotate(var(--a,17deg)) translateX(calc(var(--vw,1vw) * 26));
  }
  #game .wx-ray:nth-child(1){opacity:calc(var(--wx-sunbreak-opacity,.28) * .6)}
  #game .wx-ray:nth-child(n+2){opacity:0}

  /* The dusk still arrives; it just arrives. */
  #game .wx-dusk,
  #game[data-weather="aurora"][data-wx-night="1"] .stars{transition:none}

  /* One dim ribbon, holding still. */
  #game[data-weather="aurora"] .wx-ribbon{
    animation:none;
    transform:skewY(-2deg);
  }
  #game[data-weather="aurora"][data-wx-phase="front"] .wx-ribbon,
  #game[data-weather="aurora"][data-wx-phase="transform"] .wx-ribbon,
  #game[data-weather="aurora"][data-wx-phase="linger"] .wx-ribbon{
    opacity:0;
  }
  #game[data-weather="aurora"][data-wx-phase="front"] .wx-ribbon:first-child,
  #game[data-weather="aurora"][data-wx-phase="transform"] .wx-ribbon:first-child,
  #game[data-weather="aurora"][data-wx-phase="linger"] .wx-ribbon:first-child{
    opacity:calc(var(--wx-aurora-ribbon-opacity,.42) * .62);
  }
  /* It fades where it stands rather than sliding off the edge. */
  #game[data-weather="aurora"][data-wx-phase="end"] .wx-ribbon{
    translate:0 0;
    transition:opacity 1.2s ease;
  }

  /* Nobody was moving anyway — but the pose stops being a move. */
  #game[data-weather="aurora"][data-wx-phase="transform"] .cr-body,
  #game[data-weather="aurora"][data-wx-phase="transform"] .cr-sprout,
  #game[data-weather="aurora"][data-wx-phase="transform"] .cr-eyes,
  #game[data-weather="aurora"][data-wx-phase="linger"] .cr-body,
  #game[data-weather="aurora"][data-wx-phase="linger"] .cr-sprout,
  #game[data-weather="aurora"][data-wx-phase="linger"] .cr-eyes,
  #game[data-weather="aurora"] .tf-head{transition:none}

  /* The veil hangs there, one colour, no shimmer and no breathing. */
  #game[data-weather="wonderfall"][data-wx-phase="transform"] .wx-veil,
  #game[data-weather="wonderfall"][data-wx-phase="linger"] .wx-veil{
    animation:none;
    background-position:38% 50%;
  }
  /* Held, not breathing — and on the same layers the moving version uses, never
     on `.scenery`, whose box owns the bottom edge and whose filter cannot be
     masked away from it. */
  #game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .sky,
  #game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .celestial,
  #game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .stars,
  #game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .clouds-far,
  #game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .clouds-near,
  #game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .hills-far,
  #game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .hills-mid,
  #game[data-weather="wonderfall"]:is([data-wx-phase="transform"],[data-wx-phase="linger"]) .hills-near{
    animation:none;
    filter:saturate(calc(1 + var(--wx-warp) * .5));
  }

  /* No bobbing, no singing mouth — the flower keeps the wide-open eyes, because
     a held pose is not motion. */
  #game[data-weather="wonderfall"][data-wx-phase="transform"] .plot[data-state="ready"] .plant,
  #game[data-weather="wonderfall"][data-wx-phase="linger"] .plot[data-state="ready"] .plant,
  #game[data-weather="wonderfall"][data-wx-phase="end"] .plot[data-state="ready"] .plant{
    animation:none;
  }
  #game[data-weather="wonderfall"][data-wx-phase="transform"] .tf-head,
  #game[data-weather="wonderfall"][data-wx-phase="linger"] .tf-head{
    animation:none;
    transform:translate(60px,54px);
  }
  #game[data-weather="wonderfall"][data-wx-phase="transform"] .tf-mouth,
  #game[data-weather="wonderfall"][data-wx-phase="linger"] .tf-mouth{
    animation:none;
    transform:translate(0,1px) scale(1.1,1.4);
  }

  /* The cue becomes a light coming up and going down again. */
  #game.wx-cue .wx-takeover{
    animation:none;
    opacity:.3;
  }
  /* The global clamp above is `transition-duration:.08s !important` on every
     element, and an `!important` declaration beats a plain one whatever the
     specificity — so the cue's fade has to be important too or it becomes a
     step, which is the flash this block exists to replace. */
  #game .wx-takeover{
    transition:opacity .5s ease;
    transition-duration:.5s !important;
  }
}

/* ---------- REDUCED MOTION: the ready Turn button ----------
   Last in the file on purpose. A media query adds no specificity, so a cancel
   written beside the rule it cancels loses the cascade — and the global clamp
   near line 2232 has to be above this too. What it repairs is not only the new
   glint: the breath is an animation, the clamp runs it once for .001ms and
   drops it, and the attention dot is hidden on the assumption that the button
   is breathing. A player with the preference on has therefore had NO ready
   signal on the Turn button at all. Hold the ring on solid instead of pulsing
   it, and turn the travelling glint into a still gold wash across the face:
   the button is highlighted, it just does not move. */
@media (prefers-reduced-motion:reduce){
  .dock-btn.turn.ready{
    animation:none;
    box-shadow:0 5px 0 var(--ink-2), 0 9px 14px rgba(44,26,16,.22), 0 0 0 5px var(--coin);
  }
  .dock-btn.turn.ready .turn-fill::after{
    animation:none;
    top:0;bottom:0;left:0;width:auto;right:0;transform:none;
    background:linear-gradient(180deg,rgba(255,201,60,.3),rgba(255,201,60,.14));
  }
}
