/* Drople — dark, cinematic, restrained. Colour carries meaning: green is a
   confirmed tier, amber is the player's own mark, red is cold. */

:root {
  --ink: #f4f2ee;
  --dim: #9b978f;
  --faint: #5c584f;
  --bg: #090a0d;
  --panel: #121419;
  --raised: #1a1d24;
  --line: #262a33;

  --hit: #4ea46a;
  --hit-dim: #16321f;
  --accent: #67e8f9;
  --cold: #d0563f;

  /* Pale, because a guess row carries four of them and a wall of saturated
     colour reads as an alarm rather than as a score. */
  --hit-pale: #4ea46a6b;
  --cold-pale: #d0563f4a;

  --ui: ui-sans-serif, system-ui, "Segoe UI Variable Text", "Segoe UI", Inter, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;

  --pad: 12px;
  --gap: 12px;
  --r-sm: 6px;
  --r: 10px;
  --r-lg: 16px;
  --shadow: 0 18px 50px -12px #000c, 0 2px 8px #0006;
  --ease: cubic-bezier(.22, .61, .36, 1);
}

* { box-sizing: border-box; scrollbar-width: thin; scrollbar-color: #ffffff21 transparent; }
/* Any author `display` outranks the user agent's own [hidden] rule, so
   without this, hiding an element from script silently does nothing. */
[hidden] { display: none !important; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #ffffff21; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #67e8f973; }
html { height: 100%; }
body { height: 100vh; height: 100dvh; }   /* dvh needs Chrome 108 */

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.55 var(--ui);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  /* In a webview the page is the app: nothing bounces, nothing is selectable
     by accident, and a long press on the imagery must not raise a magnifier. */
  overscroll-behavior: none;
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
dialog:focus { outline: none; }
dialog, #log { -webkit-user-select: text; user-select: text; }

button { font: inherit; cursor: default; border: 0; color: inherit; background: none; }

/* Every action shares one carbon surface. Emphasis is light, not a different
   shape, a different colour or a white slab. */
.btn {
  padding: 11px 18px; border-radius: 9px;
  background: #ffffff0a; border: 1px solid #ffffff21; color: var(--ink);
  font: 650 12.5px var(--ui); letter-spacing: .07em; text-transform: uppercase;
  transition: background .18s, border-color .18s, box-shadow .22s, transform .12s var(--ease);
}
@media (hover: hover) {
  .btn:hover:not(:disabled) { background: #67e8f914; border-color: #67e8f973;
                              color: var(--accent); }
}
.btn:focus { outline: none; }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { color: var(--faint); background: #ffffff05; }

/* The preferred action is tinted, not haloed: same shape and size, a warmer
   surface and a brighter edge. */
.btn.lit { background: #67e8f91c; border-color: #67e8f97a; color: var(--accent); }
.btn.lit:hover:not(:disabled) { background: #67e8f92e; border-color: #67e8f9ad; }
.btn.lit:disabled { background: #ffffff05; border-color: var(--line); color: var(--faint); }

.btn.danger { border-color: #5c322d; color: #e8a295; }
.btn.danger:hover { background: #2b1512; border-color: #7a4138; color: #ffd0c6; }

:focus-visible { outline: 1px solid #ffffff73; outline-offset: 1px; border-radius: 9px; }
a { color: var(--accent); }

#defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden;
      clip: rect(0 0 0 0); white-space: nowrap; }

/* ---------------------------------------------------------------- shell */

#app {
  position: relative;
  height: 100%;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto repeat(3, minmax(0, 1fr));
  grid-template-areas: "bar" "stage" "map" "rail";
  gap: var(--gap);
  /* Full-bleed under the notch and the gesture bar; the chrome keeps clear. */
  padding: calc(var(--pad) + env(safe-area-inset-top))
           calc(var(--pad) + env(safe-area-inset-right))
           calc(var(--pad) + env(safe-area-inset-bottom))
           calc(var(--pad) + env(safe-area-inset-left));
}

/* ------------------------------------------------------------------ bar */

#bar { grid-area: bar; display: flex; align-items: center;
       gap: 10px; padding: 0 4px; }

.wordmark { display: flex; align-items: center; gap: 6px; text-decoration: none; color: inherit; }
.wordmark .mark { width: 30px; height: 30px; --drop-body: var(--accent); }
.wordmark b { font-weight: 650; font-size: 14px; line-height: 1;
              letter-spacing: .15em; text-transform: uppercase; }

/* A round number tells a player nothing; the date tells them which day they
   are playing and whether it is today's. */
/* Centred in the bar rather than trailing the wordmark: it is the title
   of the round, not an afterthought beside the brand. */
#dayno {
  min-width: 0; overflow: hidden; display: flex; align-items: center; gap: 7px;
  font: 12px/1 var(--mono); letter-spacing: .04em; color: var(--faint);
  white-space: nowrap;
}
/* One shell, two glyphs. The question mark's ink sits inside its box where
   the gear's fills it, so it takes the larger box to weigh the same. */
.chip {
  position: relative;
  flex: none; width: 32px; height: 32px; padding: 0; border-radius: 9px;
  border: 1px solid var(--line); color: var(--dim);
  display: grid; place-items: center;
  transition: color .18s, border-color .18s, background .18s,
              transform .12s var(--ease);
}
/* A 48px target around a 32px button: Android's floor, without the button
   growing to meet it. */
.chip::after { content: ""; position: absolute; inset: -8px; }
.chip svg { width: 16px; height: 16px; fill: currentColor; }
/* A tap leaves hover behind on a touch screen, so a control that has been
   pressed goes on looking chosen. Hover belongs to pointers that can hover. */
@media (hover: hover) {
  .chip:hover:not(:disabled) { color: var(--accent); border-color: #67e8f973; }
}
.chip:active:not(:disabled) { transform: translateY(1px); }
.chip:focus { outline: none; }
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.chip:disabled { opacity: .3; }
/* Over the imagery it needs a floor to sit on. */
.chip.over { background: #090a0dcc; }

/* Hard right in the header, past the date. */
/* The drop's own share sits with the menu, away from the result's. */
#sharedrop { margin-left: auto; }
#sharedrop.done { color: var(--hit); border-color: #2d5c3c; }

/* ---------------------------------------------------------------- stage */

/* Every card on the board is the same card: one edge, one corner, one
   surface. What each adds below is only what makes it that card.

   Each is its own stacking layer, so nothing the map libraries put inside —
   MapillaryJS stacks its loader at 100 — can rise over the launch screen,
   the menu or a dialog. */
#stage, #mapwrap, .block {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--panel);
  isolation: isolate;
}

#stage {
  grid-area: stage;
  position: relative;
  overflow: hidden;
  background: #000;        /* letterboxed imagery, not panel */
}
#pano { position: absolute; inset: 0; }

/* One line, one baseline, bottom left of the stage. */
/* Both credits are the same badge in the same corner of their own card:
   the source, and whoever made it. The licences live in the credits, which
   is what tapping either one opens. */
.credit {
  position: absolute; right: 0; bottom: 0; z-index: 10; margin: 0;
  padding: 4px 11px 5px 13px; border: 0; border-top-left-radius: var(--r);
  font: 9.5px/1 var(--mono); color: #9b968c; letter-spacing: .02em;
  text-align: right; max-width: 72%;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  background: linear-gradient(270deg, #000000a6 70%, #0000);
  transition: color .18s;
}
.credit:empty { display: none; }
.credit:hover { color: var(--ink); }

#compass .tick { fill: var(--ink); font: 700 13px var(--ui); text-anchor: middle; }
#compass .tick.minor { fill: var(--faint); font-size: 11px; }

/* ------------------------------------------------------- movement arrows */

/* MapillaryJS ships chunky filled chevrons on a black hover disc. Same
   affordance, drawn thin and lit in the game's cyan.

   It juggles class names for the same control — step against spherical,
   sequence against not — and swaps in -highlight while hovered, so state is
   matched on the substring. Pairing exact combinations left some arrows with
   no hover at all.

   Every kind is drawn at one height, whatever disc the library sizes it
   against: the steps and the turns are the same control and should read so. */
#pano { --arrow-w: 67px; --arrow-h: 39px; }
#pano [class*="mapillary-direction-arrow"] {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 58.77'%3E%3Cpath fill='%2367e8f9' stroke='%2367e8f9' stroke-width='7' stroke-linejoin='round' d='M50 11 L85 45 L50 33 L15 45 Z'/%3E%3C/svg%3E");
  background-size: contain; background-position: center;
  opacity: .7; width: var(--arrow-w); height: var(--arrow-h); top: 50%;
  filter: drop-shadow(0 2px 4px #000b);
  transition: opacity .15s var(--ease), filter .15s var(--ease);
}
#pano [class*="mapillary-direction-arrow-inactive"] { opacity: .12; }

/* The turns are a separate family — their class says turn, not arrow — and
   they ship as white SVGs baked into the library. Same drawing, our colour,
   and the same weight as the chevrons beside them. */
#pano [class*="mapillary-direction-turn-"]:not([class*="circle"]) {
  height: var(--arrow-h); background-size: contain; background-repeat: no-repeat;
  background-position: center;
  opacity: .62;
  filter: drop-shadow(0 2px 4px #000b);
  transition: opacity .15s var(--ease), filter .15s var(--ease);
}
#pano .mapillary-direction-turn-left,
#pano .mapillary-direction-turn-right { width: calc(var(--arrow-h) * .84); }
#pano .mapillary-direction-turn-around { width: calc(var(--arrow-h) * 1.19); }
#pano .mapillary-direction-turn-left {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%20100%20119.06%27%3E%3Cpath%20fill%3D%27%2367e8f9%27%20d%3D%27M99.2%2C76C98%2C64.91%2C95.06%2C56.6%2C89.57%2C50%2C79%2C37.38%2C60.3%2C32.72%2C24.42%2C32.39L44.89%2C11.93A7%2C7%2C0%2C0%2C0%2C35%2C2h0l-33%2C33a7%2C7%2C0%2C0%2C0%2C0%2C9.88L35%2C77.85a7%2C7%2C0%2C1%2C0%2C9.9-9.9L24.79%2C47.89C56%2C48.18%2C71%2C51.93%2C77.72%2C60c3.21%2C3.88%2C5.18%2C9.42%2C6.11%2C17.77%2C0.85%2C7.59.87%2C16.22%2C0.34%2C33.36a7.74%2C7.74%2C0%2C1%2C0%2C15.46.48C100.19%2C93.72%2C100.16%2C84.6%2C99.2%2C76Z%27%2F%3E%3C%2Fsvg%3E");
}
#pano .mapillary-direction-turn-right {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%20100%20119.06%27%3E%3Cpath%20fill%3D%27%2367e8f9%27%20d%3D%27M99.2%2C76C98%2C64.91%2C95.06%2C56.6%2C89.57%2C50%2C79%2C37.38%2C60.3%2C32.72%2C24.42%2C32.39L44.89%2C11.93A7%2C7%2C0%2C0%2C0%2C35%2C2h0l-33%2C33a7%2C7%2C0%2C0%2C0%2C0%2C9.88L35%2C77.85a7%2C7%2C0%2C1%2C0%2C9.9-9.9L24.79%2C47.89C56%2C48.18%2C71%2C51.93%2C77.72%2C60c3.21%2C3.88%2C5.18%2C9.42%2C6.11%2C17.77%2C0.85%2C7.59.87%2C16.22%2C0.34%2C33.36a7.74%2C7.74%2C0%2C1%2C0%2C15.46.48C100.19%2C93.72%2C100.16%2C84.6%2C99.2%2C76Z%27%2F%3E%3C%2Fsvg%3E");
}
#pano .mapillary-direction-turn-around {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%20100%2084.33%27%3E%3Cpath%20fill%3D%27%2367e8f9%27%20d%3D%27M65.47%2C0C56%2C0%2C46.86%2C5.39%2C40.21%2C16.07%2C33.47%2C27%2C29.36%2C43.18%2C28.43%2C65L10%2C46.59a5.87%2C5.87%2C0%2C0%2C0-8.3%2C8.3L29.44%2C82.61a5.88%2C5.88%2C0%2C0%2C0%2C8.3%2C0L65.47%2C55a5.88%2C5.88%2C0%2C1%2C0-8.32-8.32L39.32%2C64.47c0.92-19.56%2C4.5-33.7%2C10.11-42.72%2C4.72-7.62%2C10.45-11%2C16.1-11a16.64%2C16.64%2C0%2C0%2C1%2C14.78%2C8C86%2C27%2C89.21%2C41.85%2C89.21%2C64.47a5.39%2C5.39%2C0%2C1%2C0%2C10.79%2C0C100.05%2C18.78%2C87.22%2C0%2C65.47%2C0Z%27%2F%3E%3C%2Fsvg%3E");
}

#pano [class*="mapillary-direction-turn-circle"]:hover [class*="mapillary-direction-turn-"],
#pano [class*="mapillary-direction-turn-circle"][class*="highlight"] [class*="mapillary-direction-turn-"] {
  opacity: 1;
  filter: brightness(1.2) drop-shadow(0 0 7px #67e8f999) drop-shadow(0 2px 4px #000b);
}

/* The arrow carries its own state, so the disc under it never shows. */
#pano [class*="mapillary-direction-circle"],
#pano [class*="mapillary-direction-turn-circle"] {
  background: transparent; box-shadow: none;
}

#pano [class*="mapillary-direction-circle"]:hover [class*="mapillary-direction-arrow"],
#pano [class*="mapillary-direction-circle"][class*="highlight"] [class*="mapillary-direction-arrow"] {
  opacity: 1;
  filter: brightness(1.2) drop-shadow(0 0 7px #67e8f999) drop-shadow(0 2px 4px #000b);
}

/* Opposite the maximise control, and above the credit line. */
/* ------------------------------------------------------------- compass */

#compass {
  position: absolute; top: 10px; left: 10px; z-index: 12;
  width: 54px; height: 54px; pointer-events: none;
}
#compass svg { width: 100%; height: 100%; display: block; }
#compass .dial { fill: #0a0b0ec4; stroke: var(--line); stroke-width: 1.5; }
#rose { transition: transform .12s linear; }
#compass .needle-n { fill: var(--cold); }
#compass .needle-s { fill: #ffffff2e; }
#compass .tick { fill: var(--ink); font: 700 13px var(--ui); text-anchor: middle; }
#compass .tick.minor { fill: var(--faint); font-size: 11px; }

/* ------------------------------------------------------------------ map */

/* Beside the stage rather than nested inside it, so it never fights the
   stage's overflow clipping. */
#mapwrap {
  position: relative; grid-area: map; z-index: 30;
  overflow: hidden;
  box-shadow: var(--shadow);
  transition: height .38s var(--ease);
}
/* Focus only applies to the inline card. Fullscreen and the result view set
   their own height, and these rules out-specify both if left open. */
