/* ════════════════════════════════════════════════════════════════════
   Bir Davet — 15 Eylül
   Mobile-first, portrait, one-handed. Desktop is an afterthought.
   No external requests except the three self-hosted fonts.
   ════════════════════════════════════════════════════════════════════ */

/* ── fonts ────────────────────────────────────────────────────────── */
/* Grenze Gotisch + Grenze (Omnibus-Type, OFL). Subset to Latin Ext-A so
   the Turkish glyphs ş ğ ı İ ç ö ü survive any rewrite of the copy.
   Regenerate with build/build-fonts.sh. */
@font-face {
  font-family: "Grenze Gotisch";
  src: url("fonts/grenze-gotisch-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Grenze";
  src: url("fonts/grenze-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Grenze";
  src: url("fonts/grenze-600.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
/* The fishing hole's font, two Google subsets so the Turkish letters are
   covered -- and one screen of Turkish needs both of them. Only that screen
   uses it, so it is not in the <head> with the fonts the gate waits on;
   warmPixelify() in app.js pulls both subsets once the gate has opened. */
@font-face {
  font-family: "Pixelify Sans";
  src: url("fonts/pixelify-latin.woff2") format("woff2");
  font-weight: 400 700; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Pixelify Sans";
  src: url("fonts/pixelify-latin-ext.woff2") format("woff2");
  font-weight: 400 700; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+2020, U+20A0-20AB, U+20AD-20C0;
}

/* ── tokens ───────────────────────────────────────────────────────── */
:root {
  --ink:        #0a0708;
  --ink-2:      #140b0f;
  --ink-3:      #1d1116;

  --oxblood:    #5e1420;
  --crimson:    #8b1a2b;
  --crimson-lt: #a8202f;

  --bone:       #e6dccb;
  --bone-dim:   #a9a091;
  --gold:       #c9a961;
  --gold-dim:   #8a7440;

  --flame-warm: #ffb648;

  --display: "Grenze Gotisch", "Palatino Linotype", Palatino, Georgia, serif;
  --body:    "Grenze", Georgia, "Times New Roman", serif;

  --pad: clamp(1.1rem, 5vw, 2rem);
  --safe-b: env(safe-area-inset-bottom, 0px);

  --room-light: 1;
}

/* ── reset ────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--ink);
  color: var(--bone);
  font-family: var(--body);
  font-size: clamp(1rem, 4.1vw, 1.15rem);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  overflow: hidden;                 /* one screen at a time, never scrolls */
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
}
button { font: inherit; color: inherit; background: none; border: 0; }
h1, h2, p, figure, ol, li { margin: 0; }
ol { list-style: none; padding: 0; }
img { max-width: 100%; display: block; }

/* ── ground: tiled damask + vignette ──────────────────────────────── */
.ground {
  position: fixed; inset: 0; z-index: 0;
  background-color: var(--ink);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44' viewBox='0 0 44 44'%3E%3Cg fill='none' stroke='%238b1a2b' stroke-width='1.1'%3E%3Cpath d='M22 5C29 12 29 17 22 22C15 17 15 12 22 5Z'/%3E%3Cpath d='M22 39C29 32 29 27 22 22C15 27 15 32 22 39Z'/%3E%3Cpath d='M5 22C12 15 17 15 22 22C17 29 12 29 5 22Z'/%3E%3Cpath d='M39 22C32 15 27 15 22 22C27 29 32 29 39 22Z'/%3E%3C/g%3E%3Cg fill='%238b1a2b'%3E%3Ccircle cx='22' cy='22' r='1.7'/%3E%3Ccircle cx='0' cy='0' r='2.3'/%3E%3Ccircle cx='44' cy='0' r='2.3'/%3E%3Ccircle cx='0' cy='44' r='2.3'/%3E%3Ccircle cx='44' cy='44' r='2.3'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 44px 44px;
  opacity: .17;
  filter: brightness(var(--room-light));
  transition: opacity 1.6s ease, filter 1.6s ease;
}
.vignette {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(120% 78% at 50% 34%, rgba(139,26,43,.10), transparent 62%),
    radial-gradient(105% 88% at 50% 46%, transparent 34%, rgba(4,2,3,.78) 88%, #040203 100%);
  transition: opacity 1.6s ease;
}

/* ── y2k furniture: corner cobwebs, drifting dust ─────────────────── */
/* Both live behind the stage and never take a pointer event. The charm of a
   hand-made page is that it is over-decorated; the discipline is that none of
   it can be in the way. */
.cobwebs, .motes {
  position: fixed; inset: 0; z-index: 1;
  pointer-events: none;
  overflow: hidden;
}
.cobweb {
  position: absolute; top: 0;
  width: clamp(66px, 23vw, 118px); height: auto;
  opacity: .68;
}
.cobweb--l { left: 0; }
.cobweb--r { right: 0; }

.motes i {
  position: absolute; bottom: -10px;
  width: 3px; height: 3px;
  border-radius: 50%;
  background: rgba(201,169,97,.55);
  filter: blur(.4px);
  animation: mote-rise linear infinite;
}
.motes i:nth-child(1) { left:  9%; animation-duration: 21s; animation-delay:  -3s; }
.motes i:nth-child(2) { left: 27%; animation-duration: 17s; animation-delay:  -9s; }
.motes i:nth-child(3) { left: 44%; animation-duration: 25s; animation-delay: -14s; }
.motes i:nth-child(4) { left: 61%; animation-duration: 19s; animation-delay:  -6s; }
.motes i:nth-child(5) { left: 78%; animation-duration: 23s; animation-delay: -17s; }
.motes i:nth-child(6) { left: 91%; animation-duration: 15s; animation-delay: -11s; }
@keyframes mote-rise {
  0%   { opacity: 0;   transform: translate(0, 0); }
  14%  { opacity: .75; }
  86%  { opacity: .45; }
  100% { opacity: 0;   transform: translate(20px, -104vh); }
}

/* ── stage / screens ──────────────────────────────────────────────── */
.stage { position: relative; z-index: 2; height: 100%; }
.screen {
  position: absolute; inset: 0;
  display: flex;
  overflow-y: auto;                 /* see the margin:auto note below */
  overscroll-behavior: contain;
  padding: calc(var(--pad) + env(safe-area-inset-top, 0px)) var(--pad)
           calc(var(--pad) + var(--safe-b));
  opacity: 0;
  pointer-events: none;
  /* Drifts up into place instead of a flat cross-fade. `transform` is only
     ever non-none mid-transition, so it never becomes a containing block for
     anything that matters -- the cat is fixed and lives outside .stage. */
  transform: translateY(12px) scale(.994);
  transition: opacity .7s ease, transform .7s cubic-bezier(.22,.9,.3,1);
}
/* Auto margins, not place-items:center. They centre exactly the same while
   the content fits, but they collapse to zero when it does not, so a screen
   that overruns a small phone scrolls from the top instead of having its head
   clipped off. Grid/flex centring clips, and body{overflow:hidden} would make
   that unrecoverable -- Turkish runs long and the longest option in question
   one is three lines wide on a narrow screen. */
.screen > * { margin: auto; flex: 0 0 auto; }
.screen.is-active { opacity: 1; pointer-events: auto; transform: none; }
.screen[hidden] { display: none; }

/* ── type ─────────────────────────────────────────────────────────── */
.display {
  font-family: var(--display);
  font-weight: 500;
  line-height: 1.06;
  letter-spacing: .012em;
  color: var(--bone);
  text-shadow: 0 2px 0 rgba(0,0,0,.85), 0 0 22px rgba(139,26,43,.32);
}
.display--xl { font-size: clamp(3rem, 15.5vw, 4.6rem); }
.display--lg { font-size: clamp(2.3rem, 11.5vw, 3.4rem); }
.display--md { font-size: clamp(1.55rem, 6.6vw, 2.1rem); line-height: 1.18; }

/* Plain Grenze, not the Gotisch: blackletter capitals spaced this wide
   stop being letters, and "BİRİNCİ BÖLÜM" read as a row of ornaments. */
.kicker {
  font-family: var(--body);
  font-weight: 600;
  font-size: clamp(.8rem, 3.5vw, .95rem);
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--gold-dim);
  margin-bottom: .9rem;
}
.lede { color: var(--bone); }
.lede--wish { font-family: var(--display); font-size: clamp(1.5rem,6.6vw,2rem); line-height:1.2; }
.whisper {
  color: var(--bone-dim);
  font-size: .92em;
  line-height: 1.5;
}
.whisper--wide { max-width: 30ch; margin-inline: auto; }
.tiny {
  font-size: .76em;
  color: #6f6459;
  letter-spacing: .04em;
}

/* ornamental rule */
.rule {
  display: flex; align-items: center; justify-content: center;
  gap: .7rem; margin: 1rem 0 1.15rem;
  color: var(--gold-dim); font-size: .8rem;
}
.rule span {
  height: 1px; width: clamp(2.4rem, 16vw, 5rem);
  background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
  animation: rule-breathe 5.2s ease-in-out infinite;
}
.rule span:last-of-type { animation-delay: -2.6s; }
@keyframes rule-breathe {
  0%, 100% { opacity: .5; }
  50%      { opacity: 1; }
}

/* ── frames: visible borders, drop shadows slightly too harsh ─────── */
.frame {
  position: relative;
  border: 1px solid var(--crimson);
  outline: 1px solid rgba(201,169,97,.26);
  outline-offset: 4px;
  background:
    linear-gradient(180deg, rgba(30,14,20,.92), rgba(10,6,8,.95));
  box-shadow: 5px 6px 0 rgba(0,0,0,.72), 0 0 34px rgba(139,26,43,.14) inset;
  padding: clamp(1.2rem, 6vw, 2rem) clamp(1rem, 5.4vw, 1.9rem);
  text-align: center;
}
/* corner ticks — handmade, deliberately not quite aligned */
.frame::before, .frame::after {
  content: "";
  position: absolute; width: 13px; height: 13px;
  border: 1px solid var(--gold-dim);
}
.frame::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
.frame::after  { bottom: -2px; right: -1px; border-left: 0; border-top: 0; }

/* ── buttons ──────────────────────────────────────────────────────── */
.btn {
  display: inline-block;
  position: relative;
  cursor: pointer;
  font-family: var(--display);
  letter-spacing: .07em;
  color: var(--bone);
  background: linear-gradient(180deg, #3a1018, #1a0a0e);
  border: 1px solid var(--crimson-lt);
  box-shadow: 3px 4px 0 rgba(0,0,0,.8);
  padding: .72em 1.65em;
  min-height: 48px;                 /* thumb target */
  transition: transform .09s ease, box-shadow .09s ease, background .25s ease;
}
.btn--major { font-size: clamp(1.25rem, 5.6vw, 1.6rem); margin-top: 1.6rem; }
.btn--minor { font-size: clamp(1.05rem, 4.6vw, 1.25rem); margin-top: 1.3rem; }
.btn:active {
  transform: translate(3px, 4px);
  box-shadow: 0 0 0 rgba(0,0,0,.8);
  background: linear-gradient(180deg, #4a141e, #240c12);
}
.btn[disabled] { opacity: .32; cursor: default; }
/* Every pressable here sinks a few px on :active, and the hit test sinks
   with it: a press that went down on the top or left edge came back up
   outside the button, and the browser sends no click for that -- it looked
   pressed and did nothing. While it is held, this keeps its old footprint
   under the pointer. Only on :active, so at rest no hit area grows. */
.btn:active::after, .option:active::after, .fbtn:active::after {
  content: ""; position: absolute; inset: -8px;
}
.btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* ═══ THE GATE ════════════════════════════════════════════════════ */
/* The photograph is a grey Polish afternoon and this page is a night in
   September, so nothing here is the picture as shot: it is desaturated, pulled
   down two stops, and dissolved into the dark at every edge so it reads as
   something the vignette found rather than a rectangle pasted on top. */
.gate {
  position: absolute; inset: 0;
  z-index: 0;
  overflow: hidden;                 /* the plate is wider than the door */
  pointer-events: none;
  /* the gap between the pillars, where the path goes -- the point she walks
     at, and so the point everything scales away from */
  transform-origin: 55% 50%;
  will-change: transform;
}

/* The photograph does not reach the top and bottom of a portrait phone, and
   what fills the gap should look like a decision rather than a shortfall. So
   it is the sky out of the photograph itself, carried on past the edges of the
   plate: #d4dae2 raw, #c0c3c7 once the plate's own grade is applied to it, a
   cool blue-grey that the warm tint in .gate__fog then marries to the rest of
   the page. Pale at the horizon, night at the top of the frame, ground fog
   going to ink at the bottom -- the order the scene is actually lit in.

   The banks on top of the ramp are what stop it reading as a ramp. They are
   gradients, not a blurred bitmap: this whole element gets re-rasterised on
   every frame of the walk, and a full-viewport blur there is what made the
   transition stutter. Nothing here moves, for the same reason the plate no
   longer drifts. */
.gate__fill {
  position: absolute; inset: 0;
  background:
    radial-gradient(86% 30% at 26% 20%, rgba(198,206,218,.20), transparent 72%),
    radial-gradient(74% 26% at 77% 38%, rgba(198,206,218,.15), transparent 74%),
    radial-gradient(124% 44% at 50% 50%, rgba(184,190,202,.22), transparent 76%),
    linear-gradient(180deg,
      #3f4249  0%,   /* top of the frame: sky, but night */
      #6b7079 14%,   /* meets the photograph's own sky at the plate's edge */
      #7e8288 30%,
      #8e8d8f 48%,   /* the horizon, where fog is always brightest */
      #5c5a58 70%,   /* the ground beyond the gate */
      #2a2724 86%,
      #171513 100%); /* foreground, into ink */
}

/* Sized to the photo's own 4:3 and floated a little high, rather than covering
   the viewport. Cover on a portrait phone crops away both stone pillars and
   leaves an anonymous foggy path -- and the pillars are the whole reason this
   photograph is here. Letterboxing is the honest trade, and the masks mean the
   letterbox is night rather than bars.

   The width never goes below 100%, so the plate is always at least as wide as
   the window and its left and right edges are permanently off-screen. Only the
   horizontal ones ever show, and the sky above handles those.

   Two masks, on two elements, because they have different jobs and one
   gradient cannot do both. The plate kills the photograph's two horizontal
   edges outright; the radial below it is the vignette. Compositing them into
   a single mask would need mask-composite, which is a different keyword on
   webkit -- a nested element is the portable version of the same idea.

   The plate held a slow ken-burns drift for a while. It came out: the frame
   over it is dead still, and a photograph that creeps under a frame that does
   not reads as a rendering fault rather than as atmosphere. */
.gate__plate {
  position: absolute;
  left: 50%; top: 42%;
  width: max(100%, min(152%, 1280px));
  aspect-ratio: 612 / 459;
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0%, #000 11.5%, #000 88%, transparent 100%);
          mask-image: linear-gradient(180deg,
    transparent 0%, #000 11.5%, #000 88%, transparent 100%);
  transform: translate(-50%, -50%);
}
.gate__photo {
  position: absolute; inset: 0;
  background: url("assets/cemetery_entrance.jpg") center / cover no-repeat;
  filter: grayscale(.52) contrast(1.1) brightness(.86) blur(.4px);
  -webkit-mask-image: radial-gradient(88% 76% at 52% 54%, #000 48%, transparent 100%);
          mask-image: radial-gradient(88% 76% at 52% 54%, #000 48%, transparent 100%);
}

/* Fog and vignette in this page's colours, so the photo belongs to the same
   room as the damask behind it. */
.gate__fog {
  position: absolute; inset: 0;
  background:
    radial-gradient(120% 84% at 55% 46%, rgba(139,26,43,.20), transparent 62%),
    linear-gradient(180deg,
      rgba(10,7,8,.72) 0%, rgba(10,7,8,.06) 26%,
      rgba(10,7,8,.14) 56%, rgba(10,7,8,.86) 100%);
}

/* The dark between the pillars. It is what she is walking into, so it deepens
   as she gets there. */
.gate__dark {
  position: absolute; inset: 0;
  background: radial-gradient(26% 21% at 55% 52%, rgba(2,1,2,.92), transparent 74%);
  opacity: .42;
  transition: opacity 1.1s ease;
}

/* Invisible until the walk. It is what the far side of the gate looks like. */
.gate__veil {
  position: absolute; inset: 0;
  background: #040203;
  opacity: 0;
}

/* ── walking through ──────────────────────────────────────────────── */
/* One gesture, three parts, all of it CSS: the gate rushes past the camera,
   the invitation goes with it a little faster because it is nearer, and the
   dark closes over the last third. app.js only adds the class and waits. */
/* Everything below is transform and opacity on purpose. The still scene is
   built out of the three most expensive things a browser paints -- a filter
   chain, two masks and a backdrop-filter -- and that is fine while it is
   still. Under an animated scale it is not: the whole subtree re-rasterises
   every frame, at up to 3.6x the area, with a backdrop-filter nested inside an
   ancestor that is itself being blurred. So the walk switches the costly ones
   off rather than animating through them. Nothing here is visible at the far
   side of the gate anyway. */
.screen--intro.is-entering { pointer-events: none; }

/* Smoked glass means re-sampling everything behind it, every frame, while
   everything behind it is moving. The flat fill below is matched to what the
   backdrop-filter was producing, so the swap does not show. */
.screen--intro.is-entering .frame--intro {
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  background: linear-gradient(180deg, rgba(28,17,22,.93), rgba(19,13,15,.95));
}

/* The .4px softening is there to take the edge off an upscaled photo held
   still. Nobody has ever seen it on something moving this fast. */
.screen--intro.is-entering .gate__photo {
  filter: grayscale(.52) contrast(1.1) brightness(.86);
}
.screen--intro.is-entering .gate {
  animation: gate-walk 1.4s cubic-bezier(.45,0,.85,.38) forwards;
}
.screen--intro.is-entering .gate__dark { opacity: 1; }
.screen--intro.is-entering .gate__veil {
  animation: gate-veil 1.4s ease-in forwards;
}
.screen--intro.is-entering .intro {
  animation: gate-pass 1.15s cubic-bezier(.45,0,.85,.38) forwards;
  will-change: transform, opacity, filter;
}
.screen--intro.is-entering .gate__veil { will-change: opacity; }
@keyframes gate-walk {
  from { transform: scale(1); }
  to   { transform: scale(3.6); }
}
@keyframes gate-veil {
  0%   { opacity: 0; }
  46%  { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes gate-pass {
  from { opacity: 1; transform: scale(1); filter: blur(0); }
  70%  { opacity: .35; }
  to   { opacity: 0; transform: scale(1.85) translateY(4%); filter: blur(7px); }
}

.intro {
  position: relative; z-index: 1;   /* the invitation hangs in the gateway */
  text-align: center; max-width: 34rem; width: 100%;
}

/* Three black sticks over wrought iron scrollwork, which is itself a mess of
   thin black verticals -- the candles were reading as part of the gate. Same
   answer as the date below them: they bring their own dark. The drop-shadow
   is what separates the wax from the ironwork; the pool is what stops the
   whole group from floating. */
.screen--intro .intro__candles {
  position: relative;
  z-index: 0;
}
.screen--intro .intro__candles::before {
  content: "";
  position: absolute; z-index: -1;
  left: 50%; bottom: -.7rem;
  width: 13rem; height: 8rem;
  transform: translateX(-50%);
  background: radial-gradient(closest-side,
    rgba(5,3,4,.88), rgba(5,3,4,.52) 46%, transparent 100%);
}
.screen--intro .minicandle {
  filter: drop-shadow(0 1px 2px rgba(4,2,3,.95))
          drop-shadow(0 0 6px rgba(4,2,3,.8));
}

/* The date sits on the arch's ironwork, which is exactly the wrong
   background for gold letterspaced to .42em -- the scrollwork reads as more
   letters. So it brings its own dark with it: a soft pool behind the line,
   and the gold a step brighter than elsewhere, against it. */
.screen--intro .kicker {
  position: relative;
  z-index: 0;
  color: var(--gold);
  text-shadow: 0 1px 3px rgba(4,2,3,.95), 0 0 12px rgba(4,2,3,.85);
}
.screen--intro .kicker::before {
  content: "";
  position: absolute; z-index: -1;
  left: 50%; top: 50%;
  width: 19rem; height: 3.6rem;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side,
    rgba(5,3,4,.88), rgba(5,3,4,.50) 44%, transparent 100%);
}
/* Everywhere else the frame is a solid plate. Here it hangs in a gateway, and
   a solid plate would brick up the one thing the screen is made of -- so this
   one is smoked glass: dark enough to read Grenze Gotisch against, thin enough
   that the arch and the path stay visible behind it. */
.frame--intro {
  padding-block: clamp(1.5rem, 7vw, 2.4rem);
  background: linear-gradient(180deg, rgba(22,9,14,.72), rgba(8,5,7,.80));
  -webkit-backdrop-filter: blur(4px) brightness(.72);
          backdrop-filter: blur(4px) brightness(.72);
  box-shadow: 5px 6px 0 rgba(0,0,0,.55), 0 0 34px rgba(139,26,43,.18) inset;
}

.intro__candles {
  display: flex; align-items: flex-end; justify-content: center;
  gap: clamp(1.1rem, 6vw, 2rem);
  margin-bottom: clamp(1rem, 5vw, 1.6rem);
}
.minicandle {
  position: relative;
  width: 9px; height: 42px;
  background: linear-gradient(90deg, #0f0a0c, #3a2a2f 42%, #1c1417 78%, #0b0708);
  border-radius: 1px;
  box-shadow: 0 0 14px rgba(255,150,60,.18);
}
.minicandle--tall { height: 62px; }
.minicandle__flame {
  position: absolute; left: 50%; bottom: 100%;
  width: 9px; height: 15px;
  margin-left: -4.5px; margin-bottom: 2px;
  border-radius: 50% 50% 46% 46% / 62% 62% 38% 38%;
  background: radial-gradient(ellipse at 50% 72%, #fff3c4 0%, #ffb648 42%, #e8641a 76%, rgba(140,31,8,.4) 100%);
  filter: blur(.3px);
  transform-origin: 50% 100%;
  animation: candle-dance 2.4s ease-in-out infinite;
  box-shadow: 0 0 22px 5px rgba(255,140,50,.28);
}
.minicandle:nth-child(2) .minicandle__flame { animation-duration: 3.1s; animation-delay: -.7s; }
.minicandle:nth-child(3) .minicandle__flame { animation-duration: 2.7s; animation-delay: -1.4s; }

@keyframes candle-dance {
  0%,100% { transform: scale(1,1) rotate(-2deg); opacity: .96; }
  25%     { transform: scale(.94,1.09) rotate(2.5deg); opacity: 1; }
  50%     { transform: scale(1.05,.95) rotate(-1deg); opacity: .9; }
  75%     { transform: scale(.97,1.05) rotate(3deg); opacity: 1; }
}

.loading {
  margin-top: .85rem;
  font-size: .78rem;
  letter-spacing: .18em;
  color: #5c5149;
  transition: opacity .4s ease;
}
.loading[hidden] { display: none; }

/* ═══ QUIZ ════════════════════════════════════════════════════════ */
.quiz { width: 100%; max-width: 34rem; text-align: center; }
.quiz__lead {
  font-size: clamp(1.2rem, 5.2vw, 1.4rem);
  line-height: 1.45;
  max-width: 24ch; margin: 0 auto clamp(1rem, 4.5vw, 1.4rem);
  animation: quiz-arrive 1.1s ease .35s both;
}
/* The quiz waits for the lead to be read. Hidden rather than just clear
   until then, so a tap on the blank page cannot answer a question unseen.
   On the wrapper, not the frame: the frame's own animation is the hijack's
   shake, and handing it back would replay this. */
.quiz__body { animation: quiz-arrive .9s ease 2.4s both; }
@keyframes quiz-arrive {
  from { opacity: 0; transform: translateY(10px); visibility: hidden; }
  to   { opacity: 1; transform: none; visibility: visible; }
}

.progress {
  display: flex; justify-content: center; gap: 1.15rem;
  margin-bottom: clamp(.9rem, 4vw, 1.3rem);
}
/* the quiz's rail sits under its frame */
.progress--below { margin: clamp(.9rem, 4vw, 1.3rem) 0 0; }
/* plain Grenze here too: a Gotisch I is a D, so I II III read "D DD DDD" */
.progress__step {
  font-family: var(--body);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: .06em;
  color: #4e443c;
  width: 2.1rem; height: 2.1rem;
  display: grid; place-items: center;
  border: 1px solid #2c2027;
  transition: color .35s ease, border-color .35s ease, box-shadow .35s ease;
}
.progress__step.is-current {
  color: var(--gold);
  border-color: var(--crimson);
  box-shadow: 0 0 14px rgba(139,26,43,.4), 2px 2px 0 rgba(0,0,0,.7);
}
.progress__step.is-done { color: var(--crimson-lt); border-color: #45141d; }

.frame--quiz { min-height: 16rem; display: grid; align-items: center; }

.options { display: grid; gap: .6rem; margin-top: 1.35rem; }
.option {
  cursor: pointer;
  text-align: left;
  font-size: clamp(.95rem, 4vw, 1.05rem);
  line-height: 1.42;
  color: var(--bone);
  padding: .82em 1em .82em 2.5em;
  min-height: 48px;
  position: relative;
  border: 1px solid #43161f;
  background: linear-gradient(180deg, rgba(38,16,22,.7), rgba(14,8,10,.8));
  box-shadow: 2px 3px 0 rgba(0,0,0,.62);
  transition: transform .09s ease, box-shadow .09s ease,
              border-color .25s ease, background .25s ease;
}
/* CSS-drawn diamond: no glyph, so nothing to fall back to a system font */
.option::before {
  content: "";
  position: absolute; left: 1.05em; top: 1.32em;
  width: 7px; height: 7px;
  border: 1px solid var(--crimson-lt);
  transform: rotate(45deg);
  transition: background .25s ease, border-color .25s ease;
}
.option:active {
  transform: translate(2px, 3px);
  box-shadow: 0 0 0 rgba(0,0,0,.62);
  border-color: var(--crimson-lt);
}
.option:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.option[disabled] { cursor: default; opacity: .55; }
.option.is-picked { border-color: var(--gold-dim); background: linear-gradient(180deg, rgba(58,22,30,.85), rgba(20,10,14,.9)); opacity: 1; }
.option.is-picked::before { background: var(--gold); border-color: var(--gold); }

/* reaction */
.quiz__react[hidden] { display: none; }
.react__art {
  margin-bottom: .8rem;
  animation: react-pop .45s cubic-bezier(.2,1.5,.4,1) both;
}
/* pixel sprites: keep the hard edges, never let the browser smooth them */
.pixel {
  display: block;
  margin: 0 auto;
  height: auto;
  max-width: 40vw;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  filter: drop-shadow(2px 3px 0 rgba(0,0,0,.7))
          drop-shadow(0 0 14px rgba(139,26,43,.35));
  animation: sprite-bob 1.5s steps(2, end) infinite;
}
@keyframes sprite-bob {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(-3px); }
  100% { transform: translateY(0); }
}
@keyframes react-pop {
  from { transform: scale(.55) rotate(-7deg); opacity: 0; }
  to   { transform: scale(1) rotate(0deg); opacity: 1; }
}
.react__text {
  font-size: clamp(1.02rem, 4.4vw, 1.16rem);
  line-height: 1.55;
  color: var(--bone);
  max-width: 26ch; margin-inline: auto;
}
/* a reaction GIF or picture in place of a sprite */
.react__gif {
  display: block;
  margin: 0 auto;
  max-width: min(62vw, 240px); max-height: 30vh;
  width: auto; height: auto;
  border: 1px solid var(--gold-dim);
  box-shadow: 3px 4px 0 rgba(0,0,0,.75);
}

/* ── the hijack: the last answer, stolen ─────────────────────────────
   Nothing thrown off the frame may make the screen scroll sideways. */
.screen--quiz { overflow-x: hidden; }
.frame--quiz.is-shaking { animation: quiz-shake .5s steps(10, end) 1; }
@keyframes quiz-shake {
  0%, 100% { transform: none; }
  10% { transform: translate(-7px, 2px) rotate(-.8deg); }
  20% { transform: translate(6px, -3px) rotate(.7deg); }
  30% { transform: translate(-5px, -1px) rotate(-.5deg); }
  40% { transform: translate(7px, 3px) rotate(.8deg); }
  50% { transform: translate(-6px, 1px); }
  60% { transform: translate(4px, -2px) rotate(.4deg); }
  70% { transform: translate(-3px, 2px); }
  80% { transform: translate(2px, -1px); }
  90% { transform: translate(-1px, 0); }
}
/* the question and the answers she did not pick: flung, each its own way */
#quiz-question.is-thrown, .option.is-thrown {
  transform: translate(var(--dx), var(--dy)) rotate(var(--rot));
  opacity: 0;
  transition: transform .7s cubic-bezier(.4,0,.8,.5), opacity .7s ease-in;
}
/* the one she picked, dragged out of the window by the hand holding it */
.option.is-taken {
  transform: translateX(var(--dx));
  transition: transform .38s cubic-bezier(.55,0,.9,.45);
}
/* and then the dark the kidnapping card comes up out of */
.screen--quiz::after {
  content: "";
  position: fixed; inset: 0;
  background: #050304;
  opacity: 0;
  pointer-events: none;
  transition: opacity .42s ease-in;
}
.screen--quiz.is-dark::after { opacity: 1; }

/* The hand. Fixed to the window, like the cat and over it; placed and moved
   by a transform from JS. The reach is quick and eases in to land; the yank
   is quicker and speeds up as it goes. */
.hand {
  position: fixed; left: 0; top: 0;
  z-index: 7;
  pointer-events: none;
  will-change: transform;
  filter: drop-shadow(3px 4px 0 rgba(0,0,0,.7));
}
.hand[hidden], .hand__pose[hidden] { display: none; }
.hand.is-reaching { transition: transform .45s cubic-bezier(.15,.8,.3,1); }
.hand.is-yanking  { transition: transform .38s cubic-bezier(.55,0,.9,.45); }
.hand__svg, .skeleton__svg {
  display: block; width: 100%; height: auto;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* ── the absurd intruder: the cat ────────────────────────────────── */
/* Fixed to the window and driven entirely by a transform from JS, so it walks
   over whatever screen is up without being part of any screen's layout.
   `touch-action: none` is what makes the drag work on a phone -- without it
   iOS claims the gesture as a scroll before the first pointermove lands. */
.cat {
  position: fixed;
  left: 0; top: 0;
  z-index: 6;                       /* over the screens, under ?debug */
  width: clamp(66px, 20vw, 100px);
  touch-action: none;
  cursor: grab;
  will-change: transform;
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;      /* no share sheet on a long press */
  filter: drop-shadow(2px 3px 0 rgba(0,0,0,.55));
}
.cat[hidden] { display: none; }
.cat.is-gone { opacity: 0; transition: opacity .4s ease; }
.cat:active { cursor: grabbing; }
.cat__pose[hidden] { display: none; }
.cat__svg {
  display: block; width: 100%; height: auto;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
/* Her name, and her speech bubble above it, centred on her head rather than
   the whole sprite -- the head is columns 13-20 of 21, so ~79% across. The
   parent's scaleX(-1) carries it to ~21% when she walks left, which keeps it
   over the head; .is-left only has to un-mirror the letters. Anchored at the
   bottom, so a bubble appearing grows the stack upward and the name stays
   put. No pointer events, so nothing hanging above her can eat a tap meant
   for a button. */
.cat__head {
  position: absolute;
  left: 79%; bottom: 100%;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center;
  gap: .4rem;                       /* the bubble's tail hangs in this */
  margin-bottom: .1rem;
  pointer-events: none;
}
.cat.is-left .cat__head { transform: translateX(-50%) scaleX(-1); }
.cat__name {
  font-family: var(--display);
  font-size: clamp(.95rem, 4.2vw, 1.15rem);
  line-height: 1;
  color: var(--gold);
  white-space: nowrap;
}
.cat.is-on-paper .cat__name { color: #12100f; }
/* On paper the shadow under the whole cat turned the black name into two
   names, one ghosted under the other. There it goes on the sprite alone,
   and in the page's own light pencil grey rather than the candlelit black. */
.cat.is-on-paper { filter: none; }
.cat.is-on-paper .cat__pose { filter: drop-shadow(2px 3px 0 rgba(18,16,15,.2)); }

/* Bone on the dark screens, and the ink border is what holds its edge on the
   paper one. The tail is a rotated square whose top half sits over the
   bubble's own bottom border, so the two read as one outline. */
.cat__say {
  position: relative;
  padding: .26em .6em .3em;
  font-family: var(--body);
  font-weight: 600;
  font-size: clamp(.85rem, 3.8vw, .98rem);
  line-height: 1.1;
  white-space: nowrap;
  color: var(--ink);
  background: var(--bone);
  border: 1.5px solid var(--ink);
  border-radius: .7em;
  transform-origin: 50% 100%;
  animation: cat-say-pop .24s cubic-bezier(.2,1.5,.4,1) both;
}
.cat__say[hidden] { display: none; }
.cat__say::after {
  content: "";
  position: absolute;
  left: 50%; top: calc(100% + .75px);
  width: .5em; height: .5em;
  background: var(--bone);
  border-right: 1.5px solid var(--ink);
  border-bottom: 1.5px solid var(--ink);
  transform: translate(-50%, -50%) rotate(45deg);
}
@keyframes cat-say-pop {
  from { opacity: 0; transform: scale(.4); }
  to   { opacity: 1; transform: scale(1); }
}

/* Reserve the cat a lane at the bottom of the three screens it walks on, so
   it can never sit on top of a button -- but only where there is height to
   spare. On a short phone the quiz is already tight and a scrolling screen
   beats a guarded one. */
@media (min-height: 640px) {
  .screen--quiz, .screen--tale, .screen--story, .screen--fish,
  .screen--trolley {
    padding-bottom: calc(var(--pad) + var(--safe-b) + 3.4rem);
  }
}

/* ═══ THE STORY BETWEEN — the narration cards ═════════════════════ */
/* The drawing sits in the same kind of frame as the photograph on the last
   screen, because they are the same object: a picture this site is holding a
   place for. Until one is drawn the dashed box says so out loud rather than
   pretending the layout is finished. */
.beat { width: 100%; max-width: 34rem; text-align: center; }

/* A card's pixel drawing: no frame, standing on the page the way Lila
   does, with the same hard shadow. */
.beat__scene {
  width: min(58vw, 230px);
  margin: 0 auto clamp(.7rem, 3.5vw, 1.1rem);
}
.beat__scene[hidden] { display: none; }
.scene__svg {
  display: block; width: 100%; height: auto;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  filter: drop-shadow(3px 4px 0 rgba(0,0,0,.7));
  animation: photo-in .5s ease both;
}

/* As wide as the words under it: the pictures are wide photographs, and at
   the old 62vw they came out a thumbnail on a phone. */
.beat__art { margin: 0 auto clamp(.9rem, 4vw, 1.3rem); width: 100%; }
.beat__art[hidden] { display: none; }   /* a words-only card: art: false */
.beat__frame {
  position: relative;
  border: 1px solid var(--gold-dim);
  outline: 1px solid rgba(139,26,43,.5);
  outline-offset: 4px;
  background: #0b0708;
  box-shadow: 4px 5px 0 rgba(0,0,0,.8), 0 0 30px rgba(139,26,43,.15);
  padding: 7px;
}
.beat__placeholder {
  aspect-ratio: 4 / 3;
  display: grid; place-content: center; gap: .45rem;
  border: 1px dashed #3a2a30;
  color: #5a4d44;
}
.beat__placeholder[hidden] { display: none; }
.beat__mark { font-size: 1.6rem; color: var(--crimson); opacity: .55; }
.beat__label { font-size: .62rem; letter-spacing: .16em; text-transform: uppercase; }
/* Photographs, scaled down on every phone -- smooth scaling, not pixelated,
   or the downscale shimmers. Fades in, in case it arrives after the words. */
.beat__img {
  display: block;
  width: 100%; height: auto;
  animation: photo-in .5s ease both;
}

/* The button the photo sits in. It adds nothing to the layout -- the frame
   still looks like a frame -- except a small drawn lens in the corner, so on
   a phone, where there is no zoom-in cursor, the picture says it can be
   opened. Hidden, not the <img>, while the picture is still loading. */
.beat__zoom {
  display: block; position: relative;
  width: 100%; padding: 0;
  cursor: zoom-in;
}
.beat__zoom[hidden] { display: none; }
.beat__zoom:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
/* z-index because the photo's fade-in makes it a layer of its own, which
   would otherwise paint straight over the ::before that comes ahead of it */
.beat__zoom::before, .beat__zoom::after {
  content: ""; position: absolute; z-index: 1;
  pointer-events: none;
  filter: drop-shadow(0 1px 1px rgba(0,0,0,.9));
  opacity: .78;
}
.beat__zoom::before {                /* the glass */
  right: 13px; bottom: 13px;
  width: 11px; height: 11px;
  border: 1.5px solid var(--bone);
  border-radius: 50%;
}
.beat__zoom::after {                 /* the handle */
  right: 8px; bottom: 11px;
  width: 7px; height: 1.5px;
  background: var(--bone);
  transform: rotate(45deg);
}

/* ── the lightbox ───────────────────────────────────────────────────
   Over everything, the cat included. The page underneath goes nearly black
   rather than all the way, so it still reads as the same room. It claims
   every touch -- a pinch in here is the picture's, not the page's -- and
   app.js moves the picture with one transform and nothing else. */
.lightbox {
  position: fixed; inset: 0; z-index: 20;
  display: flex; align-items: center; justify-content: center;
  padding: calc(3.4rem + env(safe-area-inset-top, 0px)) 0
           calc(3.2rem + var(--safe-b));
  background: rgba(5,3,4,.94);
  overflow: hidden;
  touch-action: none;
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
  opacity: 0;
  transition: opacity .25s ease;
}
.lightbox[hidden] { display: none; }
.lightbox.is-open { opacity: 1; }
.lightbox__img {
  display: block;
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  box-shadow: 0 0 0 1px var(--gold-dim), 0 0 40px rgba(139,26,43,.25);
  cursor: zoom-in;
  transform-origin: 50% 50%;
  transition: transform .3s cubic-bezier(.22,.9,.3,1);
  -webkit-user-drag: none;
}
.lightbox.is-zoomed .lightbox__img { cursor: grab; }
.lightbox.is-moving .lightbox__img { transition: none; cursor: grabbing; }
.lightbox__close {
  position: absolute;
  top: calc(.5rem + env(safe-area-inset-top, 0px)); right: .5rem;
  width: 48px; height: 48px;         /* thumb target */
  cursor: pointer;
}
.lightbox__close::before, .lightbox__close::after {   /* drawn, not a glyph */
  content: ""; position: absolute;
  left: 50%; top: 50%;
  width: 22px; height: 1.5px;
  background: var(--bone);
}
.lightbox__close::before { transform: translate(-50%, -50%) rotate(45deg); }
.lightbox__close::after  { transform: translate(-50%, -50%) rotate(-45deg); }
.lightbox__close:focus-visible { outline: 2px solid var(--gold); outline-offset: -4px; }
.lightbox__hint {
  position: absolute; left: 0; right: 0;
  bottom: calc(1rem + var(--safe-b));
  font-size: .72rem; letter-spacing: .12em;
  color: var(--bone-dim);
  text-align: center;
  pointer-events: none;
  transition: opacity .25s ease;
}
.lightbox.is-zoomed .lightbox__hint { opacity: 0; }

#beat-chapter { margin-bottom: clamp(.5rem, 3vw, .8rem); }
.frame--tale { padding-block: clamp(1.1rem, 5vw, 1.6rem); }
.frame--tale .display--md { margin-bottom: .6rem; }

.beat__lines { max-width: 34ch; margin-inline: auto; }
.beat__line {
  color: var(--bone-dim);
  font-size: clamp(.98rem, 4vw, 1.08rem);
  line-height: 1.6;
}
.beat__line + .beat__line { margin-top: .55rem; }
/* the last line of a card is the one that lands, so it is the one lit */
.beat__line:last-child { color: var(--bone); }

/* Read out a line at a time. A waiting line keeps its place -- the card
   is its full height from the start, so nothing below it moves as the
   words come in -- and only its ink is missing. */
.beat__line { transition: opacity .32s ease, transform .32s ease; }
.beat__line.is-waiting { opacity: 0; transform: translateY(5px); }

/* The button is not there until the words are. Invisible, not removed, for
   the same reason as the lines: it holds its place. */
#beat-next {
  transition: opacity .3s ease, transform .09s ease, box-shadow .09s ease,
              background .25s ease;
}
#beat-next.is-waiting { opacity: 0; visibility: hidden; }

/* A card whose photo is the punchline shows it under the words, not over
   them, so the eye arrives at it straight from the last line. Until then the
   frame stands empty in its place. */
.beat.is-art-after { display: flex; flex-direction: column; }
.beat.is-art-after .beat__art { order: 1; margin: clamp(.9rem, 4vw, 1.3rem) auto 0; }
.beat.is-art-after #beat-next { order: 2; align-self: center; }
.beat__zoom { transition: opacity .45s ease, transform .45s cubic-bezier(.2,1.4,.4,1); }
.beat__art.is-veiled .beat__zoom { opacity: 0; transform: scale(.94); visibility: hidden; }
.beat__art.is-veiled .beat__placeholder > * { visibility: hidden; }

/* ═══ THREE SENTENCES ═════════════════════════════════════════════ */
/* Same room as the quiz, and it borrows the quiz's frame, progress rail and
   option buttons outright. The only thing this screen owns is the tale: the
   lines she has taken, stacked, each one dropping in as it lands. */
.story { width: 100%; max-width: 34rem; text-align: center; }
.frame--story { min-height: 20rem; display: grid; align-items: center; }
.story__build[hidden], .story__done[hidden] { display: none; }

#story-title { margin-bottom: .35rem; }
#story-setup { margin-bottom: .2rem; }
/* Holds the step's place so the options below do not jump a line when
   "Bir gün…" is swapped for the longer "Demiş ki…". */
#story-step { margin-top: 1.1rem; min-height: 1.4em; }

.tale {
  display: grid; gap: .5rem;
  text-align: left;
  max-width: 34ch; margin-inline: auto;
}
.tale:empty { display: none; }               /* no gap before the first pick */
.tale__line {
  position: relative;
  padding: .34em 0 .34em 1.5em;
  font-size: clamp(.95rem, 3.9vw, 1.04rem);
  line-height: 1.5;
  color: var(--bone-dim);
  border-left: 1px solid #351019;
}
/* the same crimson diamond the option buttons wear, kept as the tick */
.tale__line::before {
  content: "";
  position: absolute; left: .55em; top: 1.05em;
  width: 5px; height: 5px;
  background: var(--crimson);
  transform: rotate(45deg);
}
/* the line that just landed: brighter for a beat, then it settles back */
.tale__line.is-new {
  color: var(--bone);
  animation: tale-drop .5s cubic-bezier(.2,1.1,.3,1) both;
}
@keyframes tale-drop {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

/* the finished fıkra reads as prose, not as a list of choices */
.tale--full .tale__line {
  color: var(--bone);
  border-left-color: #45141d;
}
#story-verdict { margin-top: 1.1rem; color: var(--bone-dim); }

/* Devam sits on the right, where Devam always is; "bir daha" is the quieter
   of the two and says so. They wrap to one column when the room runs out. */
.story__buttons {
  display: flex; flex-wrap: wrap;
  justify-content: center; gap: .8rem;
}
#story-again {
  color: var(--bone-dim);
  border-color: #43161f;
  background: linear-gradient(180deg, rgba(38,16,22,.7), rgba(14,8,10,.8));
}
#story-again[hidden] { display: none; }   /* once the cin has asked for the fur */

/* ═══ FISHING ═════════════════════════════════════════════════════ */
/* The other screen that is not gothic. For one cast the site stops being a
   candlelit room and becomes Stardew Valley. It keeps the shape every other
   screen has -- a centred column: a line at the top, a framed picture, the
   thing she acts on, a box of words, a button -- and only the clothes are
   the game's. Like the trolley page it declares its own palette and inherits
   none of the candlelight; the stage sits above the damask and the vignette,
   so an opaque background here is enough to put them away.

   The art is the four files in assets/: the lake tile, the dock, her, and
   the hair band she pulls out of it. */
.screen--fish {
  --sv-wood-lt: #e8b46f;
  --sv-wood-dk: #5a2f10;
  --sv-paper:   #f9d38b;
  --sv-ink:     #4a2810;
  background: #1a6eb0 url("assets/water.jpg") repeat;
  color: var(--sv-ink);
  font-family: "Pixelify Sans", "Trebuchet MS", Verdana, sans-serif;
  /* The hold is a touch that must not become a scroll, so the screen claims
     the gesture outright -- which also means she cannot scroll it. Once the
     cast is over the button below is the only thing that matters, so the
     screen gives the gesture back and a short window can reach it.

     The cat is draggable here like everywhere else. It sits outside the
     stage, so a finger that lands on it never reaches the hold below: petting
     the cat mid-cast costs her the fish, which is the correct price. */
  touch-action: none;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.screen--fish.is-done { touch-action: pan-y; }
.fish { width: 100%; max-width: 34rem; text-align: center; }

/* the line at the top, in the game's lettering rather than the site's */
.ftitle {
  margin: 0 0 clamp(.6rem, 3vw, .9rem);
  font: inherit;
  font-weight: 600;
  font-size: clamp(1.35rem, 6vw, 1.9rem);
  line-height: 1.15;
  color: #fff6e2;
  text-shadow: 0 3px 0 #26150a, 0 0 12px rgba(0,0,0,.45);
}

/* ── the picture ────────────────────────────────────────────────────
   Two boxes on purpose. The outer one is the frame, and its height is set in
   vh so the column fits a short phone; the inner one keeps the picture's own
   proportions, so every percentage below lands on the same plank at every
   width. What the frame cuts off is the bottom of the picture, which is open
   water, and the lake behind the page carries straight on from it. */
.fscene {
  position: relative;
  width: 100%;
  height: clamp(7rem, 21vh, 13rem);
  overflow: hidden;
  border: 4px solid var(--sv-wood-dk);
  border-radius: .3rem;
  box-shadow: 0 0 0 3px var(--sv-wood-lt), 5px 6px 0 rgba(0,0,0,.32);
}
.fdock {
  position: absolute; left: 0; top: 0;
  width: 100%; aspect-ratio: 405 / 268;
  background: url("assets/dock.jpg") 0 0 / 100% 100% no-repeat;
}
/* Her, at the end of the planks, and the tackle hung off her.
   The sprite is 40 pixels wide and drawn at 40, so it stays crisp instead of
   landing on half a pixel -- which means the rod and the line have to be
   measured in pixels too, from her hand, or they would slide off it as the
   picture changes width. Hence the wrapper: it is her, and everything below
   is positioned inside it. */
.fangler {
  position: absolute;
  left: 50%; bottom: 56%;          /* boots on the lip of the dock */
  width: 40px; height: 86px;
}
.fher { display: block; width: 40px; height: auto; }
/* the rod: a brown line out of her hand. She has none in the sprite, so the
   string has to hang off something. */
.fangler__rod {
  position: absolute; left: 30px; top: 50px;
  width: 28px; height: 3px;
  transform-origin: 0 50%; transform: rotate(-45deg);
  border-radius: 2px;
  background: linear-gradient(90deg, #6b3f16, #b07a33);
}
/* the string, from the tip of that line down to the float */
.fline { position: absolute; left: 0; top: 0; width: 70px; height: 130px; }
.fline path {
  fill: none; stroke: #f2f6f8; stroke-width: 1.4px;
  vector-effect: non-scaling-stroke; opacity: .85;
}
.fbob {
  position: absolute; left: 52.5px; top: 102.5px;
  width: 7px; height: 7px; border-radius: 50%;
  background: linear-gradient(180deg, #e0302a 50%, #fafafa 50%);
  box-shadow: 0 0 0 1px rgba(0,0,0,.45);
}

/* ── the cast ───────────────────────────────────────────────────────
   Clocked from app.js by classes on the screen, in order: is-swing (the rod
   goes back over her shoulder and comes over), is-flying (the float arcs out
   from the rod's tip to where it lands), is-landed (a ring on the water, and
   it floats), is-bitten (it is pulled under twice, and a "!"). is-casting
   covers all of it, and keeps the bar's bubble out of sight -- in its place,
   so nothing below moves when the game begins. */
.is-casting .frod { visibility: hidden; }
/* and pops up the moment the cast is over -- which is when this starts to match */
.screen--fish:not(.is-casting) .frod { animation: frod-in .35s cubic-bezier(.3, 1.6, .5, 1) both; }
@keyframes frod-in { from { transform: scale(.6); opacity: 0; } }

.is-swing .fangler__rod { animation: fcast-swing .7s cubic-bezier(.5, 0, .3, 1) both; }
@keyframes fcast-swing {
  0%   { transform: rotate(-45deg); }
  40%  { transform: rotate(-125deg); }   /* back, over her shoulder */
  72%  { transform: rotate(-12deg); }    /* and over */
  100% { transform: rotate(-45deg); }
}
/* no line until the float is in the water; no float until it is thrown */
.fline { transition: opacity .25s ease; }
.is-swing .fline, .is-flying .fline, .is-swing .fbob { opacity: 0; }
/* From the rod's tip to the water, as a parabola sampled at fifths: up
   past the tip first, then down. The numbers are the tip's offset from
   where the float comes to rest, in the angler's own pixels. */
.is-flying .fbob { animation: fcast-fly .65s linear both; }
@keyframes fcast-fly {
  0%   { transform: translate(-6px, -76px); }
  20%  { transform: translate(-4.8px, -96px); }
  40%  { transform: translate(-3.6px, -99px); }
  60%  { transform: translate(-2.4px, -84px); }
  80%  { transform: translate(-1.2px, -51px); }
  100% { transform: translate(0, 0); }
}
.is-landed .fbob { animation: fbob-float 1s steps(2, end) infinite; }
@keyframes fbob-float { 50% { transform: translateY(1px); } }
.is-bitten .fbob { animation: fbob-bite .3s ease-in-out 2; }
@keyframes fbob-bite { 50% { transform: translateY(5px); } }

/* the ring, centred on the float: 56, 106 */
.fsplash {
  position: absolute; left: 49px; top: 103.5px;
  width: 14px; height: 5px;
  border: 1.5px solid rgba(255,255,255,.9); border-radius: 50%;
  opacity: 0;
  pointer-events: none;
}
.is-landed .fsplash { animation: fsplash .7s ease-out both; }
@keyframes fsplash {
  from { transform: scale(.3); opacity: 1; }
  to   { transform: scale(2.4); opacity: 0; }
}
/* the "!", beside her head rather than over it: over it is off the top of
   the picture on a narrow phone */
.fbang {
  position: absolute; left: 38px; top: -1px;
  display: none;
  font-weight: 700; font-size: 22px; line-height: 1;
  color: #fff;
  text-shadow: -2px 0 var(--sv-wood-dk), 2px 0 var(--sv-wood-dk),
               0 -2px var(--sv-wood-dk), 0 2px var(--sv-wood-dk);
}
.is-bitten .fbang { display: block; animation: fbang-pop .3s cubic-bezier(.3, 1.7, .5, 1) both; }
@keyframes fbang-pop { from { transform: scale(.2) translateY(6px); opacity: 0; } }

/* ── the thing she acts on ──────────────────────────────────────────
   Image four, in CSS: the rod down the left, then a wooden frame holding the
   column of water she moves the box through, and the fill beside it. */
.frod {
  position: relative;
  display: flex; align-items: stretch; gap: .35rem;
  width: fit-content;
  margin: clamp(.7rem, 3vw, 1rem) auto 0;
  padding: .45rem .5rem .45rem .45rem;
  background: #f7f0e1;
  border: 2px solid #cdbb9a; border-radius: .7rem;
  box-shadow: 0 3px 0 rgba(0,0,0,.3);
}
.frod::before {                    /* the bubble's tail */
  content: ""; position: absolute; left: -.62rem; top: 50%; margin-top: -.5rem;
  border: .5rem solid transparent; border-left: 0; border-right-color: #cdbb9a;
}
.frod__pole {
  position: relative; width: .34rem; border-radius: .2rem;
  background: linear-gradient(90deg, #6e6e6e, #e6e6e6 55%, #8a8a8a);
  box-shadow: 0 0 0 1px #3a3a3a;
}
.frod__pole::after {               /* the reel */
  content: ""; position: absolute; left: -.36rem; bottom: .9rem;
  width: .95rem; height: .95rem; border-radius: 50%;
  background: radial-gradient(circle at 40% 40%, #e0a860, #b0722c);
  border: 2px solid var(--sv-wood-dk);
}
.fframe {
  display: flex; gap: .3rem; padding: .35rem;
  border: 3px solid var(--sv-wood-dk); border-radius: .3rem;
  background:
    repeating-linear-gradient(180deg, transparent 0 2.3rem,
                              rgba(90,47,16,.5) 2.3rem calc(2.3rem + 3px)),
    linear-gradient(90deg, var(--sv-wood-lt), #c8853f 55%, var(--sv-wood-lt));
}
.ftrack, .fprog {
  position: relative; overflow: hidden;
  border: 2px solid var(--sv-wood-dk); border-radius: 2px;
}
.ftrack {
  width: 1.85rem; height: clamp(8.5rem, 22vh, 14rem);
  background: linear-gradient(180deg, #a7cdf0 0%, #6f9be6 55%, #4d78d2 100%);
}
.ftrack::after {                   /* weed on the bottom, as the game has it */
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: .5rem;
  background: repeating-linear-gradient(90deg, transparent 0 5px, #2f5aa8 5px 7px,
                                        transparent 7px 9px, #3d6cc0 9px 11px);
}
.fprog { width: .8rem; background: #6e3618; }

/* Both of these move sixty times a second, so both are parked at the bottom
   of the track and moved with a transform. Driving `bottom` instead put a
   full layout and paint in every frame of the game; a transform is the
   compositor's own job. The percentages stay honest through a resize the way
   a cached pixel height would not -- the track is a clamp() on vh. */
.fbar {
  position: absolute; left: 2px; right: 2px; bottom: 0; box-sizing: border-box;
  border-radius: 2px;
  background: rgba(130,205,130,.5);
  border: 2px solid rgba(70,150,70,.55);
  will-change: transform;
}
.fbar.is-in { background: #62e04a; border-color: #b6f58a; }   /* it has the fish */

/* A full-height rail pinned to the track, so a translateY percentage on it is
   a percentage of the track rather than of the fish. The fish itself hangs at
   the bottom of the rail and keeps its own tilt, which is why the rotate sits
   on the child: the rail's transform is the position and nothing else. */
.ffish {
  position: absolute; left: 50%; bottom: 0;
  width: 1.3rem; height: 100%; margin-left: -.65rem;
  pointer-events: none;
  will-change: transform;
}
.ffish svg {
  position: absolute; left: 0; bottom: -.38rem;      /* `bottom` is its centre */
  display: block; width: 100%; height: auto;
  transform: rotate(-22deg);
  filter: drop-shadow(0 1px 0 rgba(0,0,0,.35));
  image-rendering: pixelated;
}
.fprog__fill {
  position: absolute; left: 0; right: 0; bottom: 0; height: 100%;
  transform-origin: 50% 100%;
  background: #f07f1f;
  will-change: transform;
}

/* ── the catch ──────────────────────────────────────────────────────
   Land it and the game drops the bar and holds up what came out of the
   water. Same here: the bubble goes, but keeps its space so nothing below
   jumps, and the catch pops up in the middle of where it was, in a box like
   the one the words are in. A lost fish leaves the bar as it fell.

   The sprite is 34 pixels wide drawn at 68, a whole multiple, so no pixel of
   it lands on a half; the bob is one of those pixels, for the same reason. */
.is-caught .frod { visibility: hidden; }
.fcatch {
  display: none;
  position: absolute; left: 50%; top: 50%;
  width: max-content;              /* wider than the bubble it sits in */
  padding: .75rem .9rem;
  background: var(--sv-paper);
  border: 4px solid var(--sv-wood-dk); border-radius: .3rem;
  box-shadow: inset 0 0 0 3px var(--sv-wood-lt), 5px 6px 0 rgba(0,0,0,.32);
  transform: translate(-50%, -50%);
}
.is-caught .fcatch {
  display: flex;
  visibility: visible;
  animation: fcatch-pop .45s cubic-bezier(.3, 1.7, .5, 1) both;
}
/* Sprite on the left, its write-up on the right: side by side keeps the
   box about the sprite's height, which is what fits inside the bubble's
   footprint on a short phone. */
.fcatch { align-items: center; gap: .85rem; max-width: min(19rem, 86vw); }
.fcatch img {
  display: block; flex: 0 0 auto; width: 68px; height: auto;
  filter: drop-shadow(2px 2px 0 rgba(90,47,16,.35));
  animation: fcatch-bob .9s steps(1, end) .45s infinite;
}
/* the game's item tooltip: the name in bold, the category in its colour,
   then the description */
.fcatch__text { text-align: left; min-width: 0; }
.fcatch__name {
  display: block;
  font-weight: 700;
  font-size: clamp(1.05rem, 4.6vw, 1.2rem);
  line-height: 1.1;
}
.fcatch__kind {
  display: block;
  margin-top: .15rem;
  font-size: clamp(.8rem, 3.5vw, .9rem);
  color: #7a3a9a;
}
.fcatch__about {
  display: block;
  margin-top: .45rem;
  font-size: clamp(.85rem, 3.7vw, .95rem);
  line-height: 1.3;
}
@keyframes fcatch-pop {
  from { transform: translate(-50%, -50%) scale(.2); opacity: 0; }
  to   { transform: translate(-50%, -50%) scale(1);  opacity: 1; }
}
@keyframes fcatch-bob { 50% { transform: translateY(-2px); } }

/* ── the box of words, and the button that continues it ── */
.fsay {
  margin-top: clamp(.7rem, 3vw, 1rem);
  padding: .65rem .9rem .7rem;
  background: var(--sv-paper);
  border: 4px solid var(--sv-wood-dk); border-radius: .3rem;
  box-shadow: inset 0 0 0 3px var(--sv-wood-lt), 5px 6px 0 rgba(0,0,0,.32);
  font-size: clamp(1rem, 4.4vw, 1.2rem); line-height: 1.35;
}
.fsay p { margin: 0; min-height: 2.7em; }      /* holds its place when the text swaps */
.fbtn {
  display: block; position: relative;   /* anchors the :active hit area */
  margin: clamp(.7rem, 3vw, 1rem) auto 0;
  padding: .4rem 1.7rem .45rem;
  font: inherit; font-size: clamp(1rem, 4.4vw, 1.2rem); font-weight: 600;
  color: var(--sv-ink); background: var(--sv-paper);
  border: 3px solid var(--sv-wood-dk); border-radius: .3rem;
  box-shadow: inset 0 0 0 2px var(--sv-wood-lt), 0 4px 0 var(--sv-wood-dk);
  cursor: pointer;
}
.fbtn:active {
  transform: translateY(4px);
  box-shadow: inset 0 0 0 2px var(--sv-wood-lt), 0 0 0 var(--sv-wood-dk);
}
.fbtn:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
.fbtn[hidden] { display: none; }

/* the art is pixel art and must stay pixel art when it is scaled */
.fdock, .fher, .fcatch img {
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* ═══ TROLLEY — the trolley problem ═══════════════════════════════ */
/* The one screen that is not gothic. For thirty seconds the site stops being
   a candlelit room and becomes a photocopied page out of a textbook: paper
   ground, black ink, no ornament, no glow. The tonal break IS the joke, so
   nothing in here inherits the site's chrome -- every token is re-declared
   locally rather than overridden piecemeal.

   Paper is #f7f4ee and not #fff on purpose. She opens this on a phone, at
   night, in a dim cafe; a full-brightness white screen is physically
   unpleasant and reads as a bug, and a warm off-white reads as paper anyway. */
.screen--trolley {
  --tp-paper: #f7f4ee;
  --tp-ink:   #12100f;
  background: var(--tp-paper);
  color: var(--tp-ink);
}
.trolley { width: 100%; max-width: 34rem; text-align: center; }
.trolley > .display--md {
  margin-bottom: clamp(.6rem, 3vw, .9rem);
  color: var(--tp-ink);
  text-shadow: none;
}
.screen--trolley .whisper { color: #4e4740; }

.screen--trolley .frame {
  border: 1.5px solid var(--tp-ink);
  outline: none;
  background: none;
  box-shadow: 3px 4px 0 rgba(18,16,15,.17);
}
.screen--trolley .frame::before, .screen--trolley .frame::after { display: none; }

.screen--trolley .option {
  color: var(--tp-ink);
  border: 1.5px solid var(--tp-ink);
  background: #fffdf7;
  box-shadow: 2px 3px 0 rgba(18,16,15,.18);
}
.screen--trolley .option::before { border-color: var(--tp-ink); }
.screen--trolley .option:active { border-color: var(--tp-ink); }
.screen--trolley .option.is-picked { background: var(--tp-ink); color: var(--tp-paper); }
.screen--trolley .option.is-picked::before {
  background: var(--tp-paper); border-color: var(--tp-paper);
}
.screen--trolley .btn {
  color: var(--tp-ink);
  background: none;
  border: 1.5px solid var(--tp-ink);
  box-shadow: 3px 4px 0 rgba(18,16,15,.2);
}
.screen--trolley .btn:active { background: var(--tp-ink); color: var(--tp-paper); }
.screen--trolley .btn:focus-visible,
.screen--trolley .option:focus-visible { outline: 2px solid var(--tp-ink); }
.screen--trolley .react__text { color: var(--tp-ink); }

/* the figure itself */
.tstage {
  position: relative;
  width: 100%;
  aspect-ratio: 360 / 150;
  margin-bottom: clamp(.8rem, 4vw, 1.15rem);
  border: 1.5px solid var(--tp-ink);
  background: var(--tp-paper);
  box-shadow: 3px 4px 0 rgba(18,16,15,.17);
  overflow: hidden;
}
.trails { position: absolute; inset: 0; width: 100%; height: 100%; }
/* One declaration for every line in the drawing. Children keep their own
   fill="" attributes -- this only matches the group, so they inherit the
   stroke and nothing else. */
.ink {
  fill: none;
  stroke: #12100f;
  stroke-width: 1.35;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* she throws the lever: two drawn arm positions, cross-faded */
.lever__arm { transition: opacity .35s ease; }
.lever__arm--down { opacity: 0; }
.is-thrown .lever__arm--up { opacity: 0; }
.is-thrown .lever__arm--down { opacity: 1; }
/* or she takes it with her, and the socket is all that is left */
.lever__stick { transition: opacity .35s ease; }
.is-pocketed .lever__stick { opacity: 0; }

/* the tram. left/top/rotation all come from JS; the origin is the point
   where wheel meets rail, so the lean happens about the contact patch. */
.tcar {
  position: absolute;
  left: -11%; top: 13%;
  width: 19%;
  transform-origin: 50% 100%;
  transform: translate(-50%, -100%);
}
.tcar svg { display: block; width: 100%; height: auto; }

/* the cat that stops it. Flipped, so it is facing the oncoming tram. */
.tcat {
  position: absolute;
  left: 65%; top: 58.25%;
  width: 16%;
  transform: translate(-50%, -100%) scaleX(-1);
  animation: tcat-pop .42s cubic-bezier(.2,1.5,.4,1) both;
}
.tcat[hidden] { display: none; }
.tcat .cat__svg { display: block; width: 100%; height: auto; image-rendering: pixelated; }
@keyframes tcat-pop {
  from { opacity: 0; transform: translate(-50%,-100%) scaleX(-1) scale(.35); }
  to   { opacity: 1; transform: translate(-50%,-100%) scaleX(-1) scale(1); }
}
/* The stop. The whole figure takes the jolt, and the tram, which has been
   braking since she stepped out, pitches onto its nose and rocks back --
   about the wheels, which is where the svg's origin is. */
.tstage.is-stopped { animation: tstage-jolt .7s cubic-bezier(.2,.8,.3,1) 1; }
@keyframes tstage-jolt {
  0%   { transform: translateX(0); }
  12%  { transform: translateX(-6px) rotate(-.5deg); }
  30%  { transform: translateX(4px) rotate(.35deg); }
  48%  { transform: translateX(-3px); }
  66%  { transform: translateX(2px); }
  84%  { transform: translateX(-1px); }
  100% { transform: translateX(0); }
}
.tcar svg { transform-origin: 50% 100%; }
.is-stopped .tcar svg { animation: tcar-rock .8s ease-out 1; }
@keyframes tcar-rock {
  0%   { transform: rotate(0); }
  22%  { transform: rotate(7deg); }
  46%  { transform: rotate(-3deg); }
  68%  { transform: rotate(1.5deg); }
  100% { transform: rotate(0); }
}
.tcat__pose[hidden], .tcat__zzz[hidden] { display: none; }
/* Over her head, which is on the left once she is turned to face the tram.
   The sprite is mirrored, so the letters are mirrored back. */
.tcat__zzz {
  position: absolute;
  left: 72%; bottom: 62%;
  transform: scaleX(-1);
  font-family: var(--body);
  font-weight: 600;
  font-size: clamp(.7rem, 3vw, .95rem);
  line-height: 1;
  color: var(--tp-ink);
  white-space: nowrap;
  pointer-events: none;
}
.tcat__zzz i {
  display: inline-block;
  font-style: normal;
  opacity: 0;
  animation: zzz 2.4s ease-out infinite;
}
.tcat__zzz i:nth-child(2) { font-size: 1.25em; animation-delay: .8s; }
.tcat__zzz i:nth-child(3) { font-size: 1.5em;  animation-delay: 1.6s; }
@keyframes zzz {
  0%   { opacity: 0; transform: translate(0, 4px); }
  25%  { opacity: 1; }
  100% { opacity: 0; transform: translate(5px, -12px); }
}

.frame--trolley { min-height: 12rem; display: grid; align-items: center; }
.trolley__ask[hidden], .trolley__react[hidden] { display: none; }
#trolley-setup { margin-bottom: .2rem; }
.react__text--long { max-width: 34ch; white-space: pre-line; }

/* ═══ CANDLE ══════════════════════════════════════════════════════ */
.candle-scene {
  /* The flame's live values, written here by app.js every frame. They sit on
     the scene rather than on :root so a frame invalidates this subtree and
     not the whole document. */
  --flame-lean: 0deg;
  --flame-sx: 1;
  --flame-sy: 1;
  --flame-op: 1;

  width: 100%; max-width: 30rem;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center;
  gap: clamp(.5rem, 2.5vw, 1rem);
  text-align: center;
  height: 100%;
}
.candle-copy { transition: opacity .8s ease; }
.candle-copy .lede { margin-top: .55rem; }

.cake-wrap {
  position: relative;
  width: clamp(200px, 62vw, 280px);
  flex: 0 0 auto;
}
.cake { width: 100%; height: auto; display: block; filter: drop-shadow(0 12px 24px rgba(0,0,0,.85)); }

.cake-glow {
  position: absolute;
  left: 50%; top: 4%;
  width: 190%; aspect-ratio: 1;
  transform: translate(-50%, -22%);
  background: radial-gradient(circle,
    rgba(255,168,74,.30) 0%, rgba(200,70,20,.14) 32%, transparent 66%);
  opacity: var(--flame-op);
  pointer-events: none;
  transition: opacity .5s ease;
}

/* flame — position/scale driven by JS custom properties */
.flame {
  position: absolute;
  left: 50%; top: 0;
  width: 15%;
  transform: translateX(-50%);
  transform-origin: 50% 100%;
  pointer-events: none;
  z-index: 3;
}
.flame__svg {
  width: 100%; height: auto; display: block;
  transform-origin: 50% 94%;
  transform: rotate(var(--flame-lean)) scale(var(--flame-sx), var(--flame-sy));
  opacity: var(--flame-op);
  filter: blur(.35px);
  transition: opacity .45s ease;
}
.flame__halo {
  position: absolute; left: 50%; top: 46%;
  width: 420%; aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(255,176,74,.5) 0%, rgba(255,120,30,.16) 38%, transparent 68%);
  opacity: var(--flame-op);
  filter: blur(3px);
  transition: opacity .45s ease;
}
/* The idle flicker pulses the bright core's opacity, not a filter on
   .flame__svg. A filter whose value changes cannot be handed to the
   compositor: every frame would re-rasterise the whole flame, on top of a JS
   loop already writing four custom properties a frame. Opacity reads as the
   same change in brightness and stays on the compositor. The .35px softening
   on .flame__svg is static. */
.flame__inner {
  animation: flame-core 2.6s ease-in-out infinite;
}
@keyframes flame-core {
  0%,100% { opacity: 1; }
  40%     { opacity: .74; }
  70%     { opacity: .93; }
}
.candle-scene.is-out .flame__svg,
.candle-scene.is-out .flame__halo { opacity: 0; }
/* Blown out. The core is invisible under the parent's opacity either way, but
   an animation left running is still a frame's work forever. */
.candle-scene.is-out .flame__inner { animation: none; }

/* smoke, revealed on extinguish */
.smoke {
  position: absolute; left: 50%; top: 2%;
  width: 40px; height: 90px;
  transform: translateX(-50%);
  pointer-events: none; z-index: 2;
  opacity: 0;
}
.candle-scene.is-out .smoke { opacity: 1; }
.smoke i {
  position: absolute; bottom: 0; left: 50%;
  width: 7px; height: 7px;
  margin-left: -3.5px;
  border-radius: 50%;
  background: rgba(196,186,176,.36);
  filter: blur(3px);
  opacity: 0;
}
.candle-scene.is-out .smoke i { animation: smoke-rise 3.1s ease-out forwards; }
.candle-scene.is-out .smoke i:nth-child(2) { animation-delay: .32s; }
.candle-scene.is-out .smoke i:nth-child(3) { animation-delay: .66s; }
@keyframes smoke-rise {
  0%   { opacity: 0;   transform: translate(0,0) scale(.5); }
  18%  { opacity: .62; }
  100% { opacity: 0;   transform: translate(16px,-84px) scale(3.6); }
}

/* interaction zone */
.candle-ui { min-height: 9.5rem; display: grid; align-items: start; width: 100%; }
.candle-step { grid-area: 1 / 1; transition: opacity .5s ease; }
.candle-step[hidden] { display: none; }
.candle-step .btn--major { margin-top: 1.05rem; }
.candle-step .tiny { margin-top: .7rem; }

.tiny--fallback {
  color: var(--gold-dim);
  animation: fade-up .8s ease both;
}
.tiny--fallback[hidden] { display: none; }
@keyframes fade-up {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* invisible hold target covering the cake area */
/* sits inside .cake-wrap: generous around the cake, never over the buttons */
.hold-target {
  position: absolute;
  inset: -8% -18% -6%;
  cursor: pointer;
  z-index: 4;
  background: transparent;
  touch-action: none;               /* stop iOS treating hold as scroll/select */
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;      /* ...and no share sheet on a long press */
}
.hold-target[disabled] { pointer-events: none; }
.hold-target:focus-visible { outline: 1px dashed var(--gold-dim); outline-offset: -6px; }

/* while she is actually blowing / holding */
.candle-scene.is-blowing .candle-copy { opacity: .35; }

/* and once it is out: flame gone, smoke up, the words step back */
.candle-scene.is-out .candle-copy,
.candle-scene.is-out .candle-step { opacity: 0; }

/* ═══ MESSAGE ═════════════════════════════════════════════════════ */
.screen--message {
  background: radial-gradient(120% 80% at 50% 42%, rgba(20,10,14,.6), rgba(3,2,2,.96) 72%);
}
.message { text-align: center; max-width: 32rem; width: 100%; }

.photo { margin: 0 auto clamp(1.1rem, 5vw, 1.7rem); width: clamp(150px, 46vw, 210px); }
.photo__frame {
  position: relative;
  border: 1px solid var(--gold-dim);
  outline: 1px solid rgba(139,26,43,.5);
  outline-offset: 4px;
  background: #0b0708;
  box-shadow: 4px 5px 0 rgba(0,0,0,.8), 0 0 30px rgba(139,26,43,.15);
  padding: 8px;
  animation: photo-in 1.4s ease .1s both;
}
@keyframes photo-in { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: none; } }
.photo__placeholder {
  aspect-ratio: 4 / 5;
  display: grid; place-content: center; gap: .5rem;
  border: 1px dashed #3a2a30;
  color: #5a4d44;
}
.photo__mark { font-size: 1.9rem; color: var(--crimson); opacity: .55; }
.photo__label { font-size: .66rem; letter-spacing: .16em; text-transform: uppercase; }
/* No forced ratio: the picture is shown whole, at whatever shape it is.
   The placeholder above keeps 4/5 so the empty frame still looks like a
   photograph, but cropping the real one to match would cut its edges off. */
.photo__frame img { display: block; width: 100%; height: auto; }

.message__body { color: var(--bone-dim); margin-top: 1rem; line-height: 1.66; }
.message__love {
  font-family: var(--display);
  font-size: clamp(1.7rem, 8.5vw, 2.4rem);
  color: var(--crimson-lt);
  margin-top: 1.5rem;
  text-shadow: 0 0 26px rgba(168,32,47,.55);
}
/* after the credits lift; shown, it plays the message's own reveal */
.message__again { animation: reveal 1.5s ease .4s both; }
.message__again[hidden] { display: none; }

/* After the credits: the skeleton, up out of the bottom-right corner. It
   never comes all the way out -- the edge of the window cuts him off at the
   ribs -- and it sits under everything that could be pressed. */
.skeleton {
  position: fixed;
  right: clamp(10px, 5vw, 28px); bottom: 0;
  z-index: 6;
  width: clamp(70px, 21vw, 100px);
  pointer-events: none;
  transform: translateY(105%);
  transition: transform .75s cubic-bezier(.2,.9,.3,1);
  filter: drop-shadow(2px 3px 0 rgba(0,0,0,.55));
}
.skeleton[hidden], .skeleton__pose[hidden] { display: none; }
.skeleton.is-in { transform: translateY(16%); }
/* the same bubble Lila talks in, hung to the left of his head so it stays
   on the screen, with its tail pointing back at him */
.skeleton__say {
  position: absolute;
  right: 92%; top: 36%;
  padding: .26em .6em .3em;
  font-family: var(--body);
  font-weight: 600;
  font-size: clamp(.85rem, 3.8vw, .98rem);
  line-height: 1.1;
  white-space: nowrap;
  color: var(--ink);
  background: var(--bone);
  border: 1.5px solid var(--ink);
  border-radius: .7em;
  transform-origin: 100% 50%;
  animation: cat-say-pop .24s cubic-bezier(.2,1.5,.4,1) both;
}
.skeleton__say[hidden] { display: none; }
.skeleton__say::after {
  content: "";
  position: absolute;
  left: calc(100% + .75px); top: 50%;
  width: .5em; height: .5em;
  background: var(--bone);
  border-right: 1.5px solid var(--ink);
  border-top: 1.5px solid var(--ink);
  transform: translate(-50%, -50%) rotate(45deg);
}

/* Lila, asleep in the bottom-left corner of the message -- the skeleton has
   the right. Under the credits' veil, so she is there before it and after.
   The sleeping frame's top four rows are empty, so her head, and the name
   and the zzz over it, sit a third of the way down the box. */
.lila-sleep {
  position: fixed;
  left: clamp(10px, 5vw, 28px); bottom: calc(8px + var(--safe-b));
  z-index: 4;
  width: clamp(64px, 18vw, 88px);
  pointer-events: none;
  filter: drop-shadow(2px 3px 0 rgba(0,0,0,.55));
  /* backwards, not both: once it has played nothing is left holding her on
     a layer of her own, which is what smoothed her pixels */
  animation: lila-in 1.4s ease 4.8s backwards;   /* once "Seni seviyorum." is in */
}
.lila-sleep[hidden] { display: none; }
@keyframes lila-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.lila-sleep__name {
  position: absolute;
  left: 79%; bottom: 66%;
  transform: translateX(-50%);
  font-family: var(--display);
  font-size: clamp(.9rem, 4vw, 1.05rem);
  line-height: 1;
  color: var(--gold);
  white-space: nowrap;
}
.lila-sleep__zzz {
  position: absolute;
  left: 100%; bottom: 52%;
  font-weight: 600;
  font-size: clamp(.7rem, 3vw, .9rem);
  line-height: 1;
  color: var(--bone-dim);
  white-space: nowrap;
}
.lila-sleep__zzz i {
  display: inline-block;
  font-style: normal;
  opacity: 0;
  animation: zzz 2.4s ease-out infinite;
}
.lila-sleep__zzz i:nth-child(2) { font-size: 1.25em; animation-delay: .8s; }
.lila-sleep__zzz i:nth-child(3) { font-size: 1.5em;  animation-delay: 1.6s; }

/* The end credits: a veil over the message, dark enough that the names
   rising through it do not tangle with the words underneath -- at .9 they
   did -- with only a ghost of the message left behind it. Under the skeleton, who has to come up in front of them, and
   never in the way of a tap. */
.credits {
  position: fixed; inset: 0;
  z-index: 5;
  pointer-events: none;
  overflow: hidden;
  background: rgba(4,2,3,.96);
  opacity: 0;
  transition: opacity 1.1s ease;
}
.credits[hidden] { display: none; }
.credits.is-in { opacity: 1; }
.credits__roll {
  position: absolute; left: 0; right: 0; top: 0;
  padding-inline: var(--pad);
  text-align: center;
  will-change: transform;
}
.credits__title {
  font-family: var(--display);
  font-size: clamp(2.3rem, 11.5vw, 3.4rem);
  line-height: 1.06;
  color: var(--bone);
  text-shadow: 0 2px 0 rgba(0,0,0,.85), 0 0 22px rgba(139,26,43,.32);
  margin-bottom: 3.2rem;
}
.credits__line { margin-bottom: 1.8rem; }
/* the role in the kicker's clothes, the name under it in bone */
.credits__role {
  display: block;
  font-weight: 600;
  font-size: clamp(.74rem, 3.2vw, .86rem);
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--gold-dim);
  margin-bottom: .25rem;
}
.credits__name {
  display: block;
  font-size: clamp(1.15rem, 5vw, 1.35rem);
  color: var(--bone);
}
.credits__note {
  max-width: 26ch;
  margin: 2.8rem auto;
  font-size: .92em;
  color: var(--bone-dim);
}
/* the name the roll stops on, set apart from the cast */
.credits__line.is-last { margin-top: 3.4rem; }

/* The confetti's canvas: over the screens and the cat, under the sound
   switch, and never in the way of a tap. */
.confetti {
  position: fixed; inset: 0;
  width: 100%; height: 100%;
  z-index: 7;
  pointer-events: none;
}

/* The sound switch. Top right, over everything but the lightbox, on a
   disc of its own dark so it reads on the trolley's paper as well. */
.sound {
  position: fixed;
  top: calc(.35rem + env(safe-area-inset-top, 0px)); right: .35rem;
  z-index: 8;
  width: 44px; height: 44px;          /* thumb target */
  display: grid; place-items: center;
  padding: 0;
  cursor: pointer;
  background: rgba(10,7,8,.9);
  border: 1px solid rgba(201,169,97,.32);
  border-radius: 50%;
  opacity: .85;
  transition: opacity .2s ease;
}
.sound:active { opacity: 1; }
.sound:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.sound[hidden] { display: none; }
.sound__svg {
  width: 20px; height: auto;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
.sound__svg--off, .sound.is-off .sound__svg--on { display: none; }
.sound.is-off .sound__svg--off { display: block; }

/* ?jump: a button in the top-left corner and the list it opens. Debug
   clothes, like ?debug and ?sfx -- it is not part of the page. */
.jump {
  position: fixed; z-index: 31;
  top: calc(.35rem + env(safe-area-inset-top, 0px)); left: .35rem;
  font: 12px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
.jump__open {
  width: 44px; height: 44px;
  font-size: 18px; color: #7dff9b;
  background: rgba(0,0,0,.85);
  border: 1px solid #1d4a28; border-radius: 50%;
  cursor: pointer;
}
.jump__list {
  display: flex; flex-direction: column; gap: 3px;
  margin-top: 6px; padding: 6px;
  max-height: calc(100vh - 5rem); overflow-y: auto;
  background: rgba(0,0,0,.9);
  border: 1px solid #1d4a28;
}
.jump__list[hidden] { display: none; }
.jump__list button {
  font: inherit; text-align: left;
  color: #b9f5c8; background: #0b1a10;
  border: 1px solid #1d4a28;
  padding: 8px 12px;
  cursor: pointer;
}
.jump__list button.is-game { color: #7dff9b; font-weight: 700; }

.sfxboard {
  position: fixed; left: 6px; right: 6px; bottom: 6px; z-index: 30;
  display: flex; flex-wrap: wrap; gap: 4px;
  padding: 6px;
  background: rgba(0,0,0,.85);
  border: 1px solid #1d4a28;
}
.sfxboard button {
  font: 12px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  color: #7dff9b;
  background: #0b1a10;
  border: 1px solid #1d4a28;
  padding: 7px 8px;
  cursor: pointer;
}

.reveal { animation: reveal 1.5s ease var(--d, 0s) both; }
@keyframes reveal {
  from { opacity: 0; transform: translateY(10px); filter: blur(3px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}

/* ── short screens (small phones in portrait) ─────────────────────── */
@media (max-height: 700px) {
  .intro__candles { margin-bottom: .6rem; }
  .minicandle { height: 30px; } .minicandle--tall { height: 46px; }
  .frame { padding-block: 1.1rem; }
  .cake-wrap { width: clamp(170px, 50vw, 220px); }
  .candle-ui { min-height: 8rem; }
  .cat { width: clamp(56px, 16vw, 76px); }
  .skeleton { width: clamp(52px, 15vw, 72px); }
}
@media (max-height: 560px) {
  .intro__candles { display: none; }
  .tstage { margin-bottom: .55rem; }
}

/* ── desktop: an afterthought, but must not look broken ───────────── */
@media (min-width: 48rem) and (min-height: 42rem) {
  body { font-size: 1.12rem; }
  .cake-wrap { width: 300px; }
}

/* ── accessibility ────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .12s !important;
  }
  .minicandle__flame, .flame__inner { animation: none; }
  /* The walk is a cut instead: no rush past the camera, just the far side. */
  .screen--intro.is-entering .gate,
  .screen--intro.is-entering .intro { animation: none; }
  .screen--intro.is-entering .intro { opacity: 0; }
  .screen--intro.is-entering .gate__veil { animation: none; opacity: 1; }
  /* Ambient decoration is the first thing to go: it carries no meaning. */
  .motes { display: none; }
  .screen { transform: none; }
  .tstage.is-stopped, .is-stopped .tcar svg { animation: none; }
  .cat__svg, .tcar svg { animation: none; }
  .tcat__zzz i, .lila-sleep__zzz i { animation: none; opacity: 1; }
}

/* ── debug readout, ?debug only ───────────────────────────────────── */
.debug {
  position: fixed; left: 6px; top: 6px; z-index: 99;
  margin: 0; padding: 7px 9px;
  font: 11px/1.35 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  color: #7dff9b;
  background: rgba(0,0,0,.82);
  border: 1px solid #1d4a28;
  white-space: pre;
  pointer-events: none;
  letter-spacing: .02em;
}
