/* ============================================================================
   Playit — LANDING page styles ONLY (the welcome screen). These rules exist on
   no other page, so they live here instead of the global theme (global.css).
   Loaded after global.css, so the :root tokens and base styles are available.

   The page opens on a full-screen hero: a canvas animation fills the section
   (landing_hero.js runs two tiny self-playing games, each collapsing into a knot
   of pixels and blowing back out as the next one) with two columns layered on top
   — the written promise on the left, and on the right the doodle strip, where a
   selfie turns into the game's hero.
   ============================================================================ */

/* ---- The handwriting face ---- */
/* Caveat carries the hero's promise and the doodle strip's notes — the one place
   on the site that is a felt-tip scrawl rather than pixels. It lives HERE, not in
   the arcade kit: kit.css is injected into every generated game's frame, and no
   game should pay for a font only the front door uses. Self-hosted like the other
   two (the strict CSP blocks every external font host), and cut down to the ASCII
   this page actually writes plus the weight range it uses — 21 KB instead of the
   75 KB the full variable face costs. */
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 600 700;
  font-display: swap;
  src: url('/static/vendors/fonts/caveat-600-700.woff2') format('woff2');
}

/* The hero breaks out of .page-main's 1140px cap to span the full viewport width
   (width:100vw below). When a non-overlay scrollbar is present that breakout can
   overhang the visible area and cause a horizontal scrollbar — clipping it at the
   real scrolling element (<html>, via the page-landing class base.html stamps
   there) is the reliable fix. */
html.page-landing {
  overflow-x: hidden;
  --font-hand: 'Caveat', cursive;
}

/* The hero owns everything below the header: no page gutters, so the canvas runs
   edge to edge and header + hero together are exactly one viewport tall (the
   hero's own min-height math below re-adds the header's measured height). */
html.page-landing .page-main {
  padding: 0;
}

/* Play "▶" icon for the CREATE YOUR GAME button. A CSS triangle (not the ▶ glyph,
   so it never depends on font coverage) in the button's text colour, sized to the
   caps and vertically centred by the button's flex alignment. */
.btn-play-icon {
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 8px 0 8px 13px;
  border-color: transparent transparent transparent currentColor;
}

/* ---- The hero stage ---- */
/* Fills exactly the screen below the header — --header-h (landing.js) is the header's real
   measured height, never a guessed constant, so this is correct at any breakpoint or header
   content. dvh (falls back to vh) is the mobile fix: vh includes space hidden behind the
   browser's address bar, so on a phone it under/over-shoots the actually-visible viewport.
   isolation makes the z-index layers below (canvas 0 → shades 1 → copy 2) a private stack
   that can't interleave with anything outside the hero. */
.hero {
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  min-height: calc(100vh - var(--header-h, 82px));
  min-height: calc(100dvh - var(--header-h, 82px));
  display: flex;
  align-items: center;
  overflow: hidden;
  isolation: isolate;
}

/* The animation canvas behind everything. Scenery only: it never captures a
   click, and landing_hero.js sizes its bitmap to this box (so no width/height
   attributes in the markup). */
.hero-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
  pointer-events: none;
}

/* Two legibility gradients between the canvas and the copy: a left-to-right one
   that keeps the text column dark whatever the game behind it draws, and a
   top/bottom one that fades the stage into the page at its edges. */
.hero-shade-x {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(100deg,
    rgba(11, 10, 36, .96) 0%, rgba(11, 10, 36, .9) 26%, rgba(11, 10, 36, .55) 48%,
    rgba(11, 10, 36, .12) 70%, rgba(11, 10, 36, .45) 100%);
}
.hero-shade-y {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(11, 10, 36, .66) 0%, rgba(11, 10, 36, 0) 20%,
    rgba(11, 10, 36, 0) 74%, rgba(11, 10, 36, .55) 100%);
}

/* The layer above the canvas: the written promise on the left, the doodle strip
   on the right. They are pushed to OPPOSITE edges of the stage (space-between),
   not centred as one block — the hero is full-bleed, so a narrow centred band
   left a wide dead margin down each side of the page while the two columns were
   squeezed together in the middle. Each one now sits against its own side and
   the animation runs in the space between them. When they no longer fit side by
   side the strip wraps under the copy (see the media queries at the foot of this
   file, where both then hug the left edge instead). */
.hero-copy {
  position: relative;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: clamp(26px, 4vw, 58px);
  width: 100%;
  max-width: 1440px;
  margin: 0 auto;
  padding: clamp(30px, 5vh, 60px) clamp(20px, 4.5vw, 62px);
}

/* The left column. Shrinks before the strip does — the words reflow, the drawing
   can't — and never narrows past a readable measure. */
.hero-lede {
  flex: 0 1 462px;
  min-width: min(100%, 300px);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
}

/* Big glitchy PLAYIT title. The multi-layer text-shadow gives the 3D pixel
   drop; the ::before/::after ghosts flicker cyan/magenta over it. */