/* Maximised, the map takes the street view's row outright rather than
   floating over it — the same shape the end of a round uses. */
#mapwrap.open { animation: take-over .28s var(--ease); }
@keyframes take-over { from { opacity: 0; } }

#map { position: absolute; inset: 0; }
#map canvas { cursor: crosshair; }
body.finished #map canvas { cursor: default; }

/* Sits under the map, within reach of the clue panel it feeds. */
#maptoggle, #panotoggle { position: absolute; top: 8px; right: 8px; z-index: 13; }
#maptoggle svg, #panotoggle svg, #endtoggle svg {
  fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
#maptoggle, #panotoggle, #endtoggle { color: var(--ink); }
.ico-close, .open .ico-open { display: none; }
.open .ico-close { display: block; }

/* Offered only once there is a pin to lock in. */
/* Placement only; appearance comes from .btn. */
/* A readout above the button, so neither the button nor its label moves. */
#snapname {
  position: absolute; left: 50%; bottom: 56px; z-index: 4; margin: 0;
  transform: translateX(-50%);
  font: 12px var(--mono); letter-spacing: .04em; color: var(--accent);
  text-shadow: 0 1px 6px #000c; pointer-events: none;
}
#go {
  position: absolute; left: 50%; bottom: 14px; z-index: 4;
  transform: translateX(-50%);
  max-width: calc(100% - 28px);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  background: #090a0dd9;
  animation: lock-in .3s var(--ease);
}
#go:active:not(:disabled) { transform: translateX(-50%) translateY(1px); }

