/* The alley: the cardboard puppet stage that opens the home page.
 *
 * This file is the one source. prototype/scene.html links it too, so changes
 * tried in the prototype are already the real thing -- there is no second copy
 * to keep in step.
 *
 * The art it places is cut and composited by the tools in the repo root. A
 * re-pulled prop needs nothing here unless the staging itself changes.
 */

@property --lx { syntax: '<length>'; inherits: true; initial-value: 17px; }
@property --ly { syntax: '<length>'; inherits: true; initial-value: 21px; }

* { box-sizing: border-box; }


.frame {
  container-type: inline-size;
  container-name: stage;
  width: 100%;
}

/* The home page is the scene and nothing else, so it fills the window and
   centres in it rather than leaving room underneath. svh rather than vh so a
   phone's collapsing address bar does not make it jump. */
.alley {
  min-height: 100svh;
  display: grid;
  place-items: center;
}
.alley .stage { width: min(100%, calc(97svh * 16 / 9)); }

.stage {
  position: relative;
  /* Cap by WIDTH, not height: width keeps the aspect ratio and with it every
     prop's placement, while capping height squashes the stage and scatters
     them. .alley below sets what that cap is. */
  width: min(100%, calc(74svh * 16 / 9));
  margin-inline: auto;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  isolation: isolate;
  perspective: 1400px;    /* so the hung things flip in depth, not just squash */
  animation: light 23s ease-in-out infinite alternate;
}
@keyframes light {
  from { --lx: 12px; --ly: 19px; }
  to   { --lx: 23px; --ly: 24px; }
}

.stage img, .stage .layer, .stage .rig, .stage a { position: absolute; display: block; }

/* A navigating prop is an <a> wrapping its art, so the link's box is the prop's
   box and the hit area is exactly the thing you can see. Every such prop is
   sized by width for this reason: a height-sized anchor has no width to
   shrink-fit to and collapses to nothing. */
/* site.css desaturates every <img> for the aged look. Inside the stage the
   lighting is the scene's own job, and a prop's filter sits on its wrapper --
   the anchor or the heading -- so the inner image must not add a second one on
   top of it. The cardboard is already the aged look. */
.stage a > img { position: static; display: block; width: 100%; height: auto; filter: none; }

/* The marquee is the page's <h1> on the site and a <p> in the prototype, so it
   is neither an img nor an anchor and needs positioning of its own. */
.stage .marquee { position: absolute; margin: 0; }
.stage .marquee > img { position: static; display: block; width: 100%; height: auto; filter: none; }
/* site.css gives every link a dotted underline and fades it on hover. On a
   prop that draws a dotted rule across the scene at the bottom of each hit box
   and washes the art out on hover, so the stage opts out of both. The label
   painted on the prop is the affordance; focus still shows a ring. */