.hero-title {
  position: relative;
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(30px, 11vw, 54px);
  line-height: 1.3;
  letter-spacing: 2px;
  color: var(--color-title-yellow);
  animation: glitch-shake 4s steps(1) infinite;
  text-shadow:
    -3px 0 0 var(--color-ink), 3px 0 0 var(--color-ink),
    0 -3px 0 var(--color-ink), 0 3px 0 var(--color-ink),
    -3px -3px 0 var(--color-ink), 3px -3px 0 var(--color-ink),
    -3px 3px 0 var(--color-ink), 3px 3px 0 var(--color-ink),
    5px 6px 0 var(--color-pink), 7px 8px 0 var(--color-pink),
    9px 11px 0 var(--color-deep-pink), 11px 14px 0 var(--color-ink);
}
.hero-title::before,
.hero-title::after {
  content: 'PLAYIT';
  position: absolute;
  left: 0; top: 0;
  width: 100%;
  text-align: left;
  text-shadow: none;
  pointer-events: none;
  opacity: 0;
}
.hero-title::before { color: var(--color-cyan); animation: glitch-cyan 4s steps(1) infinite; }
.hero-title::after  { color: var(--color-pink); animation: glitch-magenta 4s steps(1) infinite; }

/* The green "IT". Its glitch is driven by JS (glitchIt in landing.js), which
   nudges each letter at random. Only the resting state is here. */
.hero-title-it {
  position: relative;
  display: inline-block;
  color: var(--color-green);
}

/* ---- The promise, handwritten ---- */
/* The one line that says what this place is for, with a felt-tip underline
   scribbled under it (two passes, yellow over pink, drawn as SVG so it keeps its
   wobble at any width instead of stretching like a border would). */
.hero-line {
  position: relative;
  margin: 26px 0 4px;
}
/* ONE line, always. The two halves are a pair — "yours, ours" — and wrapping the
   second one under the first breaks the handshake, so the line is told never to
   break, which means its size has to guarantee it fits. The tight spot is NOT the
   phone: it is the band just above the wrap point (about 900-1100px), where the
   drawing beside it keeps its full width and this column is the one that gets
   squeezed, down to roughly 350px. 3.4vw clears that band at every width, and is
   let off the leash again below it, where the column goes full width (see the
   media queries at the foot of this file). */
.tagline {
  font-family: var(--font-hand);
  font-weight: 700;
  font-size: clamp(28px, 3.4vw, 52px);
  letter-spacing: .5px;
  line-height: 1.05;
  white-space: nowrap;
  color: #ffffff;
  margin: 0;
}
/* the second half — ours — picked out in the logo's green */
.tagline-hit {
  color: var(--color-green);
}
/* the scribble runs under most of the line rather than a fixed 320px: .hero-line
   is a flex item on a flex-start column, so it already shrink-wraps to the words
   above it, and a percentage here follows them at every size */
.tagline-rule {
  display: block;
  width: 92%;
  height: 12px;
  margin-top: 2px;
  overflow: visible;
}

/* ---- The three promises ---- */
/* What the visitor gets, one line each, beside a ringed tick — a deliberately
   imperfect circle with a hand-drawn check in it. The mark is hidden from screen
   readers: it says nothing the line beside it doesn't. */
.hero-promises {
  display: flex;
  flex-direction: column;
  gap: 13px;
  list-style: none;
  margin: 20px 0;
  padding: 0;
}
.hero-promise {
  display: flex;
  align-items: center;
  gap: 13px;
}
.hero-promise-mark {
  width: 32px;
  height: 32px;
  flex: none;
  overflow: visible;
}
/* The tick's resting state is DRAWN, and the keyframes below take it away and put
   it back — so switching motion off needs nothing but `animation: none`. This is
   the one place on the page that uses animation-delay: the loop next door can't
   (delays drift apart over a long-open tab), but this fires once on load and is
   over in a second, which is exactly what a delay is for. */
.hero-tick {
  fill: none;
  stroke-width: 2.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
  animation: hero-tick-in .42s ease-out backwards;
}
.hero-tick-a { animation-delay: .35s; }
.hero-tick-b { animation-delay: .62s; }
.hero-tick-c { animation-delay: .89s; }
@keyframes hero-tick-in {
  from { stroke-dashoffset: 1; }
  to   { stroke-dashoffset: 0; }
}
.hero-promise-text {
  font-family: var(--font-hand);
  font-weight: 600;
  font-size: clamp(20px, 4.4vw, 26px);
  line-height: 1.14;
  color: #e9e6ff;
}