@keyframes lock-in { from { opacity: 0; transform: translateX(-50%) translateY(8px); } }

/* Each card is its own element in a view transition, so maximising moves
   and resizes them rather than cross-fading the whole screen. */
#stage { view-transition-name: stage; }
#mapwrap { view-transition-name: mapwrap; }
#rail { view-transition-name: rail; }
/* The snapshot layer would swallow a tap made while the cards settle; the
   real elements are already in their final places beneath it. */
::view-transition { pointer-events: none; }
::view-transition-group(*) {
  animation-duration: .42s;
  animation-timing-function: cubic-bezier(.3, 1.2, .4, 1);   /* a touch of overshoot */
}

/* A maximised card takes everything under the header. The end of a round
   maximises the map, trails, result and all; after that, maximising and
   putting the result away are separate controls that never move each other. */
body.max-map #app { grid-template-rows: auto 1fr; grid-template-areas: "bar" "map"; }
body.max-pano #app { grid-template-rows: auto 1fr; grid-template-areas: "bar" "stage"; }
body.max-map #stage, body.max-map #rail,
body.max-pano #mapwrap, body.max-pano #rail { display: none; }

/* ------------------------------------------------------------ result bar */

#endtoggle {
  position: absolute; left: 8px; top: 8px; z-index: 9;
  transition: border-color .18s, transform .28s var(--ease);
}
body.result-away #endtoggle { transform: rotate(180deg); }
body.result-away #end { translate: 0 101%; pointer-events: none; }

