/* Luten löp · one big board you drag and zoom around.
   Off-white paper wall, near-black ink, one blue (the fish's own #153dcc).
   One typeface for everything: Fraunces, tuned per role through its weight, SOFT, WONK and opsz axes.
   Polaroid captions are the one exception: handwriting (Caveat), because they are written on the photo. */

/* ---------- fonts (self-hosted latin subsets) ----------
   The @font-face rules live in the page, not here: index.html names assets/fonts/, and the hosted wall (guestbook,
   src/app/w/[org]/route.ts) names the service's own /fonts/ files, the address its event page uses too, so the
   hand-over from the wall to an event finds Fraunces in the cache. Declared here as well, Chrome fetched both
   addresses (a second @font-face with the same descriptors does not stop the first one's download). */

/* ---------- tokens ---------- */
:root {
  color-scheme: light;
  --bg: #f8f4e5;
  --ink: #141414;
  --ink-2: #6b6560;
  --ink-3: #9a938b;
  --blue: #153dcc;
  --blue-deep: #1132ad;
  --on-blue: #faf2e6;

  --font-serif: "Fraunces", Georgia, "Times New Roman", serif;
  --font-hand: "Caveat", "Bradley Hand", "Segoe Print", cursive;
  /* Fraunces voices */
  --voice-text: "SOFT" 30, "WONK" 0, "opsz" 14;
  --voice-display: "SOFT" 100, "WONK" 1, "opsz" 144;
  --voice-wall: "SOFT" 100, "WONK" 1, "opsz" 72;
  --voice-label: "SOFT" 0, "WONK" 0, "opsz" 9;

  /* The wall's own hover timing, as members know it: --dur-fast for colour, --dur for lifts and frames. */
  --ease-out: cubic-bezier(.22, 1, .36, 1);
  --dur-fast: 120ms;
  --dur: 200ms;
  /* The guest book's motion scale under the guest book's names (its globals.css), for new motion here. --duration-fast
     is 250 ms and is not --dur-fast: the two names never stand for each other. The springs are replaced by linear()
     below where the engine has it; the closed form for JS is LUTEN.spring (js/board.js). */
  --duration-stagger: 40ms;
  --duration-micro: 80ms;
  --duration-quick: 150ms;
  --duration-fast: 250ms;
  --duration-medium: 350ms;
  --duration-slow: 400ms;
  --duration-very-slow: 500ms;
  --ease-smooth-out: var(--ease-out);
  --ease-spring: var(--ease-out);
  --ease-spring-soft: var(--ease-out);
  --scale-press: .97; /* a pill pressed; 1 with reduced motion */
  --fish-size: 116px;
  /* The wall: 2200 × 1500 on every screen, the size the guest book's editor and arranger lay it out on, so a laptop, a
     big monitor and a phone show one arrangement (a text laid on a picture stays on it). A screen larger than the wall
     at its zoom sees paper round it (.sizer). It used to grow with the screen (170vw × 170vh), which spread the places
     while the sizes stayed: 3264 × 1836 on a 1920 × 1080 screen. */
  --world-w: 2200px;
  --world-h: 1500px;
  --zoom: 1;
}
/* A damped spring's step response sampled at 37 stops: --ease-spring (ζ .8, 1.5 % overshoot, for moves and presses),
   --ease-spring-soft (ζ 1, no overshoot, for size changes). The same strings as the guest book's globals.css. */
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --ease-spring: linear(0, 0.024, 0.086, 0.17, 0.266, 0.366, 0.463, 0.555, 0.638, 0.712, 0.776, 0.83, 0.875, 0.911, 0.941, 0.963, 0.981, 0.993, 1.002, 1.009, 1.012, 1.014, 1.015, 1.015, 1.014, 1.013, 1.011, 1.01, 1.008, 1.007, 1.006, 1.005, 1.004, 1.003, 1.002, 1.001, 1);
    --ease-spring-soft: linear(0, 0.028, 0.094, 0.18, 0.274, 0.367, 0.455, 0.536, 0.608, 0.671, 0.726, 0.773, 0.812, 0.846, 0.873, 0.897, 0.916, 0.932, 0.944, 0.955, 0.964, 0.971, 0.976, 0.981, 0.985, 0.988, 0.99, 0.992, 0.994, 0.995, 0.996, 0.997, 0.997, 0.998, 0.998, 0.999, 1);
  }
}