/* ---- The doodle strip (landing/partial/strip.html) ---- */
/* The right column: a scrapbook page that pins itself up, on a loop — an empty
   photo slot that a selfie falls into, that photo puffing into the cartoon
   character the platform draws from it, and finally, once the arrow pointing at it
   has finished being drawn, that character running inside a live game screen.

   TWO INDEPENDENT CLOCKS, and keeping them apart is what makes this readable:

   1. The STORY. Every animation that tells a step runs for exactly var(--beat)
      and carves its moment out of the percentages — no animation-delay anywhere.
      Delays plus differing durations drift apart over a long-open tab; a single
      shared duration cannot. Retune the whole film by changing that one number.
   2. The IDLE FLOAT. The three pieces bob on their own periods (8.2s / 9.6s /
      11s — deliberately not multiples of each other, so they never bob together
      and never line up with the story). It touches only `transform`, the story
      touches only the contents, so the two never fight over a property.

   The pieces are ABSOLUTELY placed on a deliberate zig-zag (top right, then down
   and left, then down and right) rather than laid out in a row: nothing lines up,
   the way nothing lines up on a corkboard. ONE size drives all of it — --art is
   the whole collage's width, the box holds its shape with aspect-ratio, and every
   piece inside is a percentage of that box. So the drawing rescales from a single
   number per breakpoint instead of being redrawn.

   EVERY RESTING STATE IS THE FINISHED STATE — the photo in, the character drawn,
   the arrows inked. The keyframes are what take things away and put them back. So
   switching the animation off (reduced motion, at the bottom of this file) leaves
   the completed picture rather than an empty page, and needs no second set of
   rules to say what "off" looks like. */
/* The negative top margin is what lifts the strip clear of the game playing below
   it. The strip is the TALLER of the two columns, so it is the one setting the
   flex line's height — pulling it up with a margin shortens that line rather than
   pushing anything around, which is why the written column beside it doesn't move.
   It only applies where the two sit side by side; both narrower layouts below set
   it back to 0. */
/* --art is the collage's width and the one number this whole column is sized
   from. It lives up HERE rather than on .doodle because the handwriting above the
   collage scales off it too — one number per breakpoint moves the drawing and the
   words together instead of leaving them to drift apart. */
.hero-strip {
  --art: 440px;
  flex: 0 0 auto;
  align-self: flex-start;
  width: fit-content;
  max-width: 100%;
  margin-top: clamp(-84px, -8vh, -28px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0;
}
/* The promise over the strip, in the same felt-tip as the notes under the cards.
   Two voices: a small green aside that tells you to go, and the line itself.

   It is written ON the board rather than parked above it: the negative bottom
   margin drops it into the empty corner over the SECOND card — the one lane down
   the left of the collage that stays clear until nearly a fifth of the way in —
   and the slant pivots from the bottom-left so the line lifts to the right.

   The max-width is load-bearing, not styling. It wraps the line in two, which is
   what keeps the words inside that left lane: run full width and the last word
   lands on the first card's tape. Both sizes come off --art, so the handwriting
   and the drawing shrink together. */
.hero-strip-title {
  position: relative;
  z-index: 1;
  max-width: calc(var(--art) * .56);
  margin: 0 0 calc(var(--art) * -.085) 2px;
  font-family: var(--font-hand);
  font-weight: 700;
  font-size: clamp(22px, calc(var(--art) * .082), 38px);
  line-height: 1.08;
  color: var(--color-title-yellow);
  transform: rotate(-3.4deg);
  transform-origin: 0 100%;
}
.hero-strip-kicker {
  display: block;
  width: fit-content;
  margin: 0 0 1px 4px;
  font-size: clamp(15px, calc(var(--art) * .05), 23px);
  color: var(--color-green);
  transform: rotate(2.2deg);
}
/* The scribbled underline under the aside — the same pen as the arrows below, so
   the two read as one hand. overflow visible so the round cap isn't clipped. */
.hero-strip-swoosh {
  display: block;
  width: 100%;
  height: 7px;
  margin-top: -2px;
  overflow: visible;
}

/* --beat is the story's one clock. Everything below that tells a step runs for
   exactly this long, so the film can never fall out of step with itself. Most of
   it is the last eight seconds: the board finishes building and then simply SITS
   there, finished, which is what a visitor arriving late should find. */
.doodle {
  --beat: 20s;
  position: relative;
  width: var(--art);
  max-width: 100%;
  aspect-ratio: 440 / 404;
}

/* Each piece = one card plus the caption written under it. The three tops/lefts
   below ARE the zig-zag; the pick hugs the right edge, the drawing the left, and
   the game screen the right again, so the collage fills its box corner to corner
   without a single shared edge. The float is the idle clock — see the note above. */
.doodle-piece {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(var(--art) * .017);
  animation: doodle-float 8s ease-in-out infinite;
}
.doodle-pick  { left: 65%; top: 0;   width: 33.5%; animation-duration: 8.2s; }
.doodle-toon  { left: 0;   top: 19%; width: 33.5%; animation-duration: 9.6s; }
.doodle-scene { left: 45%; top: 56%; width: 55%;   animation-duration: 11s; }

/* The paper each card is pinned on, each tilted its own way. The tilt lives here
   and the float lives on the piece above it, so the two never fight over the one
   transform. */
.doodle-card {
  position: relative;
  width: 100%;
}
.doodle-card-pick { transform: rotate(2.4deg); }
/* Held back, the same way the game screen below is: the first arrow draws itself
   pointing at bare board, and the page it was pointing at lands on its tip. */
.doodle-card-toon {
  transform: rotate(-3.6deg);
  animation: doodle-land-toon var(--beat) ease-out infinite;
}
/* The game screen is HELD BACK. It is the payoff, so it does not sit there being
   the payoff from the first frame — the arrow above draws itself pointing at bare
   board, and only once that stroke has landed does the game drop onto its tip.
   Both the arrival and the tilt live on this one element, which is why the tilt is
   repeated in every frame of those keyframes: one element, one transform. */
.doodle-card-scene {
  transform: rotate(1.2deg);
  animation: doodle-land var(--beat) ease-out infinite;
}

.doodle-box {
  position: relative;
  width: 100%;
  overflow: hidden;
  box-shadow: calc(var(--art) * .013) calc(var(--art) * .016) 0 rgba(0, 0, 0, .38);
}

/* 1. The picker. It opens EMPTY and fills itself: the dashed slot, the photo
   falling in, the flash, the green read bar, and the edge closing up solid. */
.doodle-box-shot {
  aspect-ratio: 248 / 298;
  background: #fdf8ea;
  padding: calc(var(--art) * .013);
}
/* The empty slot, sitting inside the paper frame. Hidden at rest — the resting
   state of this card is the FILLED one — and put back by its keyframes. */
.doodle-slot {
  position: absolute;
  z-index: 1;
  inset: calc(var(--art) * .013);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(var(--art) * .026);
  background: #f0e9d6;
  /* a deep green, not the neon --color-green: this sits on cream paper, where the
     bright one washes out to roughly 1.4:1 and stops being readable at all */
  color: #0b6b3a;
  opacity: 0;
  animation: doodle-slot var(--beat) ease-out infinite;
}
/* The plus, built from two bars rather than a glyph so it never depends on font
   coverage and lands on the pixel at any size. */
.doodle-plus {
  position: relative;
  width: 24%;
  aspect-ratio: 1;
}
.doodle-plus::before,
.doodle-plus::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  background: currentColor;
}
.doodle-plus::before { width: 100%; height: 15%; }
.doodle-plus::after  { width: 15%;  height: 100%; }
.doodle-slot-word {
  font-family: var(--font-hand);
  font-weight: 700;
  font-size: clamp(13px, calc(var(--art) * .055), 22px);
  line-height: 1;
}
/* The photo itself. Positioned so it paints OVER the slot behind it — an absolute
   box beats plain flow content whatever the source order says. */