#end {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 7;
  max-height: 100%; overflow-y: auto;
  transition: translate .34s var(--ease);
  /* A long ramp, so the trails still read through the top of the veil while
     the text below it sits on something solid enough to be legible. */
  background: linear-gradient(#0000 0, #090a0dbf 15%, #090a0deb 36%, #090a0dfa 62%);
  padding: 72px 13px 13px;
  animation: spring-up .7s both .25s;
}
@keyframes rise { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
/* The result comes up with weight: it overshoots, settles back, and lands. */
@keyframes spring-up {
  0%   { opacity: 0; transform: translateY(48px); animation-timing-function: cubic-bezier(.2, .8, .3, 1); }
  55%  { opacity: 1; transform: translateY(-7px); animation-timing-function: ease-in-out; }
  78%  { transform: translateY(2px); }
  100% { opacity: 1; transform: none; }
}
/* The verdict pops: squashed small, overshoots, and snaps to size. */
#endtitle { animation: pop .6s both .45s; }
@keyframes pop {
  0%   { opacity: 0; transform: scale(.6, .5); animation-timing-function: cubic-bezier(.2, .8, .3, 1); }
  55%  { opacity: 1; transform: scale(1.12, 1.08); }
  75%  { transform: scale(.96, 1.02); }
  100% { opacity: 1; transform: none; }
}

/* Title, then place, then the round at a glance, then the action. A column
   of six tier-stacks reads as "guess 1 to 6" and fits a bar; the old portrait
   grid forced a tall block beside an oversized button. */
.result {
  display: flex; flex-direction: column; align-items: center;
  gap: 11px; text-align: center;
}
#endhead {
  margin: 0; text-shadow: 0 1px 10px #000, 0 0 3px #000c;
  font: 650 10px/1 var(--ui); letter-spacing: .2em; text-transform: uppercase;
  color: var(--dim);
}
#endtitle {
  margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -.01em;
  text-shadow: 0 1px 12px #000000cc, 0 0 4px #000000a6;
}
#endsub { color: var(--dim); font-size: 13px; text-shadow: 0 1px 10px #000000cc; }
#endsub b { display: block; color: var(--ink); font-weight: 500; }
#endsub i { display: block; margin-top: 3px; font-style: normal; font-size: 12px; }

/* The ladder, exactly as it will be shared: which way you were off, and by
   how much, closing in down the rows. */
#endgrid { display: grid; gap: 3px; justify-items: center; }
#endgrid .row {
  display: grid; grid-template-columns: 1.1em auto; align-items: baseline;
  gap: 9px; font: 12.5px/1.5 var(--mono); color: var(--dim);
  animation: fade-sq .3s var(--ease) both; animation-delay: calc(var(--i) * 60ms + .3s);
}
#endgrid .aim {
  width: 1em; height: 1em; align-self: center; color: var(--faint);
  rotate: var(--turn);
}
#endgrid .aim.found { rotate: none; --drop-body: currentColor; }
#endgrid .km { text-align: right; font-variant-numeric: tabular-nums; }
#endgrid .row.on { color: var(--hit); }
#endgrid .row.on .aim { color: var(--hit); }
@keyframes fade-sq { from { opacity: 0; transform: scale(.4); } }