/* ---------- base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  overflow: hidden;
  color: var(--ink);
  font-family: var(--font-serif);
  font-weight: 500;
  font-variation-settings: var(--voice-text);
  font-size: 16px;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
  background: var(--bg);
}
a { color: inherit; }
a:focus-visible, button:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
h1, p { margin: 0; }
/* small labels: Fraunces at text optical size, set as spaced capitals */
.mono {
  font-family: var(--font-serif);
  font-weight: 600;
  font-variation-settings: var(--voice-label);
  font-size: 11px;
  letter-spacing: .1em;
  text-transform: uppercase;
  line-height: 1.2;
}
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ---------- the canvas ---------- */
.viewport {
  position: fixed;
  inset: 0;
  overflow: auto;
  scrollbar-width: none;
  overscroll-behavior: none;
  /* Paper grain lives on the viewport so it stays the same grain at every zoom; it scrolls with the wall. */
  background-color: var(--bg);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.07'/%3E%3C/svg%3E");
  background-attachment: local;
}
.viewport::-webkit-scrollbar { display: none; }
/* The zoom (js/board.js setZoom) is read by the sizer and the wall alone and written on both: registered as not
   inherited, a zoom step (every move of a pinch, every frame of a button's glide) restyles those two elements instead
   of everything on the wall. An engine without @property inherits it from :root's 1 and the elements' own values. */
@property --zoom { syntax: "<number>"; inherits: false; initial-value: 1; }
/* The sizer is the scrollable extent (the wall × zoom, never smaller than the screen); the wall is scaled inside it.
   --edge-x / --edge-y (js/board.js fitEdges) widen it by the overhang of a pin hung past the wall's left or top edge,
   on both sides so the wall stays centred: a scroll container has no negative positions, so without that paper such a
   pin could never be brought on screen. A wall with nothing hanging over is exactly the wall. */