.doodle-photo {
  position: relative;
  z-index: 2;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 4%;
  background: #f0e9d6;
}
/* It doesn't appear — it falls in, over-shoots, and settles, the way a picture
   dropped onto a slot does. */
.doodle-photo-drop {
  animation: doodle-drop var(--beat) ease-out infinite;
}
/* The green bar that reads the photo, right behind the flash. */
.doodle-sweep {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  z-index: 3;
  height: 34%;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(180deg, rgba(62, 240, 139, 0), rgba(62, 240, 139, .5));
  animation: doodle-sweep var(--beat) linear infinite;
}
/* The flash itself — one white frame over the picture, gone in half a second. */
.doodle-flash {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  background: #fff;
  opacity: 0;
  animation: doodle-flash var(--beat) ease-out infinite;
}

/* 2. The drawing. A taller card than the picker — it holds a whole standing
   character, not a face — and the character is shown WHOLE inside it, never
   cropped: it is a cut-out on a transparent background, laid on the paper. */
.doodle-box-toon {
  aspect-ratio: 248 / 318;
  background: #fdf8ea;
  padding: calc(var(--art) * .013);
}
/* The character springs OUT of the puff. The origin is the puff's own centre, not
   the middle of the card — the two have to be the same point or the drawing looks
   like it arrived from somewhere else and the puff was a coincidence. */
.doodle-toon-art {
  position: relative;
  z-index: 2;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform-origin: 50% 46%;
  animation: doodle-pop var(--beat) cubic-bezier(.3, 1.5, .5, 1) infinite;
}
/* The photo from card one, arriving in the middle of the empty page before it
   puffs. Same picture, so the eye joins the two cards without being told to. */
.doodle-seed {
  position: absolute;
  z-index: 3;
  left: 50%;
  top: 46%;
  width: 54%;
  height: auto;
  opacity: 0;
  animation: doodle-seed var(--beat) ease-in-out infinite;
}
/* The puff: one white ring blowing outwards with two sparks riding it. */
.doodle-poof {
  position: absolute;
  z-index: 4;
  left: 50%;
  top: 46%;
  width: 46%;
  aspect-ratio: 1;
  border: calc(var(--art) * .009) solid #fff;
  border-radius: 50%;
  opacity: 0;
  animation: doodle-poof var(--beat) ease-out infinite;
}
.doodle-poof::before,
.doodle-poof::after {
  content: '';
  position: absolute;
  width: 36%;
  aspect-ratio: 1;
  clip-path: polygon(50% 0, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0 50%, 40% 40%);
}
.doodle-poof::before { left: -18%;  top: 2%;    background: var(--color-gold); }
.doodle-poof::after  { right: -16%; bottom: 6%; background: var(--color-cyan); }
/* The cyan bar that reads the finished drawing over, once. It travels its own
   height eleven times over, which is exactly the card, so it clears the frame at
   both ends. */
