/* /film — the reel, in theatre mode.

   Juan chose Theatre over Projection, and asked for somewhere real to put the
   credits rather than a one-line caption.

   CENTRED IN THE VIEWPORT, the same pattern as /code and the hero and every
   /work panel: the stage and its credits are one composition that sits in the
   middle of the screen, not a page that starts at the top.

   A REEL IS IMAGE-LED, so this one IS a slideshow, and that is not a
   contradiction of the tabbed trees on /code. The objection to carousels was
   never "one thing at a time" — it was auto-rotation and hidden slides. Here
   every film is named on a real thumbnail, the whole set is visible as a
   filmstrip the entire time, and the rotation stops the moment anyone touches
   it. What changes for a gallery is only the FORMAT: a frame wants the width,
   and five stacked would bury the last three. */

/* ⚠️ THE MAIN IS NOT `.wrap` ANY MORE, and that was the whole bug. `.wrap` is a
   READING measure — 940px max, minus 7vw of padding a side — so on a 1920px
   display it strangled the theatre monitor down to 671px and left the rest of
   the screen empty. Prose belongs in a reading measure; a screen does not. The
   stage is now sized to the VIEWPORT and only the text below it keeps a measure.

   And the header was sitting OUTSIDE this block, 272px of it, which is where the
   dead space above the monitor came from. It is inside the composition now. */
.doc__main.film {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: clamp(14px, 2.2vh, 26px);
  padding: 4vh 4vw 3vh;
}

.doc__head--sub {
  min-height: 0;
  padding-block: 0;
  text-align: center;
}
/* the title is chrome around the media, not a headline competing with it */
.doc__head--sub .doc__h1 { font-size: clamp(22px, 2.6vw, 34px); margin: 0 0 8px; }
.doc__head--sub .doc__lede { font-size: clamp(13px, 1.3vw, 15px); }
.doc__head--sub .back { margin-bottom: clamp(12px, 1.8vh, 20px); }
.doc__head--sub .doc__h1 { max-width: none; margin-inline: auto; }
.doc__head--sub .doc__lede { margin-inline: auto; text-align: center; }

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

:root {
  /* ⚠️ ONE SOURCE FOR THE STAGE WIDTH. The credits were min(92vw, 1500px) while
     the monitor was min(74vw, 1180px), so the film's name started 130px to the
     LEFT of the picture it belongs to and the panel ran wider than the thing it
     captions. Two independent widths for one visual object is a drift waiting
     to happen; both read this now. */
  --stage-w: min(74vw, 1180px);
}
.film__stage {
  position: relative;
  /* AS BIG AS IT CAN BE AND STILL FIT. Width and height are BOTH bounded, and
     aspect-ratio reconciles them — so the monitor fills a wide screen and
     shrinks on a short one instead of overflowing. This is the content; it gets
     the room. */
  width: var(--stage-w);
  max-height: 48svh;
  margin: 0 auto;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #000;

  /* ⚠️ NOT FEATHERED ANY MORE, AND THE REASON IS THE CONTENT. The old mask
     dissolved 7% off each side and 9% off the top and bottom — 14% of the width
     and 18% of the height of every frame, gone. On a page whose whole job is
     showing that Juan can compose a shot, throwing away the edges of the
     composition is the one cost this treatment cannot justify. A cinematographer
     is hired on the frame.

     The instinct is to check the reference. It cannot answer: leonardomoreira.com.br
     has no photographs on it at all — 199 fillRect calls, everything a rectangle,
     all text mono. It is a terminal, not a hybrid.

     So the answer comes from the object being imitated. A CRT does not fade its
     picture to nothing at the edges; it has a DEFINED picture area with rounded
     corners and falls off in brightness toward them. That is a radius plus the
     vignette this stage already carries — both of which say "displayed by a
     tube" without deleting the image. The thumbnails keep their feather: they
     are ambience, and nobody is judging a composition at 70px.

     The 1.5% falloff is the phosphor edge, not a dissolve. */
  /* ⚠️ AND NOW NO FALLOFF AT ALL — Juan's call, and the right one. Even the
     1.5% edge was still softening the frame, and this page has exactly one job.
     The picture is crisp to its own boundary; what says "tube" is the CORNER
     RADIUS and a thin lit picture edge, not a dissolve.

     He named the reference's startup screen as the thing he liked about edges,
     with one exclusion: NOT its distortion. That is the same call the README
     already made against barrel warp — real CRT curvature baselines near 0.04
     and bending the picture costs legibility for nothing. So this takes the
     defined-picture-area half of that look and leaves the geometry alone.

     The inset rim is the phosphor edge: the picture area is slightly hotter at
     its boundary because the mask ends there. One pixel of amber at 30%, a soft
     inner bloom, and an outer halation that ties the frame to the machine
     around it. */
  border-radius: clamp(6px, 0.9vw, 14px);
  /* ⚠️ "TOO CLEAN" HAD A MEASURABLE ANSWER, AND IT WAS NOT MORE SCANLINES.
     Read off the reference's own DOM rather than guessed at: it stacks THREE
     layers over its picture, and this frame had one of them.

       .crt        radial vignette, and warm-dark (#0C0608) rather than neutral
       .crt-bulge  a warm CENTRE LIFT — the tube is brighter in the middle of
                   its face. It is the bulge read WITHOUT the geometry, which
                   is exactly the half Juan wants and the half the README keeps
                   (barrel warp stays rejected)
       .crt-glass  a SPECULAR thrown from 22% / -8% — an origin OFF the glass
                   entirely, so it reads as a reflection of the room rather
                   than as a gradient

     The page already carries all three. The frame carried only the vignette, so
     the picture sat inside it as a perfectly even rectangle — which is what
     "clean" was. The emulation literature names the same two things this was
     missing: HALATION, light bouncing under the faceplate, and DIFFUSION, the
     glow of light passing out through imperfect glass. Both ADD light, which is
     why the layer below screens rather than overlays.

     And the glass has THICKNESS. There is a dark lip just inside the boundary
     before the lit edge — the tunnel of the faceplate. That inset shadow is
     doing more work here than anything else: a bright line on a crisp edge
     reads as a border, the same line over a dark lip reads as an edge lit from
     behind. */
  box-shadow: inset 0 0 0 1px rgba(240, 168, 60, .34),
              inset 0 0 12px rgba(0, 0, 0, .58),
              inset 0 0 34px rgba(240, 168, 60, .10),
              0 0 60px rgba(240, 168, 60, .10),
              0 0 150px rgba(240, 168, 60, .05);
}
/* the glass over the picture: the room reflected in it, and the face's own
   centre lift. Screen, because both are light being ADDED. */