.sizer {
  position: relative;
  width: max(calc((var(--world-w) + 2 * var(--edge-x, 0px)) * var(--zoom)), 100%);
  height: max(calc((var(--world-h) + 2 * var(--edge-y, 0px)) * var(--zoom)), 100%);
}
.world {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--world-w);
  height: var(--world-h);
  transform: translate(-50%, -50%) scale(var(--zoom));
  background: radial-gradient(closest-side at 54% 52%, #153dcc0e, transparent 70%) 50% 50% / 90% 90% no-repeat;
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}
/* Wall templates (from the guest book's wall settings): paper only, paper + drawn map (default), or a photo. */
.world[data-template="paper"] .decor { display: none; }
.world[data-template="photo"] { background-size: cover; background-position: 50% 50%; background-repeat: no-repeat; }
.world[data-template="photo"] .decor { display: none; }
.world[data-template="photo"] .world__title { color: #fff; text-shadow: 0 1px 2px #00000066, 0 8px 30px #00000059; }
/* Dragging must pan the board, never lift a photo or link out of it. */
.world a, .world img { -webkit-user-drag: none; user-drag: none; }
/* The wall is the first Tab stop. It is bigger than the screen, so its focus ring is drawn on the viewport that scrolls
   it, at the screen's edge, and only for keyboard focus (a click on the wall focuses it too, silently). */
.world:focus { outline: none; }
.viewport:has(> .sizer > .world:focus-visible) { outline: 3px solid var(--blue); outline-offset: -3px; }
.world[data-panning="true"] { cursor: grabbing; }
/* Only anchors carry their own UA cursor; everything else inherits, so no `*` (it recalculated the whole world per toggle). */
.world[data-cursor-active="true"], .world[data-cursor-active="true"] a { cursor: none !important; }

.world__title {
  position: absolute;
  z-index: 1; /* above the map (.decor, z 0); pins and the fish are higher still */
  left: 50%;
  top: 46%;
  width: 640px;
  transform: translate(-50%, -50%);
  text-align: center;
  font-family: var(--font-serif);
  font-size: 56px; /* wall px, like everything on the wall: the editor draws it the same (guest book wall.css .we-title) */
  font-weight: 500;
  font-variation-settings: var(--voice-display);
  line-height: 1;
  letter-spacing: -.02em;
  text-wrap: balance;
  /* A word longer than the box (the guest book takes 80 characters, spaces or not) breaks inside itself instead of
     running out of the box and off the screen: on a computer past the 640 px box, on a phone past js/board.js
     fitTitle's 24 px wrap. Words that fit are never broken. */
  overflow-wrap: anywhere;
  pointer-events: none;
  user-select: none;
}

/* ---------- the map under the board (js/decor.js): a hand-drawn paper map straight on the paper ----------
   One svg covering the wall, z 0, under the title (1), pins (their z × 1000 + their place on the board, js/board.js
   stackOf), fish (100000) and marker (100001).
   One ink weight everywhere (the stamps use non-scaling strokes, so a big pine and a small tuft share a pen);
   widths are in wall px and give back a little when zoomed out so the map still reads as a whole at 40 % instead of
   fading to hairlines. The zoom they follow is the settled one (--decor-zoom, written by js/board.js once a zoom has
   come to rest), so a zoom step does not restyle the whole map. */
.decor {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  fill: none;
  stroke: var(--ink);
  stroke-opacity: .34;
  stroke-width: calc(1.1px + .5px / var(--decor-zoom, 1));
  stroke-linecap: round;
  stroke-linejoin: round;
}
.decor .fill { fill: var(--ink); fill-opacity: .3; stroke: none; }
.decor__trail {
  stroke-dasharray: calc(4px + 2px / var(--decor-zoom, 1)) calc(5px + 2px / var(--decor-zoom, 1));
  stroke-width: calc(1.25px + .55px / var(--decor-zoom, 1));
  stroke-opacity: .38;
}
.decor__lake { stroke: var(--blue); stroke-opacity: .35; }
.decor__water { fill: var(--blue); fill-opacity: .08; }

/* ---------- pins ---------- */
.pin {
  position: absolute;
  z-index: var(--z, 1);
  left: var(--x);
  top: var(--y);
  width: var(--w);
  margin: 0;
  transform: rotate(var(--rot, 0deg));
  transform-origin: 50% 50%;
}
.pin__hit {
  position: relative;
  display: block;
  color: inherit;
  text-decoration: none;
  transition: transform var(--dur) var(--ease-out);
}
.pin:hover .pin__hit, .pin:focus-within .pin__hit { transform: translateY(-3px); }
/* A pressed picture dips in 80 ms and springs back to its hover lift at the base 200 ms. A drag that began on it
   un-presses as soon as board.js marks the pan; the editor's drag keeps its own pick-up (css/events.css), and an
   invitation card (a div holding a form) stays still while someone types. The dip is the tap's answer on a phone, so
   the browser's grey tap rectangle goes where the dip plays; with reduced motion there is no dip and it stays. */
@media (prefers-reduced-motion: no-preference) {
  .world:not([data-edit="true"]) .pin a.pin__hit, .world:not([data-edit="true"]) .pin[data-perk-sponsor] > .pin__hit { -webkit-tap-highlight-color: transparent; }
  .world:not([data-edit="true"]) .pin a.pin__hit:active, .world:not([data-edit="true"]) .pin[data-perk-sponsor] > .pin__hit:active { transform: translateY(-1px) scale(.985); transition-duration: var(--duration-micro); }
  .world[data-panning="true"]:not([data-edit="true"]) .pin a.pin__hit:active, .world[data-panning="true"]:not([data-edit="true"]) .pin[data-perk-sponsor] > .pin__hit:active { transform: translateY(-3px); }
}
/* A partner with a perk is a button, the pin itself (js/board.js sponsor): its focus ring goes on the face that lifts,
   where a link's is. */
.pin[data-perk-sponsor]:focus-visible { outline: none; }
.pin[data-perk-sponsor]:focus-visible > .pin__hit { outline: 2px solid var(--blue); outline-offset: 3px; }

/* polaroid · Polaroid 600 proportions: 88.5 × 107.5 mm frame, 79 × 77 mm picture,
   4.8 mm sides, 6.1 mm top, 24.6 mm chin. All paddings are % of the frame width.
   The frame's gradient is drawn from three registered colours, so the hover's blue fades in with the caption instead
   of cutting (a gradient itself does not interpolate: `transition: background` did nothing). The frame declares its
   resting colours itself: an engine without @property (Safari before 16.4, Firefox before 128) has no initial value
   for them, and without them every frame there would lose its paper and turn transparent. Not as var() fallbacks:
   with fallbacks in the gradient, Chrome kept painting the resting paper for the whole 200 ms and cut to blue at the
   end, while the caption faded to cream on white (sampled per frame on the hosted wall, Chrome 154). */
@property --pf-a { syntax: "<color>"; inherits: false; initial-value: #ffffff; }
@property --pf-b { syntax: "<color>"; inherits: false; initial-value: #fbfaf7; }
@property --pf-c { syntax: "<color>"; inherits: false; initial-value: #f4f2ec; }
.polaroid {
  position: relative;
  display: block;
  container-type: inline-size;
  padding: 6.9% 5.4% 27.8%;
  border-radius: 3px;
  --pf-a: #ffffff;
  --pf-b: #fbfaf7;
  --pf-c: #f4f2ec;
  background: linear-gradient(165deg, var(--pf-a) 0%, var(--pf-b) 55%, var(--pf-c) 100%);
  color: #2a2a2a;
  box-shadow:
    0 0 0 .5px #00000014,
    0 1px 2px #0000001f,
    0 12px 24px -10px #00000047;
  transition: --pf-a var(--dur) var(--ease-out), --pf-b var(--dur) var(--ease-out), --pf-c var(--dur) var(--ease-out), color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}
.polaroid__photo {
  position: relative;
  display: block;
  aspect-ratio: 79 / 77;
  overflow: hidden;
  border-radius: 1px;
  background: #2b2723;
}
.polaroid__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: contrast(1.04) saturate(.9) sepia(.06);
}
/* A picture still on its way (js/board.js develop) waits unseen over the dark well; a slow one develops when it arrives
   (a stamped cover up to its own .5), a quick one just appears. The card logo keeps its hover plate's timing. */
.pin img.is-developing { opacity: 0; transition: none; }
.pin img.is-develop-fade { transition: opacity 400ms ease-out, background-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out), border-radius var(--dur) var(--ease-out); }
/* The picture sits a hair below the frame: a dark recessed edge, a soft vignette and one glossy sheen. */
.polaroid__photo::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(168deg, #ffffff2b 0%, transparent 32%),
    radial-gradient(115% 100% at 50% 42%, transparent 58%, #00000033 100%);
  box-shadow: inset 0 0 0 1px #00000042, inset 0 1px 3px #00000047;
}
.polaroid figcaption {
  position: absolute;
  left: 7%;
  right: 7%;
  bottom: 8.5%;
  text-align: center;
  font-family: var(--font-hand);
  font-weight: 500;
  font-variation-settings: normal;
  font-size: clamp(14px, 11cqi, 26px);
  line-height: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pin:hover .polaroid, .pin:focus-within .polaroid {
  --pf-a: #1f49e0;
  --pf-b: var(--blue);
  --pf-c: var(--blue-deep);
  color: var(--on-blue);
  box-shadow: 0 0 0 .5px #0000001f, 0 2px 3px #00000024, 0 16px 30px -10px #153dcc73;
}

/* A partner with a photo (js/board.js sponsor): a polaroid like the club's own pictures, whose chin is a strip of the
   chin's height (27.8 % of the frame's width, which is the aspect ratio's 278 to the picture's 892) holding the logo in
   its own colours, or the name in the caption's hand. Under the pointer the frame turns blue as every polaroid's does
   and the logo keeps its colours on the card's white plate (.card__logo), grown by 2.5 % of the picture's width. */
.polaroid--partner { padding-bottom: 0; }
.polaroid__strip { display: flex; align-items: center; justify-content: center; aspect-ratio: 892 / 278; }
.polaroid__logo { display: block; width: 72%; height: 58%; object-fit: contain; border-radius: 2px; background-color: #fff0; box-shadow: 0 0 0 2.5cqi #fff0; transition: background-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out); }
.pin:hover .polaroid__logo, .pin:focus-within .polaroid__logo { background-color: #fff; box-shadow: 0 0 0 2.5cqi #fff; }
.polaroid__name { max-width: 90%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-hand); font-weight: 500; font-variation-settings: normal; font-size: clamp(14px, 11cqi, 26px); line-height: 1.15; }
/* The perk's badge (js/board.js sponsor) is an upcoming run's (css/events.css .badge--go, blue, cream under the
   pointer): on a polaroid, the chin's first child, astride the seam between the photo and the chin at the left, so it
   covers only the photo's bottom edge, where a framed face never is (top left on the photo it sat on the sponsor's
   face); the chin keeps its height, and its logo takes half of it below a little room for the badge. On the card, in
   the card's own fixed sizes, between the label and the logo. */
.pin--sponsor .badge--perk { left: 5%; right: auto; }
.polaroid__strip { position: relative; }
.polaroid__strip > .badge--perk { top: 0; transform: translateY(-50%); }
.pin[data-perk] .polaroid__strip { padding-top: 4%; }
.pin[data-perk] .polaroid__logo { height: 50%; }
.card .badge--perk { position: relative; display: block; width: fit-content; left: auto; top: auto; margin: 0 auto 8px; font-size: 10px; }
/* The stamp that says whose picture it is, lower right on the photo: a past run's stamp (css/events.css .badge--stamp),
   smaller and quieter. Above the photo's sheen, as the badges are. */
.partner-stamp {
  position: absolute;
  z-index: 2;
  right: 5%;
  bottom: 5%;
  padding: .22em .45em .14em;
  border: 1.5px solid var(--blue);
  border-radius: 4px;
  background: #f8f4e5e6;
  color: var(--blue);
  font-family: var(--font-serif);
  font-weight: 700;
  font-variation-settings: var(--voice-label);
  font-size: clamp(8px, 4.6cqi, 12px);
  letter-spacing: .08em;
  line-height: 1.2;
  text-transform: uppercase;
  white-space: nowrap;
  transform: rotate(-4deg);
}

/* free text written straight on the wall */
.text {
  position: absolute;
  z-index: var(--z, 2);
  left: var(--x);
  top: var(--y);
  width: var(--w);
  margin: 0;
  transform: rotate(var(--rot, 0deg));
  transform-origin: 0 50%;
  font-size: var(--size, 26px);
  line-height: 1.15;
  color: var(--ink);
  text-decoration: none;
  text-wrap: balance;
}
.text--serif { font-family: var(--font-serif); font-weight: 500; font-variation-settings: var(--voice-wall); letter-spacing: -.01em; }
.text--mono { font-family: var(--font-serif); font-weight: 600; font-variation-settings: var(--voice-label); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2); line-height: 1.5; }
.text--hand { font-family: var(--font-hand); font-weight: 500; font-variation-settings: normal; }
a.text { transition: color var(--dur-fast); }
a.text:hover, a.text:focus-visible { color: var(--blue); }

/* partner card */
.card {
  display: block;
  padding: 12px 12px 14px;
  border-radius: 2px;
  background: #fffdf9;
  color: var(--ink);
  text-align: center;
  box-shadow: 0 0 0 .5px #00000014, 0 1px 2px #0000001a, 0 10px 20px -10px #00000040;
  transition: background-color var(--dur) var(--ease-out), color var(--dur) var(--ease-out);
}
.card__label { display: block; color: var(--ink-2); margin-bottom: 6px; } /* --ink-3 read 3.0:1 on the card; this is 5.7:1 */
/* The card turns blue under the pointer and its logo keeps its own colours, on a small white plate, as a polaroid's
   photo keeps its colours when its frame turns blue. The logo used to go brightness(0) invert(1), which drew any
   opaque picture (a photo, a logo on its own background) as a white slab. The plate is the logo's own box, filled and
   grown 5 px by a spread shadow, both transparent at rest, and its corners round only with it, so the card at rest is
   as it was (a logo that fills its box keeps its square corners) and only the plate fades in, with the card. */
.card__logo { display: block; width: 100%; height: auto; aspect-ratio: 12 / 5; object-fit: contain; background-color: #fff0; box-shadow: 0 0 0 5px #fff0; transition: background-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out), border-radius var(--dur) var(--ease-out); }
.card__text { display: block; margin-top: 6px; font-size: 13px; color: var(--ink-2); }
/* A partner without a logo yet: the name set large, in the logo's place. */
.card__name { display: grid; place-items: center; width: 100%; aspect-ratio: 12 / 5; font-family: var(--font-serif); font-size: 22px; font-weight: 700; letter-spacing: -.01em; font-variation-settings: var(--voice-display); text-align: center; text-wrap: balance; }
.pin:hover .card, .pin:focus-within .card { background: var(--blue); color: var(--on-blue); }
.pin:hover .card__label, .pin:hover .card__text, .pin:focus-within .card__label, .pin:focus-within .card__text { color: inherit; opacity: .85; }
.pin:hover .card__logo, .pin:focus-within .card__logo { border-radius: 3px; background-color: #fff; box-shadow: 0 0 0 5px #fff; }

/* ---------- the destination marker (this is the cursor inside the board) ---------- */
.target {
  position: absolute;
  z-index: 100001; /* above every pin (js/board.js stackOf tops out well under this) */
  left: 0;
  top: 0;
  width: 14px;
  height: 14px;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
/* The ring itself is the pseudo-element, so a press can scale it: the kit writes the marker's own transform on every
   move (translate3d), and a scale on the marker would scale that offset too and throw the ring across the wall. Inside
   the wall the ring is the cursor (the native one is hidden), so it grips while the wall is pressed, for a whole drag,
   and springs back in 150 ms. */
.target::before {
  content: "";
  position: absolute;
  inset: 0;
  border: 1.5px solid #153dcca6;
  border-radius: 50%;
  background: transparent;
  transition: background-color var(--dur-fast) ease-out, border-color var(--dur-fast) ease-out, scale var(--duration-quick) var(--ease-out);
}
.target[data-arrived="true"]::before { background: var(--blue); border-color: var(--blue); }
@media (prefers-reduced-motion: no-preference) {
  .world:active .target::before { scale: .72; transition-duration: var(--duration-micro); }
}

/* ---------- the fish (anchor is 50% across and 93.75% down each frame) ---------- */
.traveller { position: absolute; z-index: 100000; left: 0; top: 0; pointer-events: none; }
.traveller::before {
  content: "";
  position: absolute;
  left: 36%;
  top: 91.5%;
  width: 28%;
  height: 5%;
  border-radius: 50%;
  background: #0a0a0a1a;
  pointer-events: none;
}
.fish-attitude { transform-origin: 50% 93.75%; transition: transform 140ms var(--ease-out); }
.fish-celebration { transform-origin: 50% 93.75%; }
.fish-celebration.is-celebrating { animation: fish-arrival 560ms linear; }
.fish-celebration.is-celebrating .luten-fish { animation: fish-jump-feet 560ms steps(1, end); }
.traveller[data-celebrating="true"]::before { animation: fish-arrival-shadow 560ms linear; }
.luten-fish {
  position: relative;
  display: block;
  width: var(--fish-size, 144px);
  aspect-ratio: 9 / 7;
  flex: none;
  background-repeat: no-repeat;
  background-size: 1400% 1800%;
  background-position: 0 0;
  transform-origin: 50% 93.75%;
  pointer-events: none;
  user-select: none;
}
.luten-fish[data-facing="left"] { transform: scaleX(-1); }
.world[data-fish="off"] .traveller, .world[data-fish="off"] .target { display: none; }
@keyframes fish-arrival {
  0%, 100% { transform: translateY(0) scale(1) rotate(0); }
  14% { transform: translateY(0) scale(1.035, .95) rotate(0); }
  36% { transform: translateY(calc(var(--fish-size) * -.061)) scale(.99, 1.02) rotate(-1.5deg); }
  50% { transform: translateY(calc(var(--fish-size) * -.074)) scale(1) rotate(-2deg); }
  64% { transform: translateY(calc(var(--fish-size) * -.052)) scale(1) rotate(-.5deg); }
  78% { transform: translateY(0) scale(1.04, .96) rotate(.5deg); }
}
@keyframes fish-arrival-shadow {
  0%, 100% { transform: scale(1); opacity: 1; }
  14%, 78% { transform: scale(1.035); opacity: 1; }
  50% { transform: scale(.88); opacity: .65; }
}
@keyframes fish-jump-feet {
  0% { background-position: 61.53846154% 100%; }
  20% { background-position: 69.23076923% 100%; }
  32% { background-position: 76.92307692% 100%; }
  44% { background-position: 84.61538462% 100%; }
  58% { background-position: 92.30769231% 100%; }
  70%, 100% { background-position: 100% 100%; }
}

/* ---------- fixed corner overlays ---------- */
.hud { position: fixed; z-index: 10; margin: 0; }
.hud--wordmark {
  top: 16px;
  left: 20px;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}
/* On a club's own photo (the photo template) the name has no paper under it and may land on anything: a wash of paper
   behind it keeps it readable whatever the picture. The pills and the zoom control carry their own paper already. */
body:has(.world[data-template="photo"]) .hud--wordmark { margin: -4px 0 0 -8px; padding: 4px 12px 4px 8px; border-radius: 999px; background: #ffffffc8; box-shadow: 0 0 0 .5px #00000014, 0 2px 6px -2px #00000026; }
.hud__fish { display: block; width: 30px; height: auto; }
/* The wordmark is baked vector art (Fraunces 900, soft, wonky, display optical size): assets/logo/luten-lopp-wordmark.svg */
.hud__wordmark { display: block; height: 22px; width: auto; }
.hud--instagram {
  top: 16px;
  right: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--bg);
  text-decoration: none;
  transition: background-color var(--dur-fast), scale var(--duration-quick) var(--ease-out);
}
.hud--instagram svg { display: block; }
@media (hover: hover) { .hud--instagram:hover { background: var(--blue); color: var(--on-blue); } } /* on a phone the circle stayed blue after the tap */
.hud--zoom {
  left: 20px;
  bottom: 16px;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 999px;
  background: #fcfbf6;
  box-shadow: 0 0 0 .5px #00000014, 0 2px 6px -2px #00000026;
  color: var(--ink-2);
}
.hud--zoom button {
  min-width: 30px;
  height: 30px;
  padding: 0 8px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font-family: var(--font-serif);
  font-size: 13px;
  font-weight: 600;
  font-variation-settings: var(--voice-text);
  cursor: pointer;
  transition: scale var(--duration-quick) var(--ease-out);
}
@media (hover: hover) { .hud--zoom button:hover { background: #00000010; color: var(--ink); } } /* a tapped zoom button kept its grey on a phone */
.hud--zoom #zoom-reset { min-width: 54px; font-variant-numeric: tabular-nums; }
/* The HUD's paper is opaque (#fcfbf6, the old 66 % white over the wall's paper): a card or a photo under a corner used
   to show through the zoom and the pills, and "100%" printed over a run's "1 PLATS KVAR" read as neither. On the photo
   template 13 px --ink-2 on that paper still sits beside a dark picture, so it takes full ink, 13:1 (README, Kontrast). */
body:has(.world[data-template="photo"]) .hud--zoom { color: var(--ink); }
.hud--noscript { left: 20px; bottom: 60px; font-size: 14px; color: var(--ink-2); }
/* Bottom right: the next event and the club's calendar, filled by js/board.js from the guest book's board. Paper pills in
   the zoom control's family; the next-event pill is blue, the wall's one call to action. */
.hud--pills { right: 20px; bottom: 16px; display: inline-flex; align-items: center; gap: 8px; }
.hud--pills[hidden] { display: none; }
.hud__pill {
  display: inline-flex;
  align-items: center;
  height: 36px;
  padding: 0 14px;
  border-radius: 999px;
  background: #fcfbf6;
  box-shadow: 0 0 0 .5px #00000014, 0 2px 6px -2px #00000026;
  color: var(--ink);
  text-decoration: none;
  font-family: var(--font-serif);
  font-size: 14px;
  font-weight: 600;
  font-variation-settings: var(--voice-text);
  line-height: 1;
  white-space: nowrap;
  transition: background-color var(--dur-fast), color var(--dur-fast), scale var(--duration-quick) var(--ease-out);
}
.hud__pill[hidden] { display: none; }
.hud__pill--next { background: var(--blue); color: var(--on-blue); box-shadow: 0 2px 6px -2px #153dcc66; }
/* "Nästa: Tisdagsrunda, tis 29 sep" (js/board.js updatePills, the hosted route's pillsHtml): the title gives way with an
   ellipsis where the pill runs out of room, the day never does. `pre` keeps the space after "Nästa:", which ends a flex
   item of its own. At most about 420 px on a computer; on a phone the pill has its own row above Kalender, as wide as
   the screen allows (and clear of a Free wall's "byggd med Eveny", which shares that row). */
.hud__pill--next { max-width: 420px; white-space: pre; }
.hud__pill-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.hud__pill-when { flex: none; }
@media (hover: hover) { .hud__pill:hover { background: var(--ink); color: var(--bg); } }
/* The HUD's controls dip when pressed (80 ms in, 150 ms back); their hover colours keep their 120 ms and, like every
   hover here, come only where there is a pointer: on a phone a tapped pill kept its ink after the tap, and the press
   is its answer, instead of the browser's grey tap rectangle, which was square over the round pills (with reduced
   motion there is no dip, and the rectangle stays). The hosted wall's own controls (About, Strava, the corner mark) do
   the same in its inline CSS. */
@media (prefers-reduced-motion: no-preference) {
  .hud--instagram, .hud--zoom button, .hud__pill { -webkit-tap-highlight-color: transparent; }
  .hud--instagram:active, .hud--zoom button:active, .hud__pill:active { scale: .96; transition-duration: var(--duration-micro); }
}

/* ---------- phones: narrow, or short (a phone on its side) ---------- */
@media (max-width: 719px), (max-height: 499px) {
  /* The title keeps to one line; js/board.js fitTitle sizes it to the screen, and 50 px (36 on screen at rest) holds
     until it has run. A phone shows the computer's wall, every picture, text and card at its place and its size, seen
     from further away: its resting zoom is PHONE_ZOOM in js/board.js, one factor for everything, so the fish (116 wall
     px) stands at his old 84 px. The old phone positions (mx/my) are not read. The query is js/board.js `narrow`'s:
     a phone on its side is wider than 719 px but under 500 px tall, and took a computer's wall at 100 %, seen through
     a letterbox 390 px tall. */
  .world__title { width: max-content; white-space: nowrap; font-size: 50px; }
  .hud--wordmark { top: 14px; left: 14px; gap: 8px; }
  .hud__fish { width: 26px; }
  .hud__wordmark { height: 19px; }
  .hud--instagram { top: 12px; right: 14px; width: 36px; height: 36px; }
  .hud--zoom { left: 14px; bottom: 14px; }
  .hud--pills { right: 14px; bottom: 14px; flex-direction: column; align-items: flex-end; gap: 6px; }
  .hud__pill { height: 34px; padding: 0 12px; font-size: 13px; }
  .hud__pill--next { max-width: calc(100vw - 28px); }
  body:has(.hud--mark) .hud__pill--next { max-width: calc(100vw - 185px); }
}

/* ---------- reduced motion ---------- */
/* Travel goes, colour stays: no hover lift, but frames, cards and links still fade to blue (the guest book's policy). */
@media (prefers-reduced-motion: reduce) {
  :root { --scale-press: 1; }
  .world[data-cursor-active="true"], .world[data-cursor-active="true"] a { cursor: auto !important; }
  .fish-attitude { transition: none; }
  .pin:hover .pin__hit, .pin:focus-within .pin__hit { transform: none; }
  .pin img.is-develop-fade { transition: opacity 150ms ease-out, background-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out), border-radius var(--dur) var(--ease-out); }
  .fish-celebration.is-celebrating, .fish-celebration.is-celebrating .luten-fish, .traveller[data-celebrating="true"]::before { animation: none; }
}