.doodle-scan {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  z-index: 5;
  height: 9%;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(180deg, rgba(36, 229, 255, 0), rgba(36, 229, 255, .9));
  box-shadow: 0 0 calc(var(--art) * .022) rgba(36, 229, 255, .8);
  animation: doodle-scan var(--beat) linear infinite;
}
/* The gleam that crosses the finished drawing — the look of something that has
   just this second come out of the printer. */
.doodle-shine {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  background: linear-gradient(114deg,
    rgba(255, 255, 255, 0) 38%, rgba(255, 255, 255, .6) 50%, rgba(255, 255, 255, 0) 62%);
  transform: translateX(-130%);
  animation: doodle-shine var(--beat) ease-in-out infinite;
}
/* The strips of tape holding each one down, at opposite corners. */
.doodle-tape {
  position: absolute;
  z-index: 7;
  width: 44%;
  aspect-ratio: 40 / 16;
  border: 2px solid rgba(11, 10, 36, .35);
}
.doodle-tape-pick {
  top: -3.5%;
  left: 3%;
  background: rgba(255, 61, 154, .5);
  transform: rotate(-9deg);
}
.doodle-tape-toon {
  top: -3.5%;
  right: 3%;
  background: rgba(36, 229, 255, .46);
  transform: rotate(7deg);
}

/* 3. The game screen. Not a drawing of a game — the game's OWN city background
   scrolling past behind the run sheet the platform builds from a photo. */