/* Share, and in Infinite the next drop: one column, one width. */
.actions { display: grid; gap: 9px; width: min(220px, 100%); }
#copy.done { border-color: #2d5c3c; color: var(--hit); box-shadow: none; }

/* ------------------------------------------------------------------- pin */

.pin { position: relative; width: 32px; height: 32px; cursor: grab; }
.pin.truth { width: 32px; height: 32px; cursor: default; }
.pin .mark {
  width: 100%; height: 100%; display: block;
  transform-origin: 50% 100%;
  --drop-body: var(--accent);
  filter: drop-shadow(0 1px 0 #00000080) drop-shadow(0 4px 6px #000a);
}
.pin.truth .mark { --drop-body: var(--cold); }

/* Squash and stretch: it draws out as it falls, flattens on the tip, and
   recovers. No vertical rebound — the weight is carried by the shape. */
.pin.drop-in .mark { animation: pin-drop .38s cubic-bezier(.3, 0, .6, 1) both; }
@keyframes pin-drop {
  0%   { transform: translateY(-30px) scale(.84, 1.28); opacity: 0; }
  30%  { opacity: 1; }
  52%  { transform: translateY(0) scale(.88, 1.2); opacity: 1; }
  66%  { transform: scale(1.26, .76); }
  80%  { transform: scale(.95, 1.07); }
  90%  { transform: scale(1.02, .98); }
  100% { transform: none; }
}

/* The ring clears the whole pin with a gutter all round, the way the logo
   does. Offset 1 is empty, 0 is shut at the top, -1 has been pulled away from
   the bottom again — one keyframe does both halves of the gesture. */
/* Same coordinate space as the mark, so it simply overlays it. */
.pin .ring { position: absolute; inset: 0; overflow: visible;
             pointer-events: none; }
.pin .ring path {
  fill: none; stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round;
  stroke-dasharray: 1 1; stroke-dashoffset: 0;
}
.pin.drop-in .ring path { animation: trace .46s cubic-bezier(.45, 0, .55, 1) .2s both; }
/* Both halves climb from the ball and shut at the top, and then stay: the
   ring belongs to the mark, it is not a flourish over it. An offset of 1
   leaves the whole path in the gap; any more wraps a sliver of dash onto the
   far end, which is the top. Unseen until the trace begins, too: a round cap
   draws even a dash of no length. */
@keyframes trace {
  0%   { stroke-dashoffset: 1; opacity: 0; }
  4%   { opacity: 1; }
  100% { stroke-dashoffset: 0; opacity: 1; }
}

/* What a truncated label is hiding, on a long press or a hover. */
#peek {
  position: fixed; z-index: 120; padding: 7px 11px; border-radius: 9px;
  background: var(--raised); border: 1px solid var(--line); box-shadow: var(--shadow);
  font: 12.5px var(--ui); color: var(--ink); white-space: nowrap;
  /* Its foot sits exactly where the guesses card begins. */
  pointer-events: none; opacity: 0; transform: translate(-50%, -100%);
  transition: opacity .14s var(--ease);
}
#peek.on { opacity: 1; }

/* ----------------------------------------------------------------- menu */

.pick {
  display: flex; align-items: center; gap: 13px;
  height: 36px; padding: 0 20px 0 12px;
  border: 1px solid var(--line); border-radius: 9px;
  color: var(--ink); text-align: left;
  transition: border-color .16s, background .16s, color .16s,
              transform .12s var(--ease);
}
.pick + .pick { margin-top: 7px; }
.pick svg { flex: none; width: 16px; height: 16px; fill: currentColor; }
/* Drawn rather than filled, like the mark in the header, and a touch larger
   so its stroke carries the same weight as the solid glyphs beside it. */
.pick .infinity {
  fill: none; stroke: currentColor; stroke-width: 2.2;
  stroke-linecap: round; stroke-linejoin: round;
  width: 20px; height: 20px;
}
.pick span { font: 550 13px var(--ui); white-space: nowrap; }
@media (hover: hover) {
  .pick:hover { border-color: #67e8f96b; background: #67e8f90f; color: var(--accent); }
}
.pick:active { transform: translateY(1px); }

/* Not a way into the game, so it does not look like one: it sits on the floor
   of the panel as its own kind of control. */
#account {
  margin-top: auto; padding: 11px 14px;
  display: flex; align-items: center; justify-content: center; gap: 9px;
  font-size: 11.5px;
}
#account svg { width: 15px; height: 15px; fill: currentColor; }

/* ------------------------------------------------------------ statistics */

#tally {
  list-style: none; margin: 0 0 4px; padding: 0;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px;
}
#tally li { text-align: center; }
#tally b { display: block; font: 600 21px/1.2 var(--ui); color: var(--ink); }
#tally span {
  font: 650 9px/1 var(--ui); letter-spacing: .12em;
  text-transform: uppercase; color: var(--faint);
}

/* One bar per number of guesses, read against the tallest. The last row is
   the rounds that got away. */