.stage a { border-bottom: 0; }
.stage a:hover { opacity: 1; }
.stage a:focus-visible { outline: 3px solid #ffd27a; outline-offset: 4px; }

/* --- depth tiers -------------------------------------------------------- */
/* Shadow offset is a multiple of the shared light vector, so every prop agrees
   on where the lamp is. Standing props carry a second, tighter shadow. */

.back  { filter: brightness(.58) saturate(.8); }
.wall  { filter: brightness(.78) saturate(.96)
                 drop-shadow(calc(var(--lx) * .2) calc(var(--ly) * .2) 6px rgba(0,0,0,.5)); }
.taped { filter: brightness(.97)
                 drop-shadow(calc(var(--lx) * .12) calc(var(--ly) * .12) 3px rgba(0,0,0,.5)); }
.set   { filter: brightness(.98)
                 drop-shadow(2px 5px 5px rgba(0,0,0,.55))
                 drop-shadow(calc(var(--lx) * .8) calc(var(--ly) * .8) 14px rgba(0,0,0,.6)); }
.front { filter: brightness(var(--lit, 1.08))
                 drop-shadow(2px 5px 5px rgba(0,0,0,.5))
                 drop-shadow(var(--lx) var(--ly) 20px rgba(0,0,0,.62)); }
.hung  { filter: brightness(1.06)
                 drop-shadow(calc(var(--lx) * 1.2) calc(var(--ly) * 1.2) 24px rgba(0,0,0,.55)); }
.box   { filter: brightness(1.04)
                 drop-shadow(calc(var(--lx) * 1.6) calc(var(--ly) * 1.6) 30px rgba(0,0,0,.7)); }

/* --- how things are held ------------------------------------------------ */

/* On the end of a rod held from below. The pivot sits well past the bottom of
   the prop -- off the stage for the beast and the can -- so a small rotation
   reads as an arc across, not as the prop twisting on the spot. */
@keyframes stick {
  from { transform: rotate(calc(var(--tilt, 0deg) - var(--r))); }
  to   { transform: rotate(calc(var(--tilt, 0deg) + var(--r))); }
}
.stick {
  transform-origin: var(--pivot-x, 50%) var(--pivot);
  animation: stick var(--t) ease-in-out infinite alternate;
}

/* Hung from a line, flipping up toward the viewer and away about that line. */
@keyframes flip {
  from { transform: rotateX(calc(var(--a) * -1)); }
  to   { transform: rotateX(var(--a)); }
}
.flip {
  transform-origin: 50% var(--p, 0%);
  animation: flip var(--t) ease-in-out infinite alternate;
}

/* Nothing is holding the cans, so they do not swing on anything: they drift in
   a small orbit and lean as they go, the way something light sitting on the
   ground gets nudged about. The orbit and the lean are separate animations on
   separate periods, which is only possible because `translate` and `rotate` are
   their own properties -- two keyframes on `transform` would fight each other.
   Different periods mean the lean never lines up with the same point of the
   orbit twice, so it reads as drift rather than a loop. */
@keyframes orbit {
  0%   { translate: var(--o) 0; }
  25%  { translate: 0 calc(var(--o) * -1); }
  50%  { translate: calc(var(--o) * -1) 0; }
  75%  { translate: 0 var(--o); }
  100% { translate: var(--o) 0; }
}
@keyframes lean {
  from { rotate: calc(var(--lr) * -1); }
  to   { rotate: var(--lr); }
}
.drifts {
  animation: orbit var(--t) linear infinite,
             lean var(--t2) ease-in-out infinite alternate;
}

@keyframes breathe {
  from { transform: scale(1.03) translateX(-.4%); }
  to   { transform: scale(1.06) translateX(.4%); }
}

/* --- placement: landscape ----------------------------------------------- */

.curtain  { inset: 0; width: 100%; height: 100%; object-fit: cover;
            animation: breathe 31s ease-in-out infinite alternate; }
.wall-img { left: 0; bottom: 0; width: 100%; }

/* fixed */
/* The Sauce poster is paper, not cardboard, and is pasted up crooked. It is the
   only printed thing on the stage, which is the point: the charity is real and
   exists outside this make-believe.

   It casts nothing, in either layout. It is in the wall's own plane, so a cast
   shadow reads as the poster floating in front of the bricks rather than being
   stuck to them -- the one place in this scene where no shadow is the honest
   answer. Depth comes from the date sign overhanging its top edge instead. */
.poster { filter: brightness(.92) saturate(.98); }
.dumpster { right: 6%;  bottom: 7%; width: 25%; }
.poster   { left: 25%;  top: 38%;   width: 13%; transform: rotate(-1.8deg); }
.flyer    { left: 5%;   top: 30%;   width: 16%; }
/* The cans are small, brown and sat in front of a brown wall, so they need
   more light than anything else just to be findable. */
.tincans  { left: 59%;  bottom: 18%; width: 12%; --lit: 1.32;
            --o: 2.5px; --lr: 1.7deg; --t: 14.5s; --t2: 9.7s; }

/* on a stick */
.marker   { left: 16%; bottom: 6%; width: 16%; --r: 4deg; --t: 11.7s;
            /* turns about the foot of its stake, as on the phone */
            --pivot-x: 14.5%; --pivot: 105%; --tilt: -15deg; }
.beast    { left: 38%; bottom: 7%; height: 62%; --r: .65deg; --t: 7.3s;  --pivot: 265%; }
.can      { right: 9%; bottom: 18%; width: 6.62%; --r: 1.5deg; --t: 6.4s; --pivot: 320%; }

/* hung. The rig is sized so the flags are right; only the twine tails are
   stretched outward, far enough to leave the frame. */
.rig-pennants { left: 14%; top: 6%; width: 72%; }
.datesign { right: 6%; top: 27%; width: 19%; --a: 5deg; --t: 8.6s; --p: 0%; }

.rail-t { left: 0; top: 0;    width: 100%; }
.rail-b { left: 0; bottom: 0; width: 100%; }
.rail-l { left: 0;  top: 0; height: 100%; width: 5%; object-fit: fill; }
.rail-r { right: 0; top: 0; height: 100%; width: 5%; object-fit: fill;
          transform: scaleX(-1); }
.marquee { left: 50%; top: .6%; width: 44%; transform: translateX(-50%);
           filter: brightness(1.06)
                   drop-shadow(calc(var(--lx) * .3) calc(var(--ly) * .3) 4px rgba(0,0,0,.6)); }

.stage .portrait-only { display: none; }

/* --- the pennant rig ---------------------------------------------------- */
/* One image per flag, cut by tools/split_pennants.py and placed from the
   manifest it writes. The twine stays inside the bands and runs along each
   flag's rotation axis, so it barely moves and stays continuous across cuts. */

/* The rig and its rows are transparent boxes spanning the whole bunting, and
   they swallow every click across that area -- including over REGISTER, which
   they overlap. In a design where the props ARE the navigation that is a real
   bug, so only the flags themselves are hit-testable. */
.rig, .rig-row { pointer-events: none; }
.rig img { pointer-events: auto; }

.rig { perspective: 900px; }

/* Each swag is its own group, so a row can be raised, shifted or tilted without
   touching the other. --dx/--dy are fractions of the rig, --rot tilts the whole
   row: a negative angle drops its left end, which is how a real string hangs
   when one nail is lower than the other. */
.rig-row {
  position: absolute;
  inset: 0;
  transform: translate(var(--dx, 0%), var(--dy, 0%)) rotate(var(--rot, 0deg));
  transform-style: preserve-3d;
}
/* A tail is rope carried off the edge of the stage. The generated tails are
   only a couple of dozen pixels wide, so stretching one to reach the frame
   smears its twist into a smooth cord. Rope is a repeating texture along its
   own axis, so it tiles instead. */
.rig .rope {
  position: absolute;
  background-repeat: repeat-x;
  background-size: auto 100%;
  filter: brightness(1.06)
          drop-shadow(calc(var(--lx) * 1.2) calc(var(--ly) * 1.2) 20px rgba(0,0,0,.5));
}

.rig img {
  position: absolute;
  filter: brightness(1.06)
          drop-shadow(calc(var(--lx) * 1.2) calc(var(--ly) * 1.2) 20px rgba(0,0,0,.5));
}

/* --- the lamp ----------------------------------------------------------- */

/* Written on the bottom rail rather than laid over it: multiply lets the
   cardboard's grain and shadows come through the ink, so it reads as something
   someone wrote on the frame instead of a caption sitting on top of it. It
   comes after the rails in the markup so it has something to blend with. */
.credit {
  position: absolute;
  left: 50%; bottom: 5.4%;
  transform: translateX(-50%) rotate(-.7deg);
  margin: 0;
  font-family: "Reenie Beanie", "Bradley Hand", "Segoe Script", cursive;
  font-size: 2.3cqw;
  letter-spacing: .02em;
  white-space: nowrap;
  color: #261c0e;
  mix-blend-mode: multiply;
  opacity: .92;
  pointer-events: none;
}

/* A second, tighter lamp on REGISTER. The scene's own light is deliberately
   even, so the one thing the site most needs found gets its own pool. Screen
   blend rather than a brightness filter on the sign: it lifts the wall around
   it too, which is what makes it read as a light rather than a brighter prop,
   and it costs no filter re-render. */
.spot {
  position: absolute;
  inset: 0; width: 100%; height: 100%;
  background: radial-gradient(var(--sw) var(--sh) at var(--sx) var(--sy),
              rgba(255,228,176,.52), rgba(255,214,150,.20) 46%, transparent 72%);
  mix-blend-mode: screen;
  pointer-events: none;
  --sx: 13%; --sy: 41%; --sw: 15%; --sh: 21%;
  animation: flicker 17s linear infinite;
}

/* Mostly steady, with two short stutters per cycle. A cheap bulb catching,
   not a strobe -- the gaps between are the point, so the eye is drawn back
   rather than nagged. */
@keyframes flicker {
  0%, 26%, 100%   { opacity: 1; }
  27%             { opacity: .42; }
  27.8%           { opacity: .95; }
  28.4%           { opacity: .55; }
  29.2%, 68%      { opacity: 1; }
  68.6%           { opacity: .5; }
  69.1%           { opacity: 1; }
  69.7%           { opacity: .38; }
  70.4%           { opacity: .88; }
  71%             { opacity: 1; }
}

.wash {
  inset: 0; width: 100%; height: 100%;
  background: radial-gradient(52% 50% at 36% 24%,
              rgba(255,216,152,.55), rgba(255,192,112,.18) 46%, transparent 72%);
  mix-blend-mode: overlay;
  pointer-events: none;
}
.vignette {
  inset: 0; width: 100%; height: 100%;
  background:
    radial-gradient(88% 84% at 40% 34%, transparent 40%, rgba(0,0,0,.46) 100%),
    linear-gradient(to top, rgba(0,0,0,.20), transparent 28%);
  pointer-events: none;
}

/* --- portrait ----------------------------------------------------------- */
/* A tighter shot with fewer, bigger props, not the same scene scaled down. The
   side rails go thinner, and the marquee loses its cat bookends. */

@container stage (max-width: 560px) {
  .stage { aspect-ratio: 39 / 64; width: min(100%, calc(97svh * 39 / 64)); }
  .stage .landscape-only { display: none; }
  .stage .portrait-only  { display: block; }

  /* Ten props do not fit a phone by shuffling, so this is laid out in bands:
     bunting across the top, REGISTER clear beneath it, the beast lower and
     smaller than its landscape self, and the floor shared by the arrow, the
     cans and the dumpster. REGISTER is the one that must never be covered. */
  /* One swag of eight, not two of four. The flags are small enough to read
     across a phone now, and a single row costs about half the vertical band. */
  .rig-pennants { left: 4%; top: 7%; width: 92%; }

  /* The date sign hangs behind the bunting here too, which is what frees the
     band beneath it. */
  .datesign { left: auto; right: 4%; top: 23.5%; width: 40%; }

  .flyer    { left: 3%;   top: 24%;    width: 46%; }
  .beast    { left: 28%;  bottom: 3%;  height: 41%; --pivot: 230%; }
  .can      { right: 8%;  left: auto;  bottom: 6%;  width: 11.2%; --pivot: 300%; }
  .poster   { left: auto; bottom: auto; right: 3%; top: 47.5%; width: 31%; }

  /* With the poster up on the wall the corner is free again, so the dumpster
     comes back into frame and the can stands on the floor in front of it. */
  .dumpster { right: -6%;  bottom: 2%; width: 36%; }

  /* Contact stays. The floor in front of the arrow's stake is where a pair of
     cans would end up anyway, so they sit on the lip there. */
  .marker   { left: -10.2%; bottom: 21.5%; width: 36.2%;
              /* turns about the foot of its stake, just outside the frame:
                 a long arm, so a small angle carries the head a long way */
              --pivot-x: 14.5%; --pivot: 152%;
              --tilt: 15deg; --r: 1.2deg; --t: 9.1s; }
  .tincans  { left: 2%;   bottom: 7%;  width: 33%; }

  .rail-l, .rail-r { width: 3.5%; }
  .marquee { width: 80%; }
  .spot    { --sx: 26%; --sy: 35%; --sw: 31%; --sh: 15%; }
  .credit  { font-size: 3.4cqw; bottom: 1.2%; }
  .wash { background: radial-gradient(60% 34% at 40% 20%,
          rgba(255,216,152,.55), rgba(255,192,112,.18) 46%, transparent 72%); }
}

@media (prefers-reduced-motion: reduce) {
  .stage, .stick, .flip, .drifts, .curtain, .rig img,
  .spot { animation: none !important; }
}

/* --- child pages -------------------------------------------------------
   Same props, same system, smaller. The brief expected whole props to be
   unreadable at strip height and planned cropped details instead; measured at
   64px every label still reads, SPONSORS included, because the second pennant
   pull made the flags small and the string wide. Whole props it is.

   The page's own prop is the big one in the header band, so it is left out of
   the strip: the strip is everywhere else, and the marquee above it is home. */

.child-head{
  display:flex; flex-direction:column; align-items:center;
  gap:.4rem;
  padding:1rem clamp(.8rem,3vw,2rem) .2rem;
}
.home-mark{ display:block; border-bottom:0; }
.home-mark img{
  display:block; filter:none;
  width:clamp(170px, 26vw, 260px); height:auto;
}
.home-mark:hover{ opacity:1; }

.prop-strip{
  display:flex; flex-wrap:wrap; align-items:flex-end; justify-content:center;
  gap:clamp(.9rem, 3vw, 2.2rem);
  padding:.5rem 0 .2rem;
}
.prop-strip a{ display:block; border-bottom:0; }
.prop-strip a:hover{ opacity:1; }
.prop-strip img{ display:block; height:64px; width:auto; filter:none; }

.prop-strip a:focus-visible{ outline:3px solid #ffd27a; outline-offset:4px; }
@media (max-width:640px){ .prop-strip img{ height:52px; } }

/* The page's own prop, large, so you can see where you landed. */
.page-head{ text-align:center; padding:1.4rem 1rem .2rem; }
.page-head > img{
  display:block; margin:0 auto .9rem; filter:none;
  height:clamp(120px, 17vw, 200px); width:auto;
}

/* --- hover: only the things that go somewhere --------------------------
   A prop that is a link lifts under the pointer; a prop that is only scenery
   does nothing at all. Gated behind hover:hover so a phone never carries a
   hover state -- that is what makes a tap navigate first time instead of
   painting the hover and waiting for a second tap. */
@media (hover:hover) {
  .stage a > img { transition: filter .18s ease; }
  .stage a:hover > img,
  .stage a:focus-visible > img { filter: brightness(1.22) contrast(1.03); }

  .rig-pennants:hover img.flip,
  .rig-pennants:focus-visible img.flip { filter: brightness(1.22) contrast(1.03); }

  .prop-strip a img { transition: filter .18s ease; }
  .prop-strip a:hover img { filter: brightness(1.22) contrast(1.03); }
  .home-mark img { transition: filter .18s ease; }
  .home-mark:hover img { filter: brightness(1.18); }
}