.doodle-box-scene {
  aspect-ratio: 16 / 9;
  background: #4db8f0;                     /* the sky, for the frame before the art lands */
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
/* The scrolling city. The strip is five tiles wide and each tile is a fifth of
   that, so one tile is exactly 160% of the card — the shape the picture was cut
   at. Sliding it by one tile (a third of its own width) and looping is seamless. */
.doodle-sky {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 480%;
  background: url('/static/img/landing/scene.webp') 0 0 / 33.3333% 100% repeat-x;
  animation: doodle-scroll 15s linear infinite;
}
.doodle-coin {
  position: absolute;
  z-index: 1;
  width: 5.4%;
  aspect-ratio: 1;
  background: var(--color-gold);
  box-shadow: 0 0 0 1px rgba(11, 10, 36, .45);
}
.doodle-coin-a { left: 18%; top: 26%; }
.doodle-coin-b { right: 24%; top: 40%; }
/* The hero: ONE frame of the run sheet at a time. The sheet is laid four frames
   wide (background-size: 400%) and the animation walks background-position across
   it in whole steps — jump-none, not the default, because the cycle needs a stop
   ON both ends and plain steps() would skip the last one.

   It stops at 66.7%, frame THREE of four: the first three are the run cycle and
   the fourth is the jump, which would read as a stumble in a character running on
   the spot. The bottom margin sets the feet down on the grass band the background
   picture was cropped to end with. */
.doodle-hero {
  position: relative;
  z-index: 2;
  height: 68%;
  aspect-ratio: 152 / 197;
  margin: 0 0 7%;
  background: url('/static/img/landing/hero-run.webp') 0 0 / 400% 100% no-repeat;
  animation: doodle-run .48s steps(3, jump-none) infinite;
}
/* The hand-drawn frame around each card — a wobbly rectangle stretched over the
   card and sitting outside it, so the line reads as pen on top of the paper. */
.doodle-edge {
  position: absolute;
  inset: -5px;
  width: calc(100% + 10px);
  height: calc(100% + 10px);
  pointer-events: none;
  overflow: visible;
}
/* Only the picker's frame is drawn twice: dashed while the slot is empty, closing
   up solid the moment the photo is in. The dash is in the keyframes and not here,
   so the resting frame — the one with the photo already in it — is the solid one. */
.doodle-edge-pick path {
  animation: doodle-seal var(--beat) linear infinite;
}

/* The two scribbled arrows, each parked in the empty lane between the pieces it
   joins. Each is ONE unbroken pen stroke: the shaft runs to the point, then
   doubles back for each barb the way a hand draws an arrowhead — which is what
   lets the whole arrow, head and all, ink itself on in a single sweep with no
   second animation to keep in step. The ghost line under it is that same stroke
   gone over twice, plus two speed ticks.

   pathLength="1" in the markup normalises every path to a length of one, so the
   draw-on is `dashoffset: 1 -> 0` for both arrows however long the real curve is —
   no measuring, and no JS. */
.doodle-arrow {
  position: absolute;
  overflow: visible;
}
.doodle-ink {
  fill: none;
  stroke: var(--color-title-yellow);
  stroke-width: 3.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}
/* Dimmed with the stroke's own alpha, not `opacity` — the draw-on animates
   `opacity`, and that would flatten the two lines back to the same weight. */
.doodle-ink-ghost {
  stroke: rgba(255, 227, 90, .5);
  stroke-width: 2.2;
}
.doodle-arrow-a {
  left: 35%;
  top: 13%;
  width: 30%;
}
.doodle-arrow-b {
  left: 33%;
  top: 44%;
  width: 15%;
}
.doodle-arrow-a .doodle-ink { animation: doodle-draw-a var(--beat) ease-out infinite; }
.doodle-arrow-b .doodle-ink { animation: doodle-draw-b var(--beat) ease-out infinite; }

/* Each caption, in the colour of the step it belongs to, written the moment that
   step lands. Kept on one line even when it is wider than the card it labels — it
   reads as a note written under the picture, not a paragraph. */
.doodle-cap {
  font-family: var(--font-hand);
  font-weight: 700;
  font-size: clamp(14px, calc(var(--art) * .062), 25px);
  line-height: 1;
  white-space: nowrap;
}
.doodle-cap-a { color: var(--color-green); animation: doodle-note-a var(--beat) ease-out infinite; }
.doodle-cap-b { color: var(--color-cyan);  animation: doodle-note-b var(--beat) ease-out infinite; }
.doodle-cap-c { color: var(--color-gold);  animation: doodle-note-c var(--beat) ease-out infinite; }

/* ---- Animations used only by the landing page ---- */
/* The idle clock: pinned paper breathing on the board. Three different periods on
   the three pieces, so they never bob as one block. */
@keyframes doodle-float {
  0%, 100% { transform: translate3d(0, -3px, 0); }
  50%      { transform: translate3d(0, 4px, 0); }
}

/* ---- The story, one twenty-second take ----
   The board is BARE at the top of the loop. Only the picker exists; each of the
   other two cards is drawn for by its arrow first and lands on the arrow's point,
   so nothing is ever sitting there waiting to be explained.

   0-5%     the picker sits empty, plus beckoning
   4-8%     the photo falls in, the flash fires, the frame seals
   8-13%    the green bar reads it, the first note is written
   12-16%   the first arrow inks itself in, pointing at bare board
   16-22%   the drawing's page lands on its point
   22-30%   the photo arrives on it, puffs, and the character springs out
   30-46%   the bar reads him over, the gleam crosses, the second note is written
   40-43%   the second arrow inks itself in, pointing at bare board again
   44-52%   the game screen lands on ITS point, the last note is written
   52-95%   the finished board holds — EIGHT AND A HALF SECONDS of it, which is the
            whole reason the take is twenty and not twelve. Someone who arrives
            mid-loop should find a finished scrapbook page, not a half-built one.
   95-100%  it clears itself back to bare ------------------------------------- */

/* The empty slot: one beckoning pulse, then out of the way for the photo. It only
   comes back at 98%, AFTER the photo below has finished leaving — overlap the two
   and the clear-out shows "add a photo" written across a face. */
@keyframes doodle-slot {
  0%         { opacity: 1; transform: scale(1); }
  2%         { opacity: 1; transform: scale(1.07); }
  4%         { opacity: 1; transform: scale(1); }
  5.5%, 98%  { opacity: 0; transform: scale(.86); }
  100%       { opacity: 1; transform: scale(1); }
}
/* The photo falls in, over-shoots, settles — and flies back out the way it came. */
@keyframes doodle-drop {
  0%, 4%    { opacity: 0; transform: translateY(-17%) scale(1.24) rotate(-7deg); }
  6.5%      { opacity: 1; transform: translateY(2%) scale(.97) rotate(1.6deg); }
  8%, 95%   { opacity: 1; transform: none; }
  98%, 100% { opacity: 0; transform: translateY(-17%) scale(1.24) rotate(-7deg); }
}
/* One white frame, then gone. */
@keyframes doodle-flash {
  0%, 6%      { opacity: 0; }
  7%          { opacity: .95; }
  10.5%, 100% { opacity: 0; }
}
/* The dashed drop-zone edge closing up solid once the picture is in. */
@keyframes doodle-seal {
  0%, 5%   { stroke-dasharray: 9 7; }
  8%, 97%  { stroke-dasharray: 40 0; }
  100%     { stroke-dasharray: 9 7; }
}
/* The green bar that reads the photo. 294% of the bar's own 34% height is exactly
   the card, so it clears the frame at both ends. */
@keyframes doodle-sweep {
  0%, 8%      { opacity: 1; transform: translateY(-100%); }
  13%         { opacity: 1; transform: translateY(294%); }
  13.5%, 100% { opacity: 0; transform: translateY(294%); }
}
/* An arrow inks itself on from the pen-down end. dasharray 1 on a path normalised
   to length 1 is a single dash the length of the whole line; walking the offset
   from 1 to 0 slides that dash on from the start. */
@keyframes doodle-draw-a {
  0%, 11%    { opacity: 0; stroke-dashoffset: 1; }
  12%        { opacity: 1; stroke-dashoffset: 1; }
  15.8%, 95% { opacity: 1; stroke-dashoffset: 0; }
  98%      { opacity: 0; stroke-dashoffset: 0; }
  100%     { opacity: 0; stroke-dashoffset: 1; }
}
@keyframes doodle-draw-b {
  0%, 39%    { opacity: 0; stroke-dashoffset: 1; }
  39.5%      { opacity: 1; stroke-dashoffset: 1; }
  43.3%, 95% { opacity: 1; stroke-dashoffset: 0; }
  98%      { opacity: 0; stroke-dashoffset: 0; }
  100%     { opacity: 0; stroke-dashoffset: 1; }
}
/* The drawing's blank page dropping onto the first arrow's point, a beat after
   that arrow lands at 15.8%. The tilt is repeated in every frame because an
   element has only one transform to give. */
@keyframes doodle-land-toon {
  0%, 16.5% { opacity: 0; transform: rotate(-9deg) scale(.72); }
  19.5%     { opacity: 1; transform: rotate(-1.2deg) scale(1.06); }
  22%, 95%  { opacity: 1; transform: rotate(-3.6deg) scale(1); }
  100%     { opacity: 0; transform: rotate(-9deg) scale(.72); }
}
/* The photo lands on that page, shrinks, and is gone in the puff. */
@keyframes doodle-seed {
  0%, 22%    { opacity: 0; transform: translate(-50%, -50%) scale(1.5) rotate(-8deg); }
  25%        { opacity: 1; transform: translate(-50%, -50%) scale(1.04) rotate(3deg); }
  27.5%      { opacity: 1; transform: translate(-50%, -50%) scale(.9) rotate(0deg); }
  29.5%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(.32) rotate(0deg); }
}
/* The puff itself, blowing outwards. It peaks at 29% — the frame the character
   starts growing out of, so the two read as one event and not as two. */