#spread { display: flex; flex-direction: column; gap: 5px; }
.bar { display: flex; align-items: center; gap: 8px; }
.bar .n {
  flex: none; width: 13px;
  font: 500 11.5px var(--mono); color: var(--faint); text-align: center;
}
.bar .fill {
  min-width: 26px; padding: 3px 7px; border-radius: 4px;
  background: var(--hit-dim); color: var(--ink);
  font: 500 11px var(--mono); text-align: right;
  transition: width .5s var(--ease);
}
/* The rounds that got away do not read as wins. */
.bar.missed .fill { background: #2b1512; color: #f0c7bf; }
.bar.empty .fill, .bar.missed.empty .fill { background: #ffffff0f; color: var(--faint); }

/* ----------------------------------------------------------------- rail */

/* Nothing is sold in here, so the one card is composed in the column rather
   than stacked at the top of it. */
#rail { grid-area: rail; display: flex; flex-direction: column;
        gap: var(--gap); min-height: 0; }
#dossier { flex: 1; display: flex; flex-direction: column; min-height: 0; }
/* An overlay scrollbar sits on top of the content in a webview, where
   scrollbar-gutter is not honoured, so the lane is held open by hand. */
#log { flex: 1; min-height: 0; overflow-y: auto;
       scrollbar-gutter: stable; padding-right: 14px; }

.block h2 {
  margin: 0 0 11px;
  font: 650 11px/1 var(--ui); letter-spacing: .17em; text-transform: uppercase;
  color: var(--faint);
  display: flex; justify-content: space-between; align-items: baseline;
}
.block h2 span { font: 12px var(--mono); letter-spacing: 0; color: var(--dim); }

/* One line of established facts: icon and value, no heading above them and no
   colour — green and red belong to the guess marks. */