.film__stage::before {
  content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  border-radius: inherit;
  mix-blend-mode: screen;
  /* ⚠️ SCANLINES ADDED HERE Aug 31 2026, on Juan's note that the monitor looked
     too clean. The THUMBNAILS already carry this page's full CRT treatment — the
     stage had the glare and the vignette and no lines at all, so the one surface
     holding the actual work was the only one that did not look displayed.
     They go in THIS stack rather than a new layer because both pseudo-elements
     are already spoken for (::before is this glare, ::after is the vignette) and
     because screen-blended black is exactly what belongs with a glare: it does
     nothing to the dark parts of a frame and bites only on the lit ones. Kept
     light — this is the content, and it should read as displayed, not damaged. */
  background:
    repeating-linear-gradient(to bottom, rgba(0, 0, 0, .15) 0 1px, transparent 1px 3px),
    radial-gradient(80% 48% at 20% -10%, rgba(214, 234, 255, .075), transparent 60%),
    radial-gradient(74% 60% at 50% 44%, rgba(255, 220, 160, .055), transparent 66%);
}
.slide {
  position: absolute; inset: 0; margin: 0;
  opacity: 0; transition: opacity 1.6s ease;
}
.slide.is-on { opacity: 1; }
.slide picture, .slide img { display: block; width: 100%; height: 100%; }
.slide img {
  object-fit: cover;
  filter: saturate(.86) contrast(1.03) brightness(.88);
  /* the drift is what makes a still frame feel like it is being PROJECTED
     rather than displayed — it is the one motion a cinema has */
  animation: film-drift 20s ease-in-out infinite alternate;
}
@keyframes film-drift {
  from { transform: scale(1.04) translate(0, 0); }
  to   { transform: scale(1.11) translate(-1.4%, -1%); }
}

/* the room the frame sits in */
.film__stage::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  /* the tube's own falloff. It does more work now that the mask is not
     dissolving the edges — this is what keeps the frame from reading as a
     rectangle pasted on the page. */
  /* warm-dark, not neutral — the reference's own vignette runs #0C0608 to
     #020001, and a neutral grey falloff is one of the things that reads as
     "a CSS effect" rather than as a tube. */
  background: radial-gradient(122% 96% at 50% 45%, transparent 46%,
              rgba(12, 6, 8, .42) 78%, rgba(2, 0, 1, .80) 100%);
}

/* ----------------------------------------------------------- the credits --- */

/* THE CREDITS ARE A PANEL, NOT A CAPTION. A caption fits a title; this has to
   carry the role, the format and the year — the things somebody hiring a
   cinematographer actually reads. It lives OUTSIDE the slides on purpose:
   inside, two of them rendered on top of each other through every dissolve. */
.film__credits {
  /* ⚠️ ONE SOURCE FOR THE LABEL METRICS. The film's name sat lower than the
     copy beside it because the two columns have different rhythms: the meta
     column starts with an 8.5px label and puts its value underneath, while the
     title is a 32px line starting at the top of the grid. `align-items: start`
     aligns the BOXES, which is exactly why it looked wrong — the ink does not
     start where the box does. The title is pushed down by one label row so the
     two read off the same line, and both sides take the label size and gap from
     these two properties, so nudging one can never desync them. */
  --cred-label: 8.5px;
  --cred-gap: 3px;
  width: var(--stage-w); margin: 0 auto;
  display: grid; gap: 14px clamp(20px, 3vw, 44px);
  grid-template-columns: 1fr;
  text-align: center;
}
@media (max-width: 779px) { .film__t { padding-top: 0; } }
@media (min-width: 780px) {
  .film__credits { grid-template-columns: auto 1fr; text-align: left; align-items: start; }
}
.film__t {
  margin: 0 !important;
  padding-top: calc(var(--cred-label) * 1.5 + var(--cred-gap));
  font-size: clamp(21px, 2.8vw, 32px); font-weight: 800;
  letter-spacing: -.018em; line-height: 1.05; color: var(--fg);
}
.film__meta {
  display: flex; flex-wrap: wrap; gap: 6px clamp(14px, 2vw, 26px);
  justify-content: center;
}
@media (min-width: 780px) { .film__meta { justify-content: flex-start; } }
.film__meta div { display: flex; flex-direction: column; gap: var(--cred-gap); }
.film__meta dt {
  font-family: var(--mono); font-size: var(--cred-label); letter-spacing: .2em;
  line-height: 1.5;
  text-transform: uppercase; color: var(--fg-dim); opacity: .7;
}
.film__meta dd {
  margin: 0;
  font-family: var(--mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--amber);
}
.film__meta dd.plain { color: var(--fg); opacity: .85; }

/* ---------------------------------------------------------- the filmstrip --- */

/* ------------------------------------------------------------- the orbit ---

   ⚠️ THE FIRST VERSION WAS NOT FLOATING, IT WAS A GRID. Two tidy arcs under the
   monitor, every thumbnail the same size, drifting six pixels — which reads as
   two rows that wobble, not as objects in space. Juan said so immediately.

   What actually makes a field read as floating is not the motion, it is the
   DEPTH. Things at different distances are different sizes, different
   brightnesses, and move different amounts — so this gives every thumbnail a
   depth from 0 to 1 and derives scale, opacity, blur and drift distance from it
   together. Near ones are bigger, brighter and travel further; far ones are
   small, dim, slightly soft and barely move. That is parallax, and it is the
   whole trick.

   They also surround the monitor now rather than sitting under it, which is what
   "floating around it" meant.

   Still deliberately quiet — this is the shape the README rejected four times,
   and eleven thumbnails whirling around a film frame would be that mistake a
   fifth time. Long cycles, low opacity, and nothing crosses the frame. */

.film__strip {
  position: absolute; inset: 0;
  z-index: 1;
  pointer-events: none;              /* the field is not a wall; only the thumbs catch clicks */
}

/* ⚠️ THE PAGE LABEL SITS ABOVE THE ORBIT. The ring passes over the top-centre
   of the page, where the vertical page mark lives — measured at 38 of 2200
   samples. There is nowhere else for the path to go: the band above the frame is
   189px tall and the label stands in the middle of it, so routing around it
   would mean not orbiting. A dim, blurred thumbnail drifting BEHIND the mark
   costs nothing and reads as depth; the mark going illegible would not. Recall
   never fires up there either — see atSide() in film.js. */
.doc__head--sub { position: relative; z-index: 6; }