@keyframes doodle-poof {
  0%, 26%   { opacity: 0; transform: translate(-50%, -50%) scale(.25); }
  29%       { opacity: 1; transform: translate(-50%, -50%) scale(.9); }
  35%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.8); }
}
/* The character out of the puff, with the over-shoot in the easing rather than in
   an extra frame — a spring settling, not a picture fading up. */
@keyframes doodle-pop {
  0%, 28%  { opacity: 0; transform: scale(.14); }
  33%, 95% { opacity: 1; transform: scale(1); }
  100%     { opacity: 0; transform: scale(.14); }
}
/* The bar reading him over once he has landed. */
@keyframes doodle-scan {
  0%, 34%     { opacity: 0; transform: translateY(0); }
  35.5%       { opacity: 1; transform: translateY(160%); }
  40%         { opacity: 1; transform: translateY(1011%); }
  41.5%, 100% { opacity: 0; transform: translateY(1011%); }
}
/* The gleam crossing the finished drawing, right behind the bar. */
@keyframes doodle-shine {
  0%, 41%   { transform: translateX(-130%); }
  46%, 100% { transform: translateX(130%); }
}
/* The game screen dropping onto the second arrow's point, a beat after that arrow
   lands at 43.3%. Same held-back arrival as the drawing's page above. */
@keyframes doodle-land {
  0%, 44%  { opacity: 0; transform: rotate(-4deg) scale(.72); }
  47%      { opacity: 1; transform: rotate(2.6deg) scale(1.06); }
  50%, 95% { opacity: 1; transform: rotate(1.2deg) scale(1); }
  100%     { opacity: 0; transform: rotate(-4deg) scale(.72); }
}
/* Each note written just after the card it labels has finished landing. */
@keyframes doodle-note-a {
  0%, 8%   { opacity: 0; transform: translateY(5px) rotate(-5deg); }
  11%, 95% { opacity: 1; transform: none; }
  100%     { opacity: 0; transform: translateY(5px) rotate(-5deg); }
}
@keyframes doodle-note-b {
  0%, 31%  { opacity: 0; transform: translateY(5px) rotate(5deg); }
  34%, 95% { opacity: 1; transform: none; }
  100%     { opacity: 0; transform: translateY(5px) rotate(5deg); }
}
@keyframes doodle-note-c {
  0%, 49%  { opacity: 0; transform: translateY(5px) rotate(-4deg); }
  52%, 95% { opacity: 1; transform: none; }
  100%     { opacity: 0; transform: translateY(5px) rotate(-4deg); }
}
/* One tile of city slides past. A third of a five-tile strip is one whole tile,
   so the loop point is invisible. */
@keyframes doodle-scroll {
  to { transform: translate3d(-33.3333%, 0, 0); }
}
/* One pass of the run cycle: frames one to three of the four on the sheet. 100%
   would land on the fourth (the jump); 66.667% is the third. */