#clues {
  list-style: none; margin: 0 0 10px; padding: 0;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px;
}
.clue {
  display: flex; align-items: center; gap: 5px; min-width: 0;
  padding: 5px 6px; border-radius: var(--r-sm);
  background: #ffffff05;
}
.clue .ico {
  width: 13px; height: 13px; flex: none;
  fill: var(--faint); transition: fill .3s var(--ease);
}
.clue .ico.drop { --drop-body: var(--faint); transition: none; }
.clue.known .ico.drop { --drop-body: var(--dim); }
.clue .val {
  flex: 1; min-width: 0; text-align: center;
  font-size: 11.5px; color: var(--faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.clue.known { background: #ffffff0d; animation: fill-in .45s var(--ease); }
/* Earned during play, or only shown because the guesses ran out. */
.clue.earned { background: #4ea46a1f; }
.clue.missed { background: #d0563f1a; }
.clue.known:hover { background: #67e8f912; }
.guess:hover .where { color: var(--accent); }
.clue.known .ico { fill: var(--dim); }
.clue.known .val { color: var(--ink); font-weight: 550; }
.clue.na { opacity: .35; }
@keyframes fill-in { from { background: #ffffff26; } }

#log {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 7px;
}

/* A guess reports on the four slots above it, so its marks are dashes in the
   same four columns rather than a cluster of squares off to one side. */
.guess {
  padding: 9px 12px 10px;
  border: 1px solid var(--line); border-radius: var(--r);
  transition: border-color .16s, background .16s;
}
@media (hover: hover) {
  .guess:not(.blank):hover { border-color: #67e8f96b; background: #67e8f90d; }
}
.guess.blank { border-color: #ffffff12; }
.guess.fresh { animation: slide-in .35s var(--ease) both; }
@keyframes slide-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.guess.blank { opacity: .5; }
.guess.blank .where::after { content: "—"; color: var(--faint); }

.guess .line { display: flex; align-items: center; gap: 9px; margin-bottom: 7px; }
.guess .n {
  flex: none; width: 17px; height: 17px; border-radius: 50%;
  display: grid; place-items: center;
  font: 650 10px var(--mono); color: var(--faint);
  box-shadow: inset 0 0 0 1px var(--line);
}
.guess .where {
  flex: 1; min-width: 0;
  font-size: 12.5px; font-weight: 450;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.guess .d { flex: none; font: 11px var(--mono); }

.guess .marks { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.guess .tile { height: 4px; border-radius: 2px; background: #ffffff21; }
.guess.fresh .tile { animation: dash-in .4s var(--ease) both; animation-delay: calc(var(--i) * 70ms); }
.guess .tile.on { background: var(--hit-pale); }
/* A tier that was wrong says so, rather than looking like one not yet tried. */
.guess .tile.off { background: var(--cold-pale); }
.guess .tile.na { background: none; box-shadow: inset 0 0 0 1px var(--line); }
@keyframes dash-in { from { transform: scaleX(0); } }

/* --------------------------------------------------------------- dialogs */

/* Four cards, one shape: the same measure, the same gutters, the same title
   and a close in the same corner. */
#howto, #roundpicker, #adgate, #prefs, #stats {
  position: relative;
  width: min(380px, 92vw); padding: 18px;
  border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--raised); color: var(--ink); box-shadow: var(--shadow);
}
#howto::backdrop, #roundpicker::backdrop, #adgate::backdrop, #prefs::backdrop,
#stats::backdrop {
  background: #000b; backdrop-filter: blur(3px);
}
#howto h2, #roundpicker h2, #adgate h2, #prefs h2, #stats h2 {
  margin: 0 0 16px; font: 650 11px/1 var(--ui);
  letter-spacing: .18em; text-transform: uppercase; color: var(--dim);
}
.dismiss { position: absolute; top: 10px; right: 10px; z-index: 2; }
.dismiss svg { width: 15px; height: 15px; }

/* It comes out from under the header, which stays where it is — so the menu
   button is both the way in and the way out, and no close is needed.
   Not modal: a backdrop would put the header behind a veil. `top` is set
   from the header's own box when it opens. */
#drawer {
  position: fixed; right: 0; bottom: auto; left: auto; z-index: 100;
  margin: 0; max-height: none;
  width: max-content; min-width: 208px; max-width: 76vw;
  padding: 14px;
  border: 1px solid var(--line); border-radius: var(--r-lg);
  /* Carbon rather than a slab: the game stays visible behind it, the way the
     result does over the map. */
  background: #090a0de6; backdrop-filter: blur(14px);
  color: var(--ink);
  box-shadow: -14px 16px 50px #000a;
  display: flex; flex-direction: column;
}
/* display here would otherwise beat the UA's rule for a closed dialog,
   leaving the panel on screen over the right of the app. */
#drawer:not([open]) { display: none; }
#drawer[open] { animation: drawer-in .26s var(--ease); }
#drawer.closing { animation: drawer-out .2s var(--ease) forwards; }
@keyframes drawer-in { from { transform: translateX(100%); } }
@keyframes drawer-out { to { transform: translateX(100%); } }

/* Lit while it is open, like any other pressed control. */
#menu[aria-expanded="true"] {
  background: #67e8f91c; border-color: #67e8f97a; color: var(--accent);
}

/* The account keeps the floor of the panel however short the list is. */

#howto h3, #stats h3 {
  margin: 18px 0 8px; font: 650 11px/1 var(--ui);
  letter-spacing: .18em; text-transform: uppercase; color: var(--dim);
}
#howto ol { margin: 0; padding-left: 18px; color: var(--dim); font-size: 13.5px; }
#howto li { margin-bottom: 8px; }
#credits {
  list-style: none; margin: 0; padding: 0;
  font-size: 12px; line-height: 1.75; color: var(--faint);
}

/* A setting is a name, a line about it, and the one control it needs. */
.setting {
  display: flex; align-items: center; gap: 14px;
  padding: 13px 0; border-top: 1px solid var(--line);
}
.setting > div { flex: 1; min-width: 0; }
.setting b { display: block; font: 550 13px var(--ui); }
.setting span { display: block; margin-top: 2px; font-size: 11.5px; line-height: 1.35;
                color: var(--faint); }
.setting .btn { flex: none; min-width: 84px; padding: 9px 14px; font-size: 11px; }

/* The month and its arrows are one control across the head of the archive. */
.calhead {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  margin: 22px 0 16px;
}
#prevmonth { justify-self: center; }
#nextmonth { justify-self: center; }

/* A corner control, not a bar across the foot of the dialog. */
/* A fixed measure, so the arrows do not shuffle as the month name changes. */
#monthlabel { text-align: center;
              font: 600 14px var(--ui); letter-spacing: .06em; }

.dow, #cal { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; }
.dow {
  margin-bottom: 6px;
  font: 650 10px/1 var(--ui); letter-spacing: .1em; color: var(--faint);
}
.dow i { font-style: normal; text-align: center; }

#cal { margin-bottom: 18px; }
.day {
  aspect-ratio: 1; border-radius: 9px;
  display: grid; place-items: center;
  font: 500 13px var(--mono); color: var(--faint);
  border: 1px solid transparent;
}
.day.pad { visibility: hidden; }
.day.out { opacity: .22; }
.day.locked { opacity: .5; }            /* playable, but it costs a video or the pass */                       /* before launch or still ahead */
.day.open {
  color: var(--dim); border-color: var(--line); background: #ffffff05;
  transition: background .15s, color .15s, border-color .15s;
}
.day.open:hover { background: #67e8f914; color: var(--accent); border-color: #67e8f973; }
.day.won { background: var(--hit-dim); border-color: #2d5c3c; color: #cfe9d8; }
.day.lost { background: #2b1512; border-color: #5c322d; color: #f0c7bf; }
.day.today { box-shadow: inset 0 0 0 1.5px var(--accent); color: var(--accent); }
.day.current { outline: 2px solid var(--faint); outline-offset: 1px; }
.day.today.current { outline: none; }    /* the amber ring already says it */

.legend {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  font: 11px var(--ui); color: var(--faint);
}
.legend .key { width: 10px; height: 10px; border-radius: 3px; margin-left: 9px; }
.legend .key:first-child { margin-left: 0; }
.legend .won { background: var(--hit-dim); box-shadow: inset 0 0 0 1px #2d5c3c; }
.legend .lost { background: #2b1512; box-shadow: inset 0 0 0 1px #5c322d; }
.legend .open { background: #ffffff0f; box-shadow: inset 0 0 0 1px var(--line); }

#adgate h2 { margin-bottom: 14px; }
#adgate .note { margin: 0 0 16px; color: var(--dim); font-size: 13px; text-align: center; }
/* Same shape for both, so the difference reads as emphasis and not as two
   unrelated controls. */
.askrow { display: flex; gap: 10px; }
.askrow .btn { flex: 1; }



/* A quiet second route out of a paywall, next to the loud one. */

/* The scale bar belongs to the map, not to MapLibre's default chrome. */
.maplibregl-ctrl-scale {
  background: none; border: 0; border-bottom: 1px solid #ffffff4d;
  border-radius: 0;
  color: var(--dim); font: 9.5px var(--mono); line-height: 1.4;
  padding: 0 0 2px; margin: 0;
  text-shadow: 0 1px 3px #000000b3;
}
.maplibregl-ctrl-bottom-left { bottom: 6px; left: 6px; }
body.finished:not(.result-away) .maplibregl-ctrl-scale { display: none; }

/* Every control presses the same way; .chip already carries its own. */
.day.open, .day.won, .day.lost {
  transition: background .18s var(--ease), border-color .18s var(--ease),
              color .18s var(--ease), box-shadow .18s var(--ease),
              transform .12s var(--ease);
}
.day.open:active, .day.won:active, .day.lost:active { transform: translateY(1px); }
.day.won:hover, .day.lost:hover { filter: brightness(1.25); }

/* Exactly the Android launch screen: the icon's carbon behind the mark, and
   the mark at the size the launch screen draws it, 117dp of ring. When the
   round is ready the mark flies to its place in the header and the game
   shows through. */
#boot {
  position: fixed; inset: 0; z-index: 60; display: grid; place-content: center;
  justify-items: center; gap: 18px; padding: 24px; text-align: center;
  background: #101217; font: 13px/1.6 var(--mono); color: var(--dim);
}
#boot .mark { position: relative; width: 121px; height: 121px; --drop-body: var(--accent); }
#boot .mark svg, #boot .mark canvas {
  position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible;
}

/* The header's own mark waits for this one to land on it. */
body:has(#boot) .wordmark .mark { visibility: hidden; }

#boot.leaving { pointer-events: none; animation: boot-out .5s ease-in .3s forwards; }
#boot.leaving .mark { animation: boot-fly .85s forwards; }
@keyframes boot-out { to { background-color: #10121700; } }
/* Cartoon timing: a slight crouch, an accelerating leap, an overshoot past
   the mark, a squash on landing, and a snap back to size. The take-off is
   kept gentle — the mark has to read as itself while it moves. */
@keyframes boot-fly {
  0%   { transform: none; animation-timing-function: cubic-bezier(.3, 0, .6, 1); }
  14%  { transform: translate(calc(var(--dx) * -.03), calc(var(--dy) * -.03)) scale(1.03, .97);
         animation-timing-function: cubic-bezier(.6, 0, .9, .6); }
  62%  { transform: translate(calc(var(--dx) * 1.05), calc(var(--dy) * 1.05))
                    scale(calc(var(--s) * .95), calc(var(--s) * 1.05));
         animation-timing-function: cubic-bezier(.2, .7, .4, 1); }
  78%  { transform: translate(var(--dx), var(--dy))
                    scale(calc(var(--s) * 1.22), calc(var(--s) * .8)); }
  90%  { transform: translate(var(--dx), var(--dy))
                    scale(calc(var(--s) * .95), calc(var(--s) * 1.06)); }
  100% { transform: translate(var(--dx), var(--dy)) scale(var(--s)); }
}

/* ------------------------------------------------------------ responsive */

/* The app is one shape at every size: the map is a card under the street
   view, because the imagery is the puzzle and the map is only the answer.
   What is left here is unconditional, so it belongs with the rules it
   refines rather than below them. */

#stage { min-height: 0; }

.block { padding: 12px 13px; }

/* A tall window keeps the stack, but a stack has no business being 1600px
   wide, so it holds a phone's measure and sits in the middle. */
@media (min-width: 620px) { #app { max-width: 560px; margin: 0 auto; } }

/* Room for two: the imagery and the map side by side, the guesses under both.
   The toggle goes with them — it exists to swap two cards that cannot share a
   screen, and here they can. */
@media (min-width: 700px) and (orientation: landscape) {
  #app {
    max-width: none;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1.1fr) minmax(0, 1fr);
    grid-template-areas: "bar bar" "stage map" "stage rail";
  }
  #log { gap: 9px; }
  .guess .line { margin-bottom: 9px; }

  /* Maximising means the same everywhere: the card takes the whole area. */
  body.max-map #app, body.max-pano #app { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-delay: 0ms !important;
    transition-duration: .01ms !important;
  }
}

/* ------------------------------------------------------------- trouble */

/* Anything that failed says so here: a failed panorama used to be a black
   rectangle and no explanation. Above the cards, clear of the header. */
#trouble {
  position: fixed; z-index: 120; left: 50%; translate: -50% 0;
  top: calc(56px + env(safe-area-inset-top));   /* clear of the header */
  display: grid; gap: 8px; justify-items: center;
  width: min(92vw, 420px); pointer-events: none;
}
#trouble:empty { display: none; }

.alert {
  pointer-events: auto;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  justify-content: center; text-align: center;
  padding: 11px 14px; border-radius: 12px;
  background: #16181ff2; border: 1px solid #ffffff24;
  color: var(--ink); font-size: 12.5px; line-height: 1.45;
  box-shadow: 0 8px 26px #00000070;
  animation: rise .3s var(--ease) both;
}
.alert .btn { padding: 7px 13px; font-size: 11.5px; }


/* The result sits over the map, so the credit comes up above it rather
   than going away — the map is still on screen and still needs crediting. */
body.finished:not(.result-away) #mapcredit { z-index: 11; }