.film__orbit button {
  /* the path itself is chosen by class — see the seven @keyframes below */
  position: absolute;
  pointer-events: auto;
  padding: 0; border: none; background: none; cursor: pointer;
  line-height: 0;
  /* --x/--y are PIXELS, set by the measured placement in film.js, and they are
     the top-left of the TRAVEL ENVELOPE rather than of the thumbnail — the
     wander is centred on where it was placed */
  left: var(--x, 0); top: var(--y, 0);
  /* ⚠️ DIM, NOT SEE-THROUGH — and the difference is the whole of Juan's note.
     Depth was carried by `opacity`, which does not just darken a thumbnail, it
     makes it TRANSPARENT: two of them crossing showed through each other, so a
     pass read as a glitch rather than as one thing going behind another. Dimness
     and translucency look similar alone and behave nothing alike when they
     overlap.

     Brightness carries the depth instead. A dim opaque thumbnail still reads as
     far away — that is how distance looks — while an occlusion now happens,
     which is the single strongest depth cue there is. z-index follows depth so
     the nearer one is the one in front, rather than whichever the DOM happened
     to list last. */
  opacity: 1;
  filter: blur(var(--soft, 0px)) brightness(var(--dimb, .55));
  z-index: var(--z, 1);
  /* ⚠️ THE SIZE LIVES ON THE .thumb CHILD, NOT HERE, AND THAT IS THE FIX FOR
     THE HOVER POPPING DOWNWARDS. The drift's second lane is `transform`, which
     composes AFTER `scale` in the individual-property order — so scaling this
     element on hover multiplied the drift offset too. Mid-travel at 40px down,
     a jump to 1.25 dragged the thumbnail another 10px DOWN as it grew, which is
     exactly the lopsided pop Juan reported. Scaling a child instead means the
     two never meet: this element only ever drifts, the child only ever grows,
     and the growth is symmetric about its own centre. */
  transition: opacity .4s ease, filter .4s ease;
  /* the wander, split across two periods that do not share a small common
     multiple — see film.js. One period on both axes is a straight line sliding
     back and forth, which is exactly what "it stays in the same place" looked
     like. Two makes a slow Lissajous that does not visibly repeat. */
  rotate: var(--tilt, 0deg);
  /* ⚠️ TWO LANES, EQUAL PERIODS, A QUARTER TURN APART — which is an ellipse.
     They used to run at different periods on purpose, to make a Lissajous that
     never repeats, because each thumbnail was drifting in place. They are
     orbiting a shared centre now, so the periods must MATCH: a cosine on
     `translate` and a sine on `transform`, one delay for both, and the phase
     offset lives in the keyframes rather than in the delay. `linear`, because
     an eased orbit speeds up and slows down twice a lap and reads as a wobble. */
  animation-duration: var(--dur, 168s);
  animation-timing-function: linear;
  animation-delay: var(--dly, 0s);
  animation-iteration-count: infinite;
  animation-direction: var(--dir, normal);
}

.film__orbit button.orb-p0 { animation-name: orb-p0; }
.film__orbit button.orb-p1 { animation-name: orb-p1; }
.film__orbit button.orb-p2 { animation-name: orb-p2; }
.film__orbit button.orb-p3 { animation-name: orb-p3; }
.film__orbit button.orb-p4 { animation-name: orb-p4; }
.film__orbit button.orb-p5 { animation-name: orb-p5; }
.film__orbit button.orb-p6 { animation-name: orb-p6; }

/* ---------------------------------------------------------------------------
   ⚠️ AND THEY HAVE TO LOOK DISPLAYED, NOT PASTED. Juan's read was that the
   thumbnails "felt too new" and did not belong in this world, and he was right
   for a reason the reference site cannot show him: leonardomoreira.com.br has
   no photographs on it at all. It is a terminal wholesale — 199 fillRect calls,
   everything a rectangle, all text mono. There is no thumbnail treatment there
   to copy.

   So the rule to apply is this site's OWN, already written for the portrait on
   /work: a photograph must look like it is being displayed BY the phosphor
   rather than sitting on top of it. The monitor gets that treatment and the
   thumbnails only got half of it — a mask and a brightness cut. They now get
   the rest of what this page already claims a CRT does to a picture:

   · scanlines, at a 3px pitch so they read at thumbnail size
   · a vignette, because a tube is dimmer at its corners
   · a phosphor tint — the amber the whole machine is lit in
   · doubled chromatic aberration, reusing the page's own #crt-chroma. It is
     tuned at 0.55px for text at full size; on a 70px thumbnail one pass is
     invisible, so the thumbnails take a second one.

   The mask moved from the img to the BUTTON so the overlay feathers with the
   picture instead of ending in a hard rectangle over a soft one. */

/* ⚠️ THE CLIP OCCUPIES THE STILL'S BOX RATHER THAN FOLLOWING IT. Appended as a
   sibling it laid itself out below the image, so a playing memory was a picture
   with a video under it — two things, when the whole idea is one thing
   remembered. Absolute, inset 0, object-fit: cover, and the still cross-fades
   out beneath it. Both sit inside .thumb, so the feather mask and the scanlines
   govern the footage exactly as they govern the picture. */
.film__orbit .thumb video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity .5s ease;
}
.film__orbit button.is-playing .thumb video { opacity: 1; }
.film__orbit button.is-playing .thumb img   { opacity: 0; }
.film__orbit .thumb img { transition: opacity .5s ease; }