@keyframes doodle-run {
  from { background-position-x: 0%; }
  to   { background-position-x: 66.667%; }
}

/* ---- Narrower screens ---- */
/* Once the two columns can't sit side by side the strip wraps under the copy, and
   both stop centring: on a phone the words should start at the same left edge as
   everything else on the page, not float in the middle of it. The collage shrinks
   with the viewport from its one --art size, so it always fits the width without a
   sideways scroll — at 320px it is 260px wide inside 280px of room. */
@media (max-width: 900px) {
  .hero-copy {
    justify-content: flex-start;
    gap: clamp(22px, 5vw, 34px);
  }
  .hero-lede { flex: 1 1 100%; }
  /* the squeeze is over — this column has the whole width now, so the headline
     goes back to filling it (still on one line, still with room to spare) */
  .tagline { font-size: clamp(30px, 6.2vw, 46px); }
  .hero-strip { margin-top: 0; --art: clamp(260px, 44vw, 392px); }
}

/* Phone. The same layout with the air let out of it — the hero has one screen to
   land in and a phone's screen is the shortest one it gets. */
@media (max-width: 560px) {
  .hero-copy { padding-block: clamp(20px, 3.5vh, 40px); }
  .hero-line { margin: 18px 0 4px; }
  .hero-promises { gap: 10px; margin: 16px 0; }
  .hero-strip { gap: 0; }
}

/* Phone held upright — the scrapbook goes away completely. It is a wide, three-card
   collage with handwriting around it; stacked under the copy on a portrait phone it
   costs more than a screen of scrolling and pushes the one button that matters off
   the bottom. The left column says the same thing in words, and the game playing
   behind it still shows what the cards show. Held sideways there is width for it, so
   this is portrait only. (landing_hero.js re-measures on resize and finds no strip
   here — its "that's you!" note then has the whole stage to write in.) */
@media (max-width: 560px) and (orientation: portrait) {
  .hero-strip { display: none; }
  /* With the collage gone the copy is much shorter than the screen, and centred in
     all that height it read as adrift. It sits near the top instead — near, not
     against it: the padding below keeps a band of the game visible above the title,
     which is what tells you the backdrop is playing. */
  .hero { align-items: flex-start; }
  .hero-copy { padding-top: clamp(48px, 13vh, 112px); }
}

/* Phone held sideways (and any short, wide window). There is width to spare and
   almost no height, so the columns go back to sitting side by side — this is the
   one place they are told never to wrap — and everything comes down a size or two
   so the button still lands above the fold. */
@media (orientation: landscape) and (max-height: 620px) {
  .hero-copy {
    flex-wrap: nowrap;
    justify-content: space-between;
    gap: clamp(18px, 3vw, 34px);
    padding: 14px clamp(16px, 3vw, 34px);
  }
  .hero-lede {
    flex: 0 1 auto;
    min-width: 0;
  }
  .hero-title { font-size: clamp(24px, 4.2vw, 38px); }
  .hero-line { margin: 14px 0 2px; }
  .tagline { font-size: clamp(22px, 3.4vw, 32px); }
  .tagline-rule { height: 9px; }
  .hero-promises { gap: 7px; margin: 12px 0 14px; }
  .hero-promise { gap: 10px; }
  .hero-promise-mark { width: 24px; height: 24px; }
  .hero-promise-text { font-size: clamp(16px, 2.2vw, 20px); }
  .hero-strip {
    margin-top: 0;
    --art: clamp(200px, 30vw, 284px);
  }
}

/* ---- Reduced motion ---- */
/* Every resting state up in the strip is already the FINISHED one — the photo in
   the picker, the character printed, the arrows inked, the notes written — so
   switching the film off is the whole fix here. No second set of rules describing
   what "off" looks like, and nothing left half-drawn. The parts that only ever
   exist DURING the film (the empty slot, the puff, the passes of light) rest at
   opacity 0, so they simply never appear. */
@media (prefers-reduced-motion: reduce) {
  .doodle-piece,
  .doodle-card-toon,
  .doodle-card-scene,
  .doodle-slot,
  .doodle-photo-drop,
  .doodle-sweep,
  .doodle-flash,
  .doodle-edge-pick path,
  .doodle-ink,
  .doodle-seed,
  .doodle-poof,
  .doodle-toon-art,
  .doodle-scan,
  .doodle-shine,
  .doodle-cap,
  .doodle-sky,
  .hero-tick {
    animation: none;
  }
  /* the pieces rest mid-bob, and the two held-back cards keep only their tilt */
  .doodle-piece { transform: none; }
  .doodle-card-toon { transform: rotate(-3.6deg); }
  .doodle-card-scene { transform: rotate(1.2deg); }
  /* frame 1 of the sheet, feet planted — a still pose, not a frozen mid-jump */
  .doodle-hero {
    animation: none;
    background-position-x: 0%;
  }
}
