:root {
  color-scheme: dark;
  font-family: ui-rounded, "Avenir Next", "Trebuchet MS", system-ui, sans-serif;
  --ink: #fdf8ed;
  --night: #101c2c;
  --panel: #182a41;
  --pink: #ff72a5;
  --pink-dark: #bd356d;
  --gold: #ffd56a;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
}

* { box-sizing: border-box; }
html, body { width: 100%; min-height: 100%; margin: 0; background: var(--night); }
body { min-height: 100dvh; color: var(--ink); overflow: hidden; }
button, select, .button-link { font: inherit; }
button, .button-link { min-width: 44px; min-height: 44px; border-radius: 999px; cursor: pointer; }
button:focus-visible, a:focus-visible, select:focus-visible { outline: 4px solid #fff; outline-offset: 3px; }
button:disabled { cursor: default; opacity: .45; }

.app-shell { display: grid; grid-template-rows: auto 1fr; min-height: 100dvh; }
.topbar {
  z-index: 5;
  display: grid;
  grid-template-columns: minmax(160px, 1fr) auto minmax(160px, 1fr);
  align-items: center;
  gap: 12px;
  min-height: calc(62px + var(--safe-top));
  padding: calc(8px + var(--safe-top)) calc(14px + var(--safe-right)) 8px calc(14px + var(--safe-left));
  background: #14243a;
  box-shadow: 0 2px 15px #0008;
}
.brand-button { justify-self: start; border: 0; background: transparent; color: var(--ink); padding: 6px 12px; font-weight: 900; font-size: clamp(16px, 2.2vw, 23px); }
.brand-button span { font-size: 1.35em; }
.hunt-stats { display: flex; gap: 18px; align-items: baseline; white-space: nowrap; }
/* Wide enough for `100.0s`, so the bar does not shuffle at the 9.9 → 10.0 step. */
#timer { min-width: 5.6ch; color: var(--gold); font-variant-numeric: tabular-nums; font-size: 1.5rem; font-weight: 900; }
.top-actions { justify-self: end; display: flex; gap: 6px; }
/* THESE ARE CONTROLS, AND THEY SHOULD LOOK LIKE THE CONTROLS THIS GAME ALREADY HAS.
   Sound, Help and Pause were bare words on the bar — three labels that read as a caption until you
   hovered one. They take Hungry Hungry Snake's treatment (its `.icon-button`: a hairline border, a
   translucent fill, 8px/13px, .8rem, 800) re-derived for this game's dark band, and the border alpha
   is the one `.scene-controls button` below already wears, so the top row and the bottom row are one
   control language rather than two. The wordmark opposite keeps no border: it is a name, not a
   button. */
.icon-button { border: 1px solid #ffffff33; background: #ffffff14; color: var(--ink); padding: 8px 13px; font-size: .8rem; font-weight: 800; }

.game-area { min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr) auto; background: #0a121e; }
.scene-viewport {
  position: relative;
  min-height: 0;
  overflow: hidden;
  background: #070b12;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: crosshair;
}
.scene-viewport.concealed::after { content: ""; position: absolute; inset: 0; z-index: 20; background: #0b1523; }
.scene-transform { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; }
.scene-transform img, .target-overlay { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.scene-transform img { pointer-events: none; -webkit-user-drag: none; }
.target-overlay { overflow: visible; pointer-events: none; }
.found-region { fill: #ff7bac80; stroke: #fff; stroke-width: 10; vector-effect: non-scaling-stroke; filter: drop-shadow(0 0 12px #ff72a5); transform-box: fill-box; transform-origin: center; animation: foundPop .75s cubic-bezier(.2, 1.8, .35, 1) both; }
/* The hint region. Sized at 1.6x the AREA it used to have (x1.2649 on each axis, aspect ratio
   unchanged) so the help it gives is a neighbourhood rather than a pin. */
.broad-hint { position: absolute; z-index: 12; width: min(35.4vw, 316px); aspect-ratio: 1.4; translate: -50% -50%; border: 5px solid var(--gold); border-radius: 50%; box-shadow: 0 0 0 10px #ffd56a44, 0 0 35px #ffd56a; pointer-events: none; }
.reticle { position: absolute; z-index: 12; width: 38px; height: 38px; translate: -50% -50%; border: 4px solid #fff; border-radius: 50%; box-shadow: 0 0 0 3px #14243a, 0 0 18px #fff; pointer-events: none; }
.reticle::before, .reticle::after { content: ""; position: absolute; background: #fff; }
.reticle::before { width: 54px; height: 3px; left: -12px; top: 14px; }
.reticle::after { height: 54px; width: 3px; top: -12px; left: 14px; }
.effects { position: absolute; inset: 0; z-index: 12; pointer-events: none; overflow: hidden; }
.ripple { position: absolute; translate: -50% -50%; color: #fff; font-weight: 900; text-shadow: 0 2px 5px #000; animation: ripple .65s ease-out both; }
/* The burst at a found bird. It used to be eight spans carrying the literal characters ◆ and ● at
   25px — a font's opinion of a diamond, in a game whose every other mark is drawn. A mote is a soft
   petal instead: `currentColor` so one rule serves all three palette colours, and everything that
   varies per mote (size, throw, spin, delay) arrives as a custom property main.js sets. */
.mote { position: absolute; width: var(--size, 9px); height: var(--size, 9px); translate: -50% -50%; border-radius: 62% 38% 55% 45% / 55% 62% 38% 45%; background: currentColor; box-shadow: 0 0 7px currentColor; animation: mote .9s cubic-bezier(.16, .72, .3, 1) both; }
/* The clear, blooming OUT OF THE BIRD. The flat pink border that used to fade over the whole
   viewport said "something happened" without saying where, on the one screen whose entire subject is
   a location. main.js stands this on the same focal point as the burst and the camera lean. */
.completion-bloom { position: absolute; width: 46px; height: 46px; translate: -50% -50%; border-radius: 50%; background: radial-gradient(circle, #ffffffd9 0%, #ff72a5b0 42%, #ff72a500 70%); animation: bloom .8s ease-out both; }
/* The held beat between the last find and the results card: the scene darkens from its edges while
   the camera settles back, so the card arrives as a resolve rather than as a cut. Under the effects
   layer (z-index 12) and over the picture, and gone the moment the opaque `concealed` cover lands. */
.scene-viewport.settling::before { content: ""; position: absolute; inset: 0; z-index: 10; pointer-events: none; background: radial-gradient(ellipse at center, #0b152300 34%, #0b1523d9 100%); animation: settle .6s ease-in both; }

.scene-controls {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 8px;
  padding: 8px max(12px, var(--safe-right)) calc(8px + var(--safe-bottom)) max(12px, var(--safe-left));
  background: #14243a;
}
.scene-controls button { border: 1px solid #ffffff33; background: #243a55; color: var(--ink); padding: 6px 16px; font-weight: 800; }
.scene-controls button:first-child, .scene-controls button:nth-child(3) { width: 48px; padding: 0; font-size: 1.5rem; }
#zoom-label { width: 4.5ch; text-align: center; font-variant-numeric: tabular-nums; }
.scene-controls .hint-button { margin-left: min(8vw, 70px); background: var(--pink-dark); }
.hint-button.attention { animation: hintPulse 1.2s ease-in-out infinite; }

/* `minmax(0, 1fr)` and not the implicit `auto`: an auto column is sized to the largest min-content
   in it and is free to grow PAST its own container, which is how the board card came out 418px wide
   on a 390px phone and scrolled sideways. The 0 is the whole point — a `min-width: 0` on the card
   only bites once the track is definite. */
.overlay { position: fixed; z-index: 50; inset: 0; display: grid; grid-template-columns: minmax(0, 1fr); place-items: center; padding: calc(16px + var(--safe-top)) calc(16px + var(--safe-right)) calc(16px + var(--safe-bottom)) calc(16px + var(--safe-left)); overflow: auto; background: #0d1725; }
.hidden { display: none !important; }
.card { width: min(100%, 580px); padding: clamp(22px, 4vw, 44px); border: 1px solid #ffffff2b; border-radius: 26px; text-align: center; background: linear-gradient(155deg, #203a58, #142338); box-shadow: 0 28px 80px #0009; }
.wide-card { width: min(100%, 980px); }
.card h1 { margin: 5px 0 12px; font-size: clamp(2rem, 6vw, 4.4rem); line-height: .95; }
/* The `9vh` term is the whole reason this is not a breakpoint: on a short viewport the headline
   shrinks with the room it has, continuously, instead of stepping at one authored height and being
   wrong on either side of it. */
.compact-card h1 { font-size: clamp(1.7rem, min(6vw, 9vh), 3.4rem); }
/* How to play, and only How to play. It is the one use of this cover that carries two paragraphs and
   a demo, so it is the one that is given room to lay them out side by side; a paused player still
   gets the narrow card. */
.help-card { width: min(100%, 820px); }
/* The copy is a <div> because How to play passes two paragraphs and every other kind passes one.
   main.js builds the <p>s, so the margins have to live here rather than on `.card p`. */
#interrupt-copy { display: grid; gap: 0 26px; }
#interrupt-copy p { margin: 9px 0 0; }
/* The interruption card's foot: the way out, and whatever ever joins it. It is a wrapping row rather
   than a stack because the Motion preference used to stand above the buttons and cost sixty-odd
   pixels on the card with the least to spare — the preference has since gone entirely, and the row
   is what its replacement would land in. */
.card-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 10px 20px; margin-top: 20px; }
.card-foot .card-actions { margin-top: 0; }
.eyebrow { margin: 0; text-transform: uppercase; letter-spacing: .13em; color: var(--pink); font-weight: 900; }
.small-copy { opacity: .8; font-size: .92rem; }
/* The replay warning on the ready card. It has to out-read the clock line beside it, which is the
   same size and the same muted grey — a rule about ranking that looks like boilerplate gets skipped,
   and then the results card is where the player learns it. The lifted pink rather than `--pink`:
   measured against this card's lighter gradient stop (#203a58), `--pink` is 4.54:1 and #ffa9c8 is
   6.51:1, and a line whose whole job is to be read should not sit on the AA floor. */
.ready-replay { opacity: 1; color: #ffa9c8; }
.card-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 24px; }
/* SHAPE and COLOUR belong to the same class. They used to be split — `.card-actions button` carried
   the padding, the removed border and the weight, while `.primary` carried only the fill — so a
   `.primary` button anywhere else inherited the pink and none of the shape, and rendered as a 44px
   circle with the UA border and its own word overflowing it. That is what the board's Save button
   was. A rule you have to be standing in the right container to get is not a button style. */
.primary, .secondary, .card-actions button { border: 0; padding: 11px 19px; font-weight: 900; display: inline-grid; place-items: center; text-decoration: none; }
.primary { background: var(--pink); color: #261225; }
.secondary { background: #eef4f7; color: #17263a; }
/* The Details link at the end of the title blurb. It opens a modal rather than going anywhere, so
   it is a button wearing a link — and it carries its own shape, because the global `button` rule
   would otherwise round it into a 999px pill mid-sentence. The 44px minimum is kept rather than
   traded away for a tighter line: it is the last line of the paragraph, so the extra height costs
   nothing but the tap does not miss. */
.link-button { display: inline-block; min-width: 0; padding: 10px 8px; border: 0; border-radius: 9px; background: transparent; color: #ffd9e7; font-weight: 900; line-height: 1; vertical-align: middle; text-decoration: underline; text-underline-offset: 4px; }
.title-blurb { margin: 12px 0 0; }
/* The one sentence in the blurb that decides how a run is ranked, so it has to out-read the prose
   around it. It takes the SAME lifted pink this card's eyebrow already carries rather than a new
   one — `--pink` measures 3.34:1 through the glass and #ffa9c8 measures 4.79:1 at the worst
   viewport `results-veil-check.mjs` tested, and a second accent here would be a second colour to
   re-measure every time the veil moves. */
.title-rule { color: #ffa9c8; }
.level-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 26px; text-align: left; }
/* The tiles live only on the glass title card, so they carry a little alpha of their own: fully
   opaque plates on a translucent card read as holes punched through it.

   A tile names its scene and, once there is one, the time to beat — no difficulty word and no
   target count, because every scene holds exactly one flamingo and the tiers in the manifest are
   content bookkeeping rather than a promise to the player.

   A cleared tile is stamped with the thumbs-up flamingo. It stands in the tile's own right-hand
   margin, positioned rather than in flow, so a cleared tile is exactly as tall as an uncleared one
   and the grid stays even; `padding-right` on the same class is what keeps a long scene name from
   running under the bird. The image's alt text is what says "Cleared" to a screen reader. */
.level-card { position: relative; min-height: 76px; border: 1px solid #ffffff33; border-radius: 18px; padding: 16px; background: #0f1e31d6; color: var(--ink); text-align: left; }
.level-card strong, .level-card span { display: block; }
.level-card span { margin-top: 7px; color: #ffd9e7; font-size: .85rem; }
.level-card.completed { padding-right: 68px; }
.level-stamp { position: absolute; right: 9px; bottom: 5px; width: auto; height: min(calc(100% - 10px), 68px); pointer-events: none; }

/* ===== Scenic overlays =====
   Two screens are dressed in artwork rather than the flat shell colour: the title screen wears the
   flamingo flock, and the results card wears the scene the player just searched. Same treatment,
   one definition, and the picture arrives through `--scenic-art` — a literal `url()` in the
   stylesheet for the title screen, and the level's own verified blob URL, set by `main.js`, for the
   results card.

   Four layers, painted in this order:

     1. `.overlay`'s own `#0d1725` fill, deliberately left in place as the failure state: a slow,
        blocked, or missing image leaves a finished dark screen rather than a bare page. That is
        also why `--scenic-art` defaults to `none` — an unset custom property would make the whole
        `background` shorthand invalid at computed-value time.
     2. `::before`, the picture. `position: fixed` for two reasons — an absolutely positioned pseudo
        of a grid container is inset by that container's padding, which here is the safe-area
        padding, and a short viewport scrolls the card while the artwork should stay put. Being out
        of flow also keeps both pseudo-elements from becoming grid items and shoving the card off
        centre. It is scaled past the viewport because a blurred layer fades to transparent at its
        own edges; at 10px the fade runs about 30px, and 1.09 pushes that off every side.
     3. `::after`, the veil, so the bright unmasked gutters do not compete with the card, with a
        vignette that pulls the eye to the middle. One veil serves both screens: the title screen's
        was tuned against a single picture, and `results-veil-check.mjs` then measured it against
        all fifty scenes, where the worst text contrast any of them produces is 4.73:1.
     4. The card itself, translucent over the blur.

   The pseudo-elements are `z-index: 0` and the card `z-index: 1` because paint order alone gets
   this wrong twice: an unpositioned card paints UNDER both fixed pseudos, and a merely `relative`
   one still paints under `::after`, which is its later sibling. Overlays without `.scenic` keep
   the flat shell. */
.scenic { --scenic-art: none; }
.scenic::before, .scenic::after { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; }
.scenic::before {
  background: #0d1725 var(--scenic-art) center / cover no-repeat;
  filter: blur(10px) saturate(1.18) brightness(1.14);
  transform: scale(1.09);
}
.scenic::after { background: radial-gradient(120% 85% at 50% 42%, #0a142229, #0a142252 62%, #0a142294); }
#level-overlay { --scenic-art: url("./assets/ui/flamingo-flock-background.11ae25721181.webp"); }
.glass-card {
  position: relative;
  z-index: 1;
  border-color: #ffffff42;
  background: linear-gradient(155deg, #203a58d6, #142338e0);
  backdrop-filter: blur(6px) saturate(1.25);
  -webkit-backdrop-filter: blur(6px) saturate(1.25);
  box-shadow: 0 28px 80px #000000b8, inset 0 1px 0 #ffffff26;
}
/* Glass costs contrast, and the eyebrow is the one line that could not afford it: `--pink` measured
   4.54:1 on the opaque card — barely AA — and 3.34:1 once the artwork shows through. Lifting it here
   only restores what the translucency spent. */
.glass-card .eyebrow { color: #ffa9c8; }
.spinner { width: 48px; height: 48px; margin: 0 auto 20px; border: 6px solid #ffffff33; border-top-color: var(--pink); border-radius: 50%; animation: spin .8s linear infinite; }
/* ===== How to play: the tap demo =====
   A still of a real scene with a real flamingo standing in it, and a finger that finds it on a
   loop. The still is a crop of a RETIRED playtest level — pointing at a shipped scene's bird would
   publish that scene's answer on the screen a first-time player opens.

   `--demo-x` / `--demo-y` are the bird's own centre inside that crop, and `--demo-tap-y` is where
   the fingertip lands — near the bird's feet rather than on its middle, because the hand hangs down
   and to the right of its tip and would otherwise bury the bird at the exact moment the demo is
   asking the player to look at it. All three are MEASURED out of the picture by
   `working-material/hidden-flamingo/help-demo.mjs`, copied verbatim from `help-demo/provenance.json`,
   and pinned to it by the runtime suite — so re-cutting the crop moves the finger with the bird
   instead of leaving it pointing at scenery.

   The resting state below is the base style, and the loop lives entirely in the two animations with
   no `fill-mode`. That is what makes reduced motion correct for free: the shell's global rule cuts
   every animation to one .001ms iteration, the element falls straight back to these declarations,
   and what is left is the finger on the flamingo with the find marked — a still frame that teaches
   the same thing the loop does. */
.help-demo { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 16px; margin: 16px 0 0; --demo-x: 60.95%; --demo-y: 49.9%; --demo-tap-y: 56.29%; }
/* SIZED OFF THE VIEWPORT, not off the card's width, and that is the fix for the whole card. A 16:9
   still filling a 580px card is 277px tall — most of a laptop's spare height before the title, the
   copy and the actions have had any, and the reason this card measured 857px against a 713px
   viewport. A height in `vh` cannot do that at any width, and needs no breakpoint to say so.

   The WIDTH is what the constraint is written on, though, and that is not cosmetic: a height alone
   let the aspect ratio derive a 391px frame on a 390px phone, and a `flex: none` item cannot shrink,
   so the card grew past the screen and How to play scrolled sideways. Expressed as a width, both
   limits are in the same value — the height the card can spare, converted through the ratio, and
   the width the screen actually has — and `aspect-ratio` derives the height from whichever wins. */
.help-demo-frame { position: relative; flex: none; width: min(100%, calc(min(30vh, 220px) * 760 / 428)); overflow: hidden; aspect-ratio: 760 / 428; border: 1px solid #ffffff33; border-radius: 16px; background: #070b12; }
.help-demo-shot { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The find, marked the way the scene marks one: white ring, pink wash, pink glow. The wash is
   lighter than the scene's own `.found-region` on purpose — that one lands on whatever colour the
   bird was hiding against, while this one lands on a coral bird, and pink over pink is the one case
   where marking the find hides it. */
.help-demo-mark {
  position: absolute;
  left: var(--demo-x);
  top: var(--demo-y);
  width: 13%;
  aspect-ratio: 1;
  translate: -50% -50%;
  border: 3px solid #fff;
  border-radius: 50%;
  background: #ff7bac33;
  box-shadow: 0 0 16px #ff72a5;
  animation: demoMark 4.2s ease-out infinite;
}
/* The fingertip sits at 42% / 9% of the glyph's own box, so that is both the offset that lands it
   on the bird and the origin the press scales about — a press that scaled about the centre would
   slide the fingertip off the target. */
.help-demo-hand {
  position: absolute;
  left: var(--demo-x);
  top: var(--demo-tap-y);
  width: 10%;
  height: auto;
  translate: -42% -9%;
  transform-origin: 42% 9%;
  filter: drop-shadow(0 7px 11px #000000a6);
  animation: demoHand 4.2s ease-in-out infinite;
}
.help-demo-caption { margin: 0; max-width: 22ch; opacity: .8; font-size: .9rem; font-weight: 700; }

.results-card { width: min(100%, 720px); padding: 26px 34px; }
.results-card h1 { font-size: clamp(1.8rem, 4.5vw, 2.75rem); }
.results-card .card-actions { margin-top: 18px; }

/* The level progress strip: where this scene sits in the set the player actually has. Segment
   count comes from the manifest, never from a constant — scenes are still being promoted. */
.level-progress { display: flex; align-items: center; gap: 14px; margin: 0 0 16px; }
.progress-label { text-transform: uppercase; letter-spacing: .12em; font-size: .72rem; font-weight: 900; opacity: .75; white-space: nowrap; }
.progress-remaining { color: var(--gold); opacity: 1; }
.progress-track { display: flex; gap: 5px; flex: 1; }
.progress-track.dense { gap: 3px; }
.progress-track.packed { gap: 2px; }
.progress-segment { flex: 1; height: 7px; border-radius: 999px; background: #ffffff26; }
.progress-segment.cleared { background: var(--pink); }
/* A minimum width, not just a bigger flex share: at fifty scenes 1.7 of nothing is still nothing,
   and the one segment a player looks for is the one they are standing on. */
.progress-segment.current { flex: 1.7; min-width: 9px; background: var(--gold); box-shadow: 0 0 0 3px #ffd56a2e; }

/* The flamingo holds up the scorecard: one bird, two speech bubbles. */
.result-celebration { display: grid; grid-template-columns: minmax(150px, 190px) minmax(0, 1fr); align-items: start; gap: 4px; margin: 0; }
.result-flamingo { z-index: 1; display: block; width: 100%; height: 300px; object-fit: cover; object-position: 50% 4%; filter: drop-shadow(0 14px 14px #050a1299); animation: celebrationEntrance .65s cubic-bezier(.2, 1.35, .35, 1) both, celebrationBob 2.8s ease-in-out .65s infinite; }
.result-bubbles { display: flex; flex-direction: column; gap: 10px; }
.result-bubble { position: relative; border: 3px solid #17263a; border-radius: 22px; background: #fff7df; color: #17263a; text-align: left; box-shadow: 0 10px 0 #0a1423aa; }
/* Only the top bubble gets the tail: two beaks would be two flamingos. */
.result-score { margin: 10px 0 0 -8px; padding: 16px 20px 14px; }
.result-score::before { content: ""; position: absolute; left: -28px; top: 18px; width: 30px; height: 28px; background: #fff7df; clip-path: polygon(100% 0, 100% 100%, 0 50%); filter: drop-shadow(-3px 2px 0 #17263a); }
.result-score-line { display: flex; align-items: baseline; gap: 9px; margin: 0; }
.result-adjusted { color: var(--pink-dark); font-size: clamp(2rem, 5vw, 2.6rem); font-weight: 900; line-height: 1; font-variant-numeric: tabular-nums; }
.result-breakdown { margin: 4px 0 0; opacity: .75; font-size: .84rem; font-weight: 700; }
.result-board-note { margin: 7px 0 0; color: var(--pink-dark); font-size: .8rem; font-weight: 900; }
.result-fact { margin: 0 0 0 6px; padding: 14px 20px; }
.fact-eyebrow { color: var(--pink-dark); font-size: .74rem; }
.fact-text { margin: 6px 0 0; font-size: .95rem; line-height: 1.45; }

/* The scene leaderboard. */
.board-card { width: min(100%, 720px); padding: 26px 34px 24px; text-align: left; }
.board-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.board-heading .eyebrow { font-size: .8rem; }
.board-card h1 { margin: 4px 0 0; font-size: clamp(1.5rem, 4vw, 2.15rem); line-height: 1; }
/* One control, right-aligned beside the title. It was a two-button pill group while "this scene"
   could only ever mean the scene just played; the board is now reachable from the title screen too,
   where there is no such scene, and fifty of them do not fit in a pill group anyway. There is still
   exactly one WINDOW and no toggle for it — all-time and weekly are a PORTAL question about zaps
   across the catalogue, not a question a game's own board asks. */
.board-toggles { display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.board-scene-label { text-transform: uppercase; letter-spacing: .11em; font-size: .66rem; font-weight: 900; opacity: .55; }
/* The 44px floor is the game's own accessibility contract in `_doc.md`; `max-width` is what keeps a
   long scene name from pushing the title onto its own line. */
/* The chevron is DRAWN rather than left to the UA. A native select paints its arrow hard against
   the right border edge, which inside a 999px radius sits in the pill's own curve and reads as an
   arrow falling off the control. `appearance: none` and an inline-data chevron put it on the same
   16px inset as the label on the other side; the data URI keeps this game fetching nothing. */
.board-scene { appearance: none; -webkit-appearance: none; min-height: 44px; max-width: min(58vw, 280px); padding: 10px 34px 10px 16px; border: 1px solid #ffffff26; border-radius: 999px; background: #0e1c2d; color: var(--ink); font: inherit; font-size: .8rem; font-weight: 900;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f4f8fb' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 16px center; background-size: 12px 12px; }
.board-callout { margin: 16px 0 14px; padding: 12px 16px; border-radius: 18px; border: 1px solid #ffd56a3d; background: #ffd56a14; font-size: .88rem; }
.board-callout strong { color: var(--gold); }
/* Four columns, and the tag is one of them. It used to hang inside the player cell, which the
   catalogue's shared row renderer cannot express: a row is a flat list of cells so that a board is
   the same THING in every ZapJam game. `:empty` collapses the column — a `display: none` grid item
   takes its gap with it — so a scene board with no tag looks exactly as it did. */
.board-columns { display: grid; grid-template-columns: 46px minmax(0, 1fr) auto auto; gap: 0 14px; padding: 0 16px 8px; text-transform: uppercase; letter-spacing: .11em; font-size: .66rem; font-weight: 900; opacity: .55; }
.board-columns span:last-child { text-align: right; }
/* An `<ol>`: the rows ARE a ranking. Flex children get no marker, but the list padding is the UA's
   and would indent every row by 40px. */
.board-rows { display: flex; flex-direction: column; gap: 6px; min-height: 96px; margin: 0; padding: 0; list-style: none; }
.board-row { display: grid; grid-template-columns: 46px minmax(0, 1fr) auto auto; gap: 0 14px; align-items: center; padding: 12px 16px; border-radius: 16px; background: #0e1c2d; }
.board-rank, .board-time { font-size: 1.1rem; font-weight: 900; font-variant-numeric: tabular-nums; }
.board-rank { opacity: .6; }
/* A column rather than a line of text, because the time now carries the zap conversion under it. */
.board-time { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; text-align: right; }
.board-row.podium .board-rank, .board-row.podium .board-time { color: var(--gold); opacity: 1; }
.board-name { font-size: 1rem; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* `.viewer` is the shared class every ZapJam board marks its reader's row with, and the Worker is
   what sets it — a client comparing ids would be a second answer to the same question. */
.board-row.viewer { background: var(--pink); color: #261225; box-shadow: 0 8px 0 #0a1423aa; }
.board-row.viewer .board-rank, .board-row.viewer .board-name, .board-row.viewer .board-time, .board-row.viewer .board-tag { color: #261225; opacity: 1; font-weight: 900; }
.board-tag { text-transform: uppercase; letter-spacing: .1em; font-size: .66rem; font-weight: 900; opacity: .7; }
.board-tag:empty { display: none; }
/* What the time is worth in ZapJam points. The coin is the ZapJam mark drawn as a MASK — the mark
   wears the citrus gradient and an `<img>` cannot be recoloured, an external `<use>` cannot reach
   this document's gradient, and a script is a thing that can fail mid-render. It is inline data, so
   nothing is fetched: this game loads nothing from anywhere but its own origin. */
.board-zaps { display: inline-flex; align-items: center; gap: 4px; font-size: .68rem; font-weight: 900; font-variant-numeric: tabular-nums; opacity: .82; }
/* `inline-block` is what lets the coin stand in the board note's PROSE as well as in a row. Inside
   `.board-zaps` it is a flex item and blockified either way; on its own in a sentence a bare inline
   span would ignore both dimensions and paint nothing at all. */
.board-zap-coin { display: inline-block; vertical-align: -.15em; width: .95em; height: .95em; flex: none; background: linear-gradient(135deg, #e0f050, #f4d344 42%, #f88b50);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath d='M 515.4 142.5 A 264 264 0 1 1 440.9 85.3' fill='none' stroke='%23000' stroke-width='47' stroke-linecap='round'/%3E%3Cpath d='M -84 -120 L 156 -120 L -12 48 L 156 48 L 84 120 L -156 120 L 12 -48 L -156 -48 Z' fill='%23000' transform='translate(320 310)'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath d='M 515.4 142.5 A 264 264 0 1 1 440.9 85.3' fill='none' stroke='%23000' stroke-width='47' stroke-linecap='round'/%3E%3Cpath d='M -84 -120 L 156 -120 L -12 48 L 156 48 L 84 120 L -156 120 L 12 -48 L -156 -48 Z' fill='%23000' transform='translate(320 310)'/%3E%3C/svg%3E");
  -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-position: center; mask-position: center; }
/* On the reader's own row the citrus washes out against the pink, so the coin takes the row's ink. */
.board-row.viewer .board-zap-coin { background: #261225; }
/* The word, for a screen reader that would otherwise be handed a bare number under a time. */
.board-zap-unit { position: fixed; width: 1px; height: 1px; clip: rect(0 0 0 0); overflow: hidden; }
.board-gap { padding: 2px; text-align: center; letter-spacing: .35em; opacity: .4; }
.board-empty { padding: 26px 16px; text-align: center; opacity: .78; }
/* The account panel, where the handle field used to be: signed-in line, sign-in pitch, or a pointer
   to where a rank can be earned. Its buttons are the same `.primary`/`.secondary` pair the card
   actions use — see the note above them — so this block sets layout only and never re-describes
   what a button looks like. */
.board-account { margin: 16px 0 14px; padding: 14px 16px; border: 1px solid #ffffff26; border-radius: 18px; background: #0e1c2d; }
.board-account-headline { margin: 0; font-size: .95rem; font-weight: 900; }
.board-account-benefits { margin: 10px 0 0; padding-left: 20px; display: grid; gap: 5px; font-size: .84rem; opacity: .85; }
.board-account .small-copy { margin: 9px 0 0; font-size: .78rem; }
.board-account-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.board-account-actions:empty, .board-account-actions:not(:has(> :not(.hidden))) { display: none; }
/* Signed in, the panel is a STATUS LINE rather than a card: the handle, what leaving costs, and the
   way out, on one row. As three stacked blocks it pushed the ranking itself down the screen, which
   is the one thing this overlay exists to show. It wraps on a narrow viewport — the sentence is not
   droppable, because one Clerk instance covers every domain and the player has to be told that
   before they press the button — and `margin-left: auto` is on the ACTIONS, the item that must stay
   right, so it still lands right on whatever line it wraps to. */
.board-account.compact { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 10px 8px 10px 16px; }
.board-account.compact .board-account-headline { font-size: .88rem; }
.board-account.compact .small-copy { margin: 0; opacity: .72; }
.board-account.compact .board-account-actions { margin: 0 0 0 auto; }
/* The 44px floor is this game's own accessibility contract; only the horizontal padding shrinks. */
.board-account.compact .board-account-actions button { padding: 10px 16px; font-size: .8rem; }
.visually-hidden { position: fixed; width: 1px; height: 1px; clip: rect(0 0 0 0); overflow: hidden; }

@keyframes spin { to { rotate: 360deg; } }
@keyframes ripple { 0% { opacity: 0; scale: .6; } 25% { opacity: 1; } 100% { opacity: 0; translate: -50% -110%; scale: 1.3; } }
@keyframes mote { 0% { opacity: 0; scale: .3; } 14% { opacity: 1; scale: 1; } 100% { opacity: 0; translate: calc(-50% + var(--dx)) calc(-50% + var(--dy)); rotate: var(--spin); scale: .6; } }
@keyframes bloom { from { opacity: .95; scale: .28; } to { opacity: 0; scale: 8; } }
@keyframes settle { from { opacity: 0; } to { opacity: 1; } }
@keyframes foundPop { 0% { opacity: 0; scale: .72; } 45% { opacity: 1; scale: 1.16; } 100% { opacity: 1; scale: 1; } }
@keyframes hintPulse { 50% { scale: 1.1; box-shadow: 0 0 24px #ff72a5; } }
@keyframes celebrationEntrance { from { opacity: 0; translate: 0 18px; scale: .88; } to { opacity: 1; translate: 0; scale: 1; } }
@keyframes celebrationBob { 50% { translate: 0 -5px; rotate: -1deg; } }
/* The tap demo's two halves of one gesture: the hand travels in from below-right, presses, holds
   while the find reads, and leaves. Both run 4.2s so the ring can only ever appear on the press. */
@keyframes demoHand {
  0%, 6% { opacity: 0; translate: calc(-42% + 52px) calc(-9% + 52px); scale: 1; }
  20% { opacity: 1; translate: calc(-42% + 30px) calc(-9% + 30px); scale: 1; }
  40%, 44% { opacity: 1; translate: -42% -9%; scale: 1; }
  50%, 58% { opacity: 1; translate: -42% -9%; scale: .82; }
  66%, 86% { opacity: 1; translate: -42% -9%; scale: 1; }
  100% { opacity: 0; translate: calc(-42% + 16px) calc(-9% + 16px); scale: 1; }
}
@keyframes demoMark {
  0%, 48% { opacity: 0; scale: .55; }
  56% { opacity: 1; scale: 1.18; }
  64%, 86% { opacity: 1; scale: 1; }
  100% { opacity: 0; scale: 1; }
}

/* ONE answer to "should anything move", and it is the device's. This used to be two rules — this
   media query, guarded against a `force-motion` class, and a `body.reduced-motion` class the game
   set from its own three-state preference. The preference is gone: a game does not need its own copy
   of a setting the player has already made once, for every app they own, in a place they can find.
   `isReducedMotion()` in main.js reads the same query, so the stylesheet and the JavaScript tweens
   cannot disagree about a player. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}

/* TWO COLUMNS WHERE THERE IS WIDTH, which is the other half of the How-to-play fix. The controls
   sentence and the first-clear rule are independent paragraphs of about the same length, so read
   side by side they cost half the height and no words. Below this width the card is too narrow for
   two real columns and they stack, centred, exactly as every other kind of this cover does. */
@media (min-width: 760px) {
  .help-card #interrupt-copy { grid-template-columns: 1fr 1fr; text-align: left; }
  .help-card #interrupt-copy p { margin: 6px 0 0; }
}

@media (max-width: 720px) {
  .topbar { grid-template-columns: 1fr auto 1fr; min-height: calc(52px + var(--safe-top)); gap: 4px; }
  .brand-button { font-size: 0; padding: 0; }
  .brand-button span { font-size: 28px; }
  .hunt-stats { gap: 8px; font-size: .83rem; }
  #timer { font-size: 1.18rem; }
  /* The three chips keep a hairline between them here. They used to be bare words with no fill, so
     `gap: 0` cost nothing; three bordered discs touching read as one segmented control. */
  .top-actions { gap: 4px; }
  .icon-button { font-size: 0; padding: 0; width: 44px; }
  #mute-button::after { content: "🔊"; font-size: 20px; }
  #mute-button[aria-pressed="true"]::after { content: "🔇"; }
  #help-button::after { content: "?"; font-size: 20px; font-weight: 900; }
  #pause-button::after { content: "Ⅱ"; font-size: 19px; font-weight: 900; }
  .scene-controls { padding-top: 5px; padding-bottom: calc(5px + var(--safe-bottom)); }
  .scene-controls button { min-height: 44px; padding: 4px 11px; }
  .scene-controls .hint-button { margin-left: 10px; }
  .level-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .card { padding: 22px; }
  .results-card, .board-card { padding: 22px; }
  .result-celebration { grid-template-columns: minmax(104px, 150px) minmax(0, 1fr); }
  .result-flamingo { height: 250px; }
  .result-score { padding: 13px 16px 12px; }
  .result-fact { padding: 12px 16px; }
  .level-progress { gap: 9px; }
  .progress-label { font-size: .62rem; }
  .board-head { align-items: flex-start; }
  /* The scope picker names fifty scenes, so its own content is wider than a phone. `min-width: 0` is
     what lets a flex item shrink below that at all — without it the row was 388px inside a 360px
     card and the board scrolled sideways by the difference. */
  .board-toggles { justify-content: flex-start; flex-wrap: wrap; max-width: 100%; }
  .board-row, .board-columns { grid-template-columns: 34px minmax(0, 1fr) auto auto; gap: 0 10px; }
  .board-tag { font-size: .6rem; }
  .board-scene { max-width: 100%; min-width: 0; flex: 1 1 auto; }
  /* Full bleed for the three all-content screens is at the BOTTOM of this file, because it has to
     out-rank the short-landscape query below and source order is what decides between two
     class selectors. */
}

@media (max-height: 500px) and (orientation: landscape) {
  .topbar { min-height: calc(46px + var(--safe-top)); padding-top: calc(3px + var(--safe-top)); padding-bottom: 3px; }
  .scene-controls { padding-top: 3px; padding-bottom: calc(3px + var(--safe-bottom)); }
  .scene-controls button { min-height: 44px; }
  .overlay { place-items: start center; }
  .card { margin: auto; padding: 18px 24px; }
  .card h1 { font-size: 2rem; }
  .card p { margin: 7px 0; }
  .level-list { margin-top: 12px; }
  .level-card { padding: 10px 13px; min-height: 62px; }
  /* Size the demo off the VIEWPORT height here, not the card width: a 16:9 still that fills a
     580px card is 326px tall, which is most of a landscape phone before the title and the actions
     have had any. The How-to-play prose shrinks with it, so the whole card — demo, Resume and all —
     fits a 400px-tall phone instead of hiding its own exit below the fold. */
  #interrupt-copy { font-size: .84rem; line-height: 1.35; }
  #interrupt-copy p { margin: 5px 0 0; }
  .help-demo { gap: 14px; margin-top: 8px; }
  .help-demo-frame { width: min(100%, calc(22vh * 760 / 428)); }
  .help-demo-caption { max-width: 15ch; text-align: left; font-size: .78rem; }
  /* The foot this query used to leave out. Motion moved onto How to play and kept its full margins
     at every height, which is most of what put this card's own exit back below the fold. */
  .card-foot { margin-top: 10px; gap: 8px 14px; }
  .card-actions { margin-top: 0; }
  .results-card, .board-card { position: relative; padding: 11px 18px; }
  .results-card h1 { margin: 2px 0 5px; font-size: 1.5rem; }
  .results-card > .eyebrow { font-size: .72rem; }
  .level-progress { gap: 9px; margin-bottom: 8px; }
  .progress-label { font-size: .6rem; }
  .progress-segment { height: 5px; }
  .result-celebration { grid-template-columns: 92px minmax(0, 1fr); max-width: 600px; margin: 0 auto; }
  .result-flamingo { height: 132px; object-position: 50% 3%; }
  .result-bubbles { gap: 5px; }
  .result-bubble { border-width: 2px; border-radius: 14px; box-shadow: 0 5px 0 #0a1423aa; }
  .result-score { margin: 4px 0 0 -5px; padding: 7px 11px; }
  .result-score::before { left: -18px; top: 14px; width: 20px; height: 20px; filter: drop-shadow(-2px 1px 0 #17263a); }
  .result-adjusted { font-size: 1.5rem; }
  .result-breakdown { margin-top: 2px; font-size: .72rem; }
  .result-board-note { margin-top: 3px; font-size: .7rem; }
  .result-fact { margin: 0 0 0 4px; padding: 7px 11px; }
  .card .fact-eyebrow { margin: 0; font-size: .7rem; }
  .card .fact-text { margin: 2px 0 0; font-size: .78rem; line-height: 1.25; }
  .results-card .small-copy { margin: 4px 0; font-size: .76rem; }
  .results-card .card-actions, .board-card .card-actions { margin-top: 7px; }
  .results-card .card-actions button, .board-card .card-actions button { padding-top: 8px; padding-bottom: 8px; }
  .board-card h1 { font-size: 1.4rem; }
  /* The rows scroll inside the card so the head, the callout and the two actions stay on screen.
     Scrolling the whole overlay instead puts Next Level below the fold on every phone. */
  /* 32vh, not 38: the rows are the one part of this card that scrolls on its own, so they are also
     the only place to find the height the rest of it needs. At 38 the card came out 391px tall on a
     390px landscape phone and put its own Back button five pixels below the fold. */
  .board-rows { max-height: 32vh; overflow-y: auto; }
  .board-callout { margin: 8px 0; padding: 7px 12px; font-size: .76rem; }
  /* The two blocks that are all prose. On a landscape phone the card has 390px in total and the
     friends section plus the note were 200 of them, which put Back five pixels below the fold. */
  .board-friends { margin: 8px 0; padding-bottom: 8px; gap: 4px; }
  #board-note { margin: 5px 0 0; font-size: .68rem; }
  .board-columns { padding-bottom: 4px; font-size: .6rem; }
  .board-rows { gap: 4px; min-height: 0; }
  .board-row { padding: 7px 12px; border-radius: 12px; }
  .board-rank, .board-time { font-size: .92rem; }
  .board-name { font-size: .85rem; }
  .handle-form { margin: 8px 0; padding: 9px 12px; }
}

/* You & friends, above the global rows. A friend row IS a board row — the shared renderer draws it
   with the same cells — plus a fifth, auto-sized column for the Remove control. */
.board-friends { display: flex; flex-direction: column; gap: 6px; margin: 16px 0 14px; padding-bottom: 12px; border-bottom: 1px solid #ffffff26; text-align: left; }
.board-friends-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.board-friends-heading { margin: 0; font-size: .95rem; font-weight: 900; }
/* 44px, like every other control here: this is the game's own accessibility floor and a 40 was the
   one place in the shipped UI that broke it. Only the horizontal padding and the type shrink. */
.friends-btn { min-height: 44px; padding: 8px 14px; font-size: .84rem; }
.board-friends-rows { min-height: 0; }
.board-friends-rows .board-row { grid-template-columns: 46px minmax(0, 1fr) auto auto auto; }
.board-row.friend { background: #16283f; }
.board-row.unranked { opacity: .78; }
.board-row.unranked .board-time { opacity: .8; font-size: .78rem; font-weight: 700; }
.friend-remove { justify-self: end; font-size: .78rem; }
.board-friends-note, .board-friends-status, .board-friends-links { margin: 0; text-align: left; }
/* The sign-in INSIDE the guest's friends line, drawn there by the shared client: a word in a
   sentence rather than a control beside one, so it drops `.link-button`'s padding and the 44px
   minimum every other button here carries. An inline link in prose is the one target-size
   exemption. */
.friends-signin { min-width: 0; min-height: 0; border: 0; border-radius: 0; background: none; padding: 0; color: #ffd9e7; font: inherit; font-weight: 900; text-decoration: underline; text-underline-offset: 3px; }
/* The dare card on the title screen: the card's own small copy, with a headline in it. */
.dare-card { margin: 14px 0 0; padding: 12px 16px; border: 1px solid #ffffff2e; border-radius: 16px; background: #0e1c2dcc; text-align: left; }
.dare-headline { display: block; margin-bottom: 4px; color: var(--pink); font-size: 1rem; font-weight: 900; }
/* Armed: the next press removes. */
.friend-remove.confirming { color: var(--pink); font-weight: 900; }

/* ── FULL BLEED ON A PHONE ──────────────────────────────────────────────────────────────────────
   THREE SCREENS HERE ARE THE SCREEN, and they are the three that are ALL CONTENT: the start screen
   (a fifty-tile level board), How to play (two columns of rules and a demo) and the leaderboard (a
   table of ranked rows). A card floating in a gutter says "there is a page behind this, and you can
   see it" — true on a desktop, and on a phone the page behind is a blurred picture and the gutter is
   screen the card has to give back in whichever direction it is already short of.

   THE LANDSCAPE CLAUSE IS THE HALF THAT WAS MISSING, and it is the half this game most needed:
   portrait pauses a hunt, so a phone plays this game in landscape — and a `max-width: 720px` rule
   never fires on an 844x390 handset. Measured before this rule at 844x390: the start screen was an
   812x1584 card at (16,16) with a 26px radius, and How to play an 812x379 card with a 1px border,
   both paying 32px of width and 32px of height for decoration on the axis they had least of. It is
   bounded at 900px so a short DESKTOP window — which the `max-height: 500px` query above also
   matches — keeps its card. Hungry Hungry Snake's stylesheet carries the same rule and the same
   bound.

   Every other cover stays a card, deliberately: the ready card and the pause card are small
   messages, and the results card is a scorecard held up in front of the scene it was won on.

   The safe-area insets move from the overlay onto the card, because a card asked for `100dvh`
   inside a padded overlay overflows by exactly that padding. They are what keeps the content out
   from under a landscape notch now that the overlay's own gutter is gone.

   `box-shadow: none` is not decoration-trimming: `.glass-card` carries an outer shadow, which on a
   card with no outside left is a dark band down the edge of the screen. */
@media (max-width: 720px), (max-height: 520px) and (max-width: 900px) and (orientation: landscape) {
  /* `align-items: start` IS LOAD-BEARING, and it is the half that is easy to leave out.
     `.overlay` centres its card, and a card asked for `min-height: 100dvh` is no longer
     content-sized — so the moment one is TALLER than the screen the browser centres the overflow
     and the part above the scroll origin becomes unreachable. Measured on the level board at
     390×844: a 2,657px card sat at y −907 with the title 862px above the top of a container whose
     whole scrollable height was 1,751px, so the headline, the first-clear rule and the Leaderboard
     button could not be scrolled to at all. Started rather than centred, a tall card scrolls from
     its own top, and a card SHORTER than the screen still paints all of it because `100dvh` is a
     minimum. */
  #level-overlay, #interrupt-overlay:has(.help-card), #board-overlay { padding: 0; align-items: start; }
  .wide-card, .help-card, .board-card {
    width: 100%;
    min-height: 100dvh;
    margin: 0;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: calc(18px + var(--safe-top)) calc(15px + var(--safe-right)) calc(18px + var(--safe-bottom)) calc(15px + var(--safe-left));
  }
}