.film__orbit .thumb {
  display: block; position: relative; line-height: 0;
  scale: var(--scale, 1);
  transition: scale .4s cubic-bezier(.2,.8,.25,1), filter .4s ease;
  -webkit-mask-image: radial-gradient(118% 118% at 50% 50%, #000 44%, transparent 100%);
          mask-image: radial-gradient(118% 118% at 50% 50%, #000 44%, transparent 100%);
}
.film__orbit img {
  /* ⚠️ SMALLER AT REST, AND IT BUYS BOTH OF THE THINGS JUAN ASKED FOR AT ONCE.
     He wanted rounder orbits and bigger memories, and at 92px those two fight
     over the same margin — the page is 187px from frame to edge and 101px below
     the credits, and a 92px thumbnail leaves no room for either. Measured: at
     that size NO closed path could clear the block at any exponent, so every
     orbit fell back to the squarest shape, and a memory could only reach 1.9x.
     At ~63px an exponent of 5 clears — a visibly oval path rather than a traced
     rectangle — and the memory cap goes to 2.4x. The resting thumbnail giving up
     a third of its width is what pays for both, and it also makes the memory
     read as more of a jump when it opens. */
  /* ⚠️ THE WIDTH COMES FROM film.js, WHICH SOLVES IT AGAINST THE PAGE. A fixed
     clamp had to be small enough for the tightest window it might ever meet, so
     it threw away every pixel of room a bigger display offers. The fallback here
     is only for the no-JS path, where nothing orbits and nothing has to fit. */
  display: block; width: var(--thumb-w, 68px); height: auto;
  /* ⚠️ 4:3, and it is a crop rather than a squash. These are 16:9 frames, and
     16:9 is a MODERN signal — it is most of why they read as new. 4:3 is the
     shape the machine around them broadcasts in, so a thumbnail in it reads as
     a channel rather than as a photograph someone dropped on the screen. The
     full frame is not lost: the monitor shows it, at 1500px, the moment one is
     picked. Revert by deleting these two lines. */
  aspect-ratio: 4 / 3;
  object-fit: cover;
  filter: saturate(.5) brightness(.66) sepia(.22) hue-rotate(-12deg)
          contrast(1.06) url(#crt-chroma);
}
/* scanlines + vignette, painted over the picture and travelling with it */
.film__orbit .thumb::after {
  content: ""; position: absolute; inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(to bottom,
      rgba(0, 0, 0, .34) 0 1px, transparent 1px 3px),
    radial-gradient(115% 115% at 50% 46%,
      transparent 34%, rgba(3, 5, 4, .5) 78%, rgba(3, 5, 4, .82) 100%);
  mix-blend-mode: multiply;
}
.film__orbit button:hover,
.film__orbit button:focus-visible { filter: blur(0) brightness(1.04); z-index: 40; }
.film__orbit button:hover .thumb,
.film__orbit button:focus-visible .thumb { scale: calc(var(--scale, 1) * 1.25); }
.film__orbit button:hover img,
.film__orbit button:focus-visible img {
  filter: saturate(.9) brightness(1) sepia(.06) contrast(1.02) url(#crt-chroma);
}
.film__orbit button:hover .thumb::after,
.film__orbit button:focus-visible .thumb::after { opacity: .55; }
/* ⚠️ THE FILM ON THE MONITOR IS NOT IN THE FIELD. Juan's note, and it is
   obviously right: the floating set is what you have NOT chosen, so showing the
   current film out there as well is the same picture twice, six inches apart.
   It fades rather than vanishing, and it keeps its place in the layout — the
   scatter is solved once and must not reshuffle every time somebody picks a
   different film. */
.film__orbit button.is-on {
  opacity: 0; pointer-events: none;
  transition: opacity .8s ease;
}

/* ---------------------------------------------------------------------------
   REMEMBERING. Juan's idea: the floating films should read as memories
   surfacing, not as a set of buttons — and when one surfaces it should be BIG,
   opening out into the space beside the monitor, with its edges dissolved.

   The literature on the flashback look is consistent about what sells it, and
   it is not the footage: a heavily FEATHERED vignette, a blurred copy of the
   image screened back over itself for the dream bloom, warm faded tone, and a
   soft rise rather than a cut. All four are here.

   ⚠️ IT GROWS SIDEWAYS, NEVER ACROSS THE STAGE. This page has exactly one rule
   it cannot break — nothing performs in front of the work — so a memory that
   expanded over the monitor would be the single worst thing this page could do.
   The margin beside the stage is (100vw - var(--stage-w)) / 2, which is 187px
   at 1440, and the growth is sized to live inside it. Each thumbnail also grows
   AWAY from the frame: film.js gives every one a --org matching the side it
   sits on, so a left thumbnail opens leftward and a right one rightward, and
   neither reaches inward.

   The clipless films get the same treatment on their still, so a memory is a
   memory whether or not there is footage for it. */
.film__orbit button.is-recalling {
  z-index: 2;
  /* a memory comes fully forward: no blur, and brightness past 1 so it reads as
     lit rather than merely un-dimmed. Opacity is no longer part of this — see
     the note above; it is opaque at every depth now. */
  opacity: 1;
  filter: blur(0) brightness(1.06);
  transition: filter 1.4s ease;
}
.film__orbit button.is-recalling .thumb {
  /* ⚠️ THE FALLBACKS HAVE TO BE THE SAFE ONES. film.js measures --org and --open
     per thumbnail so a memory opens AWAY from the monitor and only as far as the
     margin allows. If those never arrive — a script error, a stale cache, an
     entry path that skips placement — the animation still runs, and a generous
     default would open a memory straight across the stage. Rendered exactly that
     once: a centred origin at 2.4x put a film over the whole frame. So the
     defaults are the smallest thing that still reads as opening, and the
     measured values can only ever make it bigger. */
  transform-origin: 50% 50%;   /* the RING keeps it off the frame now, not a skewed origin */
  animation: recall-open 7s cubic-bezier(.25,.6,.2,1) both;
  filter: drop-shadow(0 0 26px rgba(255, 214, 150, .30))
          drop-shadow(0 0 60px rgba(240, 168, 60, .16));
  /* the edges dissolve much further than at rest — at rest the mask is opaque
     to 44%, remembering takes it to 22%, which is the feathering the flashback
     look actually turns on */
  /* ⚠️ FEATHERED FURTHER, ON JUAN'S READ THAT A MEMORY SHOULD NOT HAVE AN EDGE.
     At rest the mask is opaque to 44%; remembering now takes it to 8% and runs
     the falloff all the way out, so the frame has no border at all — it is a
     shape of light rather than a rectangle of video. This is the one place the
     feather can go this far: at rest a thumbnail still has to read as a
     selectable control, and dissolving its edges would make it look broken. */
  /* ⚠️ SOFTER AGAIN, on Juan's second read. The core is opaque for only the
     middle few per cent now and the falloff runs the whole way out, so a
     memory has no edge anywhere — it is a patch of light that happens to carry
     a picture. Anything harder reads as a video in a box. */
  -webkit-mask-image: radial-gradient(140% 140% at 50% 50%, #000 2%, rgba(0,0,0,.62) 42%, rgba(0,0,0,.22) 74%, transparent 100%);
          mask-image: radial-gradient(140% 140% at 50% 50%, #000 2%, rgba(0,0,0,.62) 42%, rgba(0,0,0,.22) 74%, transparent 100%);
}
/* ⚠️ THE DREAM BLOOM IS NOT A backdrop-filter. The first version screened a
   backdrop-filter layer over the frame, which is the technique every flashback
   guide describes — and it rendered as two grey RECTANGLES on the page. A
   backdrop-filter samples the page behind it and establishes its own containing
   context, so it ignored the parent's feather mask and painted a hard box out
   to the element's bounds. The mask is the whole effect here, so anything that
   escapes it is worse than no bloom at all.

   A drop-shadow glow plus a touch of blur on the frame itself gets the same
   read — light spreading out of a remembered image — using only properties the
   mask governs. */
.film__orbit button.is-recalling .thumb img,
.film__orbit button.is-recalling .thumb video {
  animation: recall-warm 7s ease-in-out both;
}
/* the scanlines drift downward through it — a tube locking onto a weak signal,
   which is the one motion that says "this is being played, not shown" */
.film__orbit button.is-recalling .thumb::after {
  animation: recall-roll 2.6s linear infinite;
  opacity: .34;
}
/* ⚠️ --open IS NOW A RATIO THAT LANDS EVERY MEMORY ON THE SAME SIZE, computed
   per thumbnail as TARGET / its own scale. It used to be a measured allowance of
   up to 3.4x on top of a scale that itself ran to 1.24, so one memory could open
   to four times another's — which is what "way too big" and "they should be the
   same size" were both describing. The ring is placed so a thumbnail at full
   TARGET still clears the frame, so this can no longer reach the monitor. */
@keyframes recall-open {
  0%   { scale: var(--scale, 1); }
  26%  { scale: calc(var(--scale, 1) * var(--open, 2.05)); }
  74%  { scale: calc(var(--scale, 1) * var(--open, 2.05)); }
  100% { scale: var(--scale, 1); }
}
@keyframes recall-bloom {
  0%, 100% { opacity: 0; }
  30%, 70% { opacity: .55; }
}
@keyframes recall-warm {
  0%   { filter: saturate(.5) brightness(.66) sepia(.22) hue-rotate(-12deg) contrast(1.06) url(#crt-chroma); }
  26%  { filter: saturate(.78) brightness(1.04) sepia(.16) contrast(1.02) blur(.5px) url(#crt-chroma); }
  74%  { filter: saturate(.78) brightness(1.02) sepia(.16) contrast(1.02) blur(.5px) url(#crt-chroma); }
  100% { filter: saturate(.5) brightness(.66) sepia(.22) hue-rotate(-12deg) contrast(1.06) url(#crt-chroma); }
}
@keyframes recall-roll { from { background-position: 0 0, 0 0; } to { background-position: 0 3px, 0 0; } }

/* a memory that arrives on its own schedule is ambience; one that arrives while
   you are reading is an interruption. Hover, focus and the chosen film all opt
   out, and reduced motion never starts it — see film.js. */
.film__orbit button:hover.is-recalling .thumb,
.film__orbit button:focus-visible.is-recalling .thumb { animation: none; }

/* two lanes, two periods. orbit-a owns `translate` and `rotate`; orbit-b owns
   `transform`. Amplitudes and periods are set per thumbnail from its measured
   envelope and its depth. */
/* ⚠️ SEVEN PATHS, NOT ONE, AND THE SET IS NOT DECORATION. Every thumbnail used
   to run the same superellipse, so the shape of the path became legible within
   seconds and four of them sat in the corners together — Juan's note, and the
   cause was that they were all the same curve.

   ⚠️ THE CLASS IS AN INDEX, NOT THE EXPONENT. It used to be built from the
   number — orb-n5.0 -> "orb-n50" — and that contract broke twice on number
   formatting alone: Python renders 5.0 as "5.0" and JavaScript renders it as
   "5", so the stylesheet said orb-n50 while the script asked for orb-n5 and
   those thumbnails simply never animated. They sat at the centre of the page,
   on the monitor, which read as a placement bug rather than a naming one. The
   exponent lives in the comment; the wire is an index.

   Each is a superellipse |x/rx|^n + |y/ry|^n = 1 at a different n: 3.4 is
   almost an oval, 10 hugs a rectangle. film.js does NOT pick one by taste — it
   takes the radius a thumbnail has been given and finds the roundest exponent
   that still clears the frame, because a rounder path has to be bigger to get
   around the same block. Outer thumbnails come out oval, inner ones stay
   squarer because nothing rounder fits.

   One lane, `translate: x y`. The two-lane split existed to make a Lissajous
   from independent oscillations; a closed parametric path is a list of points,
   and putting both coordinates in one keyframe is what allows the shape to vary
   per thumbnail at all. 24 stops — at 16 the flatter exponents visibly cut
   their corners. */
@keyframes orb-p0 {
  0%      { translate: var(--rx, 300px) 0; }
  4.1667% { translate: calc(var(--rx, 300px) * 0.9798) calc(var(--ry, 200px) * 0.4515); }
  8.3333% { translate: calc(var(--rx, 300px) * 0.9189) calc(var(--ry, 200px) * 0.6652); }
  12.5%   { translate: calc(var(--rx, 300px) * 0.8156) calc(var(--ry, 200px) * 0.8156); }
  16.6667%{ translate: calc(var(--rx, 300px) * 0.6652) calc(var(--ry, 200px) * 0.9189); }
  20.8333%{ translate: calc(var(--rx, 300px) * 0.4515) calc(var(--ry, 200px) * 0.9798); }
  25%     { translate: 0 var(--ry, 200px); }
  29.1667%{ translate: calc(var(--rx, 300px) * -0.4515) calc(var(--ry, 200px) * 0.9798); }
  33.3333%{ translate: calc(var(--rx, 300px) * -0.6652) calc(var(--ry, 200px) * 0.9189); }
  37.5%   { translate: calc(var(--rx, 300px) * -0.8156) calc(var(--ry, 200px) * 0.8156); }
  41.6667%{ translate: calc(var(--rx, 300px) * -0.9189) calc(var(--ry, 200px) * 0.6652); }
  45.8333%{ translate: calc(var(--rx, 300px) * -0.9798) calc(var(--ry, 200px) * 0.4515); }
  50%     { translate: calc(var(--rx, 300px) * -1) 0; }
  54.1667%{ translate: calc(var(--rx, 300px) * -0.9798) calc(var(--ry, 200px) * -0.4515); }
  58.3333%{ translate: calc(var(--rx, 300px) * -0.9189) calc(var(--ry, 200px) * -0.6652); }
  62.5%   { translate: calc(var(--rx, 300px) * -0.8156) calc(var(--ry, 200px) * -0.8156); }
  66.6667%{ translate: calc(var(--rx, 300px) * -0.6652) calc(var(--ry, 200px) * -0.9189); }
  70.8333%{ translate: calc(var(--rx, 300px) * -0.4515) calc(var(--ry, 200px) * -0.9798); }
  75%     { translate: 0 calc(var(--ry, 200px) * -1); }
  79.1667%{ translate: calc(var(--rx, 300px) * 0.4515) calc(var(--ry, 200px) * -0.9798); }
  83.3333%{ translate: calc(var(--rx, 300px) * 0.6652) calc(var(--ry, 200px) * -0.9189); }
  87.5%   { translate: calc(var(--rx, 300px) * 0.8156) calc(var(--ry, 200px) * -0.8156); }
  91.6667%{ translate: calc(var(--rx, 300px) * 0.9189) calc(var(--ry, 200px) * -0.6652); }
  95.8333%{ translate: calc(var(--rx, 300px) * 0.9798) calc(var(--ry, 200px) * -0.4515); }
  100%    { translate: var(--rx, 300px) 0; }
}
@keyframes orb-p1 {
  0%      { translate: var(--rx, 300px) 0; }
  4.1667% { translate: calc(var(--rx, 300px) * 0.9836) calc(var(--ry, 200px) * 0.5254); }
  8.3333% { translate: calc(var(--rx, 300px) * 0.9338) calc(var(--ry, 200px) * 0.7189); }
  12.5%   { translate: calc(var(--rx, 300px) * 0.8479) calc(var(--ry, 200px) * 0.8479); }
  16.6667%{ translate: calc(var(--rx, 300px) * 0.7189) calc(var(--ry, 200px) * 0.9338); }
  20.8333%{ translate: calc(var(--rx, 300px) * 0.5254) calc(var(--ry, 200px) * 0.9836); }
  25%     { translate: 0 var(--ry, 200px); }
  29.1667%{ translate: calc(var(--rx, 300px) * -0.5254) calc(var(--ry, 200px) * 0.9836); }
  33.3333%{ translate: calc(var(--rx, 300px) * -0.7189) calc(var(--ry, 200px) * 0.9338); }
  37.5%   { translate: calc(var(--rx, 300px) * -0.8479) calc(var(--ry, 200px) * 0.8479); }
  41.6667%{ translate: calc(var(--rx, 300px) * -0.9338) calc(var(--ry, 200px) * 0.7189); }
  45.8333%{ translate: calc(var(--rx, 300px) * -0.9836) calc(var(--ry, 200px) * 0.5254); }
  50%     { translate: calc(var(--rx, 300px) * -1) 0; }
  54.1667%{ translate: calc(var(--rx, 300px) * -0.9836) calc(var(--ry, 200px) * -0.5254); }
  58.3333%{ translate: calc(var(--rx, 300px) * -0.9338) calc(var(--ry, 200px) * -0.7189); }
  62.5%   { translate: calc(var(--rx, 300px) * -0.8479) calc(var(--ry, 200px) * -0.8479); }
  66.6667%{ translate: calc(var(--rx, 300px) * -0.7189) calc(var(--ry, 200px) * -0.9338); }
  70.8333%{ translate: calc(var(--rx, 300px) * -0.5254) calc(var(--ry, 200px) * -0.9836); }
  75%     { translate: 0 calc(var(--ry, 200px) * -1); }
  79.1667%{ translate: calc(var(--rx, 300px) * 0.5254) calc(var(--ry, 200px) * -0.9836); }
  83.3333%{ translate: calc(var(--rx, 300px) * 0.7189) calc(var(--ry, 200px) * -0.9338); }
  87.5%   { translate: calc(var(--rx, 300px) * 0.8479) calc(var(--ry, 200px) * -0.8479); }
  91.6667%{ translate: calc(var(--rx, 300px) * 0.9338) calc(var(--ry, 200px) * -0.7189); }
  95.8333%{ translate: calc(var(--rx, 300px) * 0.9836) calc(var(--ry, 200px) * -0.5254); }
  100%    { translate: var(--rx, 300px) 0; }
}
@keyframes orb-p2 {
  0%      { translate: var(--rx, 300px) 0; }
  4.1667% { translate: calc(var(--rx, 300px) * 0.9862) calc(var(--ry, 200px) * 0.5824); }
  8.3333% { translate: calc(var(--rx, 300px) * 0.9441) calc(var(--ry, 200px) * 0.7579); }
  12.5%   { translate: calc(var(--rx, 300px) * 0.8706) calc(var(--ry, 200px) * 0.8706); }
  16.6667%{ translate: calc(var(--rx, 300px) * 0.7579) calc(var(--ry, 200px) * 0.9441); }
  20.8333%{ translate: calc(var(--rx, 300px) * 0.5824) calc(var(--ry, 200px) * 0.9862); }
  25%     { translate: 0 var(--ry, 200px); }
  29.1667%{ translate: calc(var(--rx, 300px) * -0.5824) calc(var(--ry, 200px) * 0.9862); }
  33.3333%{ translate: calc(var(--rx, 300px) * -0.7579) calc(var(--ry, 200px) * 0.9441); }
  37.5%   { translate: calc(var(--rx, 300px) * -0.8706) calc(var(--ry, 200px) * 0.8706); }
  41.6667%{ translate: calc(var(--rx, 300px) * -0.9441) calc(var(--ry, 200px) * 0.7579); }
  45.8333%{ translate: calc(var(--rx, 300px) * -0.9862) calc(var(--ry, 200px) * 0.5824); }
  50%     { translate: calc(var(--rx, 300px) * -1) 0; }
  54.1667%{ translate: calc(var(--rx, 300px) * -0.9862) calc(var(--ry, 200px) * -0.5824); }
  58.3333%{ translate: calc(var(--rx, 300px) * -0.9441) calc(var(--ry, 200px) * -0.7579); }
  62.5%   { translate: calc(var(--rx, 300px) * -0.8706) calc(var(--ry, 200px) * -0.8706); }
  66.6667%{ translate: calc(var(--rx, 300px) * -0.7579) calc(var(--ry, 200px) * -0.9441); }
  70.8333%{ translate: calc(var(--rx, 300px) * -0.5824) calc(var(--ry, 200px) * -0.9862); }
  75%     { translate: 0 calc(var(--ry, 200px) * -1); }
  79.1667%{ translate: calc(var(--rx, 300px) * 0.5824) calc(var(--ry, 200px) * -0.9862); }
  83.3333%{ translate: calc(var(--rx, 300px) * 0.7579) calc(var(--ry, 200px) * -0.9441); }
  87.5%   { translate: calc(var(--rx, 300px) * 0.8706) calc(var(--ry, 200px) * -0.8706); }
  91.6667%{ translate: calc(var(--rx, 300px) * 0.9441) calc(var(--ry, 200px) * -0.7579); }
  95.8333%{ translate: calc(var(--rx, 300px) * 0.9862) calc(var(--ry, 200px) * -0.5824); }
  100%    { translate: var(--rx, 300px) 0; }
}
@keyframes orb-p3 {
  0%      { translate: var(--rx, 300px) 0; }
  4.1667% { translate: calc(var(--rx, 300px) * 0.9881) calc(var(--ry, 200px) * 0.6275); }
  8.3333% { translate: calc(var(--rx, 300px) * 0.9516) calc(var(--ry, 200px) * 0.7874); }
  12.5%   { translate: calc(var(--rx, 300px) * 0.8874) calc(var(--ry, 200px) * 0.8874); }
  16.6667%{ translate: calc(var(--rx, 300px) * 0.7874) calc(var(--ry, 200px) * 0.9516); }
  20.8333%{ translate: calc(var(--rx, 300px) * 0.6275) calc(var(--ry, 200px) * 0.9881); }
  25%     { translate: 0 var(--ry, 200px); }
  29.1667%{ translate: calc(var(--rx, 300px) * -0.6275) calc(var(--ry, 200px) * 0.9881); }
  33.3333%{ translate: calc(var(--rx, 300px) * -0.7874) calc(var(--ry, 200px) * 0.9516); }
  37.5%   { translate: calc(var(--rx, 300px) * -0.8874) calc(var(--ry, 200px) * 0.8874); }
  41.6667%{ translate: calc(var(--rx, 300px) * -0.9516) calc(var(--ry, 200px) * 0.7874); }
  45.8333%{ translate: calc(var(--rx, 300px) * -0.9881) calc(var(--ry, 200px) * 0.6275); }
  50%     { translate: calc(var(--rx, 300px) * -1) 0; }
  54.1667%{ translate: calc(var(--rx, 300px) * -0.9881) calc(var(--ry, 200px) * -0.6275); }
  58.3333%{ translate: calc(var(--rx, 300px) * -0.9516) calc(var(--ry, 200px) * -0.7874); }
  62.5%   { translate: calc(var(--rx, 300px) * -0.8874) calc(var(--ry, 200px) * -0.8874); }
  66.6667%{ translate: calc(var(--rx, 300px) * -0.7874) calc(var(--ry, 200px) * -0.9516); }
  70.8333%{ translate: calc(var(--rx, 300px) * -0.6275) calc(var(--ry, 200px) * -0.9881); }
  75%     { translate: 0 calc(var(--ry, 200px) * -1); }
  79.1667%{ translate: calc(var(--rx, 300px) * 0.6275) calc(var(--ry, 200px) * -0.9881); }
  83.3333%{ translate: calc(var(--rx, 300px) * 0.7874) calc(var(--ry, 200px) * -0.9516); }
  87.5%   { translate: calc(var(--rx, 300px) * 0.8874) calc(var(--ry, 200px) * -0.8874); }
  91.6667%{ translate: calc(var(--rx, 300px) * 0.9516) calc(var(--ry, 200px) * -0.7874); }
  95.8333%{ translate: calc(var(--rx, 300px) * 0.9881) calc(var(--ry, 200px) * -0.6275); }
  100%    { translate: var(--rx, 300px) 0; }
}
@keyframes orb-p4 {
  0%      { translate: var(--rx, 300px) 0; }
  4.1667% { translate: calc(var(--rx, 300px) * 0.9899) calc(var(--ry, 200px) * 0.672); }
  8.3333% { translate: calc(var(--rx, 300px) * 0.9586) calc(var(--ry, 200px) * 0.8156); }
  12.5%   { translate: calc(var(--rx, 300px) * 0.9031) calc(var(--ry, 200px) * 0.9031); }
  16.6667%{ translate: calc(var(--rx, 300px) * 0.8156) calc(var(--ry, 200px) * 0.9586); }
  20.8333%{ translate: calc(var(--rx, 300px) * 0.672) calc(var(--ry, 200px) * 0.9899); }
  25%     { translate: 0 var(--ry, 200px); }
  29.1667%{ translate: calc(var(--rx, 300px) * -0.672) calc(var(--ry, 200px) * 0.9899); }
  33.3333%{ translate: calc(var(--rx, 300px) * -0.8156) calc(var(--ry, 200px) * 0.9586); }
  37.5%   { translate: calc(var(--rx, 300px) * -0.9031) calc(var(--ry, 200px) * 0.9031); }
  41.6667%{ translate: calc(var(--rx, 300px) * -0.9586) calc(var(--ry, 200px) * 0.8156); }
  45.8333%{ translate: calc(var(--rx, 300px) * -0.9899) calc(var(--ry, 200px) * 0.672); }
  50%     { translate: calc(var(--rx, 300px) * -1) 0; }
  54.1667%{ translate: calc(var(--rx, 300px) * -0.9899) calc(var(--ry, 200px) * -0.672); }
  58.3333%{ translate: calc(var(--rx, 300px) * -0.9586) calc(var(--ry, 200px) * -0.8156); }
  62.5%   { translate: calc(var(--rx, 300px) * -0.9031) calc(var(--ry, 200px) * -0.9031); }
  66.6667%{ translate: calc(var(--rx, 300px) * -0.8156) calc(var(--ry, 200px) * -0.9586); }
  70.8333%{ translate: calc(var(--rx, 300px) * -0.672) calc(var(--ry, 200px) * -0.9899); }
  75%     { translate: 0 calc(var(--ry, 200px) * -1); }
  79.1667%{ translate: calc(var(--rx, 300px) * 0.672) calc(var(--ry, 200px) * -0.9899); }
  83.3333%{ translate: calc(var(--rx, 300px) * 0.8156) calc(var(--ry, 200px) * -0.9586); }
  87.5%   { translate: calc(var(--rx, 300px) * 0.9031) calc(var(--ry, 200px) * -0.9031); }
  91.6667%{ translate: calc(var(--rx, 300px) * 0.9586) calc(var(--ry, 200px) * -0.8156); }
  95.8333%{ translate: calc(var(--rx, 300px) * 0.9899) calc(var(--ry, 200px) * -0.672); }
  100%    { translate: var(--rx, 300px) 0; }
}
@keyframes orb-p5 {
  0%      { translate: var(--rx, 300px) 0; }
  4.1667% { translate: calc(var(--rx, 300px) * 0.9914) calc(var(--ry, 200px) * 0.7133); }
  8.3333% { translate: calc(var(--rx, 300px) * 0.9647) calc(var(--ry, 200px) * 0.8409); }
  12.5%   { translate: calc(var(--rx, 300px) * 0.917) calc(var(--ry, 200px) * 0.917); }
  16.6667%{ translate: calc(var(--rx, 300px) * 0.8409) calc(var(--ry, 200px) * 0.9647); }
  20.8333%{ translate: calc(var(--rx, 300px) * 0.7133) calc(var(--ry, 200px) * 0.9914); }
  25%     { translate: 0 var(--ry, 200px); }
  29.1667%{ translate: calc(var(--rx, 300px) * -0.7133) calc(var(--ry, 200px) * 0.9914); }
  33.3333%{ translate: calc(var(--rx, 300px) * -0.8409) calc(var(--ry, 200px) * 0.9647); }
  37.5%   { translate: calc(var(--rx, 300px) * -0.917) calc(var(--ry, 200px) * 0.917); }
  41.6667%{ translate: calc(var(--rx, 300px) * -0.9647) calc(var(--ry, 200px) * 0.8409); }
  45.8333%{ translate: calc(var(--rx, 300px) * -0.9914) calc(var(--ry, 200px) * 0.7133); }
  50%     { translate: calc(var(--rx, 300px) * -1) 0; }
  54.1667%{ translate: calc(var(--rx, 300px) * -0.9914) calc(var(--ry, 200px) * -0.7133); }
  58.3333%{ translate: calc(var(--rx, 300px) * -0.9647) calc(var(--ry, 200px) * -0.8409); }
  62.5%   { translate: calc(var(--rx, 300px) * -0.917) calc(var(--ry, 200px) * -0.917); }
  66.6667%{ translate: calc(var(--rx, 300px) * -0.8409) calc(var(--ry, 200px) * -0.9647); }
  70.8333%{ translate: calc(var(--rx, 300px) * -0.7133) calc(var(--ry, 200px) * -0.9914); }
  75%     { translate: 0 calc(var(--ry, 200px) * -1); }
  79.1667%{ translate: calc(var(--rx, 300px) * 0.7133) calc(var(--ry, 200px) * -0.9914); }
  83.3333%{ translate: calc(var(--rx, 300px) * 0.8409) calc(var(--ry, 200px) * -0.9647); }
  87.5%   { translate: calc(var(--rx, 300px) * 0.917) calc(var(--ry, 200px) * -0.917); }
  91.6667%{ translate: calc(var(--rx, 300px) * 0.9647) calc(var(--ry, 200px) * -0.8409); }
  95.8333%{ translate: calc(var(--rx, 300px) * 0.9914) calc(var(--ry, 200px) * -0.7133); }
  100%    { translate: var(--rx, 300px) 0; }
}
@keyframes orb-p6 {
  0%      { translate: var(--rx, 300px) 0; }
  4.1667% { translate: calc(var(--rx, 300px) * 0.9931) calc(var(--ry, 200px) * 0.7631); }
  8.3333% { translate: calc(var(--rx, 300px) * 0.9716) calc(var(--ry, 200px) * 0.8706); }
  12.5%   { translate: calc(var(--rx, 300px) * 0.933) calc(var(--ry, 200px) * 0.933); }
  16.6667%{ translate: calc(var(--rx, 300px) * 0.8706) calc(var(--ry, 200px) * 0.9716); }
  20.8333%{ translate: calc(var(--rx, 300px) * 0.7631) calc(var(--ry, 200px) * 0.9931); }
  25%     { translate: 0 var(--ry, 200px); }
  29.1667%{ translate: calc(var(--rx, 300px) * -0.7631) calc(var(--ry, 200px) * 0.9931); }
  33.3333%{ translate: calc(var(--rx, 300px) * -0.8706) calc(var(--ry, 200px) * 0.9716); }
  37.5%   { translate: calc(var(--rx, 300px) * -0.933) calc(var(--ry, 200px) * 0.933); }
  41.6667%{ translate: calc(var(--rx, 300px) * -0.9716) calc(var(--ry, 200px) * 0.8706); }
  45.8333%{ translate: calc(var(--rx, 300px) * -0.9931) calc(var(--ry, 200px) * 0.7631); }
  50%     { translate: calc(var(--rx, 300px) * -1) 0; }
  54.1667%{ translate: calc(var(--rx, 300px) * -0.9931) calc(var(--ry, 200px) * -0.7631); }
  58.3333%{ translate: calc(var(--rx, 300px) * -0.9716) calc(var(--ry, 200px) * -0.8706); }
  62.5%   { translate: calc(var(--rx, 300px) * -0.933) calc(var(--ry, 200px) * -0.933); }
  66.6667%{ translate: calc(var(--rx, 300px) * -0.8706) calc(var(--ry, 200px) * -0.9716); }
  70.8333%{ translate: calc(var(--rx, 300px) * -0.7631) calc(var(--ry, 200px) * -0.9931); }
  75%     { translate: 0 calc(var(--ry, 200px) * -1); }
  79.1667%{ translate: calc(var(--rx, 300px) * 0.7631) calc(var(--ry, 200px) * -0.9931); }
  83.3333%{ translate: calc(var(--rx, 300px) * 0.8706) calc(var(--ry, 200px) * -0.9716); }
  87.5%   { translate: calc(var(--rx, 300px) * 0.933) calc(var(--ry, 200px) * -0.933); }
  91.6667%{ translate: calc(var(--rx, 300px) * 0.9716) calc(var(--ry, 200px) * -0.8706); }
  95.8333%{ translate: calc(var(--rx, 300px) * 0.9931) calc(var(--ry, 200px) * -0.7631); }
  100%    { translate: var(--rx, 300px) 0; }
}
}

/* ⚠️ Under a phone, or under reduced motion, the field becomes a plain row. A
   scatter of absolutely-positioned thumbnails has no sensible small-screen
   layout, and a drifting field is precisely what prefers-reduced-motion asks us
   not to build. */
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .film__strip {
    position: static; inset: auto;
    width: min(94vw, 1100px); margin: clamp(12px, 2vh, 20px) auto 0;
  }
  .film__orbit {
    display: flex; flex-wrap: wrap; justify-content: center; gap: 7px;
  }
  .film__orbit button {
    /* relative, not static: the scanline/vignette overlay is an ::after with
       inset:0, so the button has to stay its containing block or the treatment
       jumps to the page. The row loses the drift and the feather, not the
       reason the thumbnails read as displayed rather than pasted. */
    position: relative; animation: none; filter: none;
    /* all four transform lanes have to be reset, not just the one that used to
       exist — a leftover `scale` or `translate` survives `transform: none` */
    transform: none; translate: none; rotate: none; scale: none;
    opacity: .45; left: auto; top: auto;
  }
  .film__orbit .thumb { scale: none; -webkit-mask-image: none; mask-image: none; }
  .film__orbit button.is-on { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .slide img { animation: none; }
  .slide { transition: none; }
  .film__strip button { transition: none; }
}

/* the one field on this page that leaves it */
.film__watch { color: var(--amber); text-decoration: none; border-bottom: 1px solid rgba(240,168,60,.35); }
.film__watch:hover { border-bottom-color: var(--amber); }
