/* =============================================================================
   TUNNEL - workstation 02, sub-sections A (photo) and B (video).
   Replaces the contact strips (css/photo.css) and the film reel (css/reel.css).
   =============================================================================

   Two layers live in this file, and only one of them is ever shown:

   1. .tun-scene   the pinned 3D corridor. Hidden by default. js/tunnel.js
                   puts .is-live on .tun when it can really drive it, and
                   only then does the scene take the screen. So a missing
                   GSAP, prefers-reduced-motion, a short viewport, a weak
                   device or no JavaScript at all never show an empty
                   black panel: they fall through to layer 2.
   2. .tun-flat    the sober fallback: one section per series with a grid of
                   480 px photographs, then the three films with their native
                   controls. Shown whenever layer 1 is not live, hidden with
                   display:none when it is (so its lazy images are never even
                   requested on a machine that gets the corridor).

   Nothing here uses filter: blur or a large shadow: the corridor moves about
   sixty layers per frame and both are paid on every one of them.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   1. Tokens. Lightened brass and porcelain, same recipe and same values as
   css/terminal.css: the site brass (#8a683a) is unreadable on the ink.
   -------------------------------------------------------------------------- */

.tun {
  --tun-ink: #0b1216;
  --tun-fg: #f4f7f9;
  --tun-dim: #93a2ad;
  --tun-brass: #c79b56;
  --tun-hair: rgba(244, 247, 249, 0.14);
  --tun-gut: clamp(1.1rem, 4vw, 3rem);
  position: relative;
}

/* -----------------------------------------------------------------------------
   2. The pinned corridor
   -------------------------------------------------------------------------- */

.tun-scene { display: none; }

/* Full bleed, edge to edge, out of `.poste > *`: body has overflow-x: clip,
   so 100vw costs no horizontal scrollbar. */
.tun.is-live .tun-scene {
  display: block;
  position: relative;
  margin-inline: calc(50% - 50vw);
  width: 100vw;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: var(--paper);
  /* The panel paints nothing outside itself and nothing inside it can
     change the size of the page: the corridor is a closed box. */
  contain: layout paint style;
}

/* The ink that swallows the paper, seen through the iris of FIG. 02.A.
   clip-path only, so it costs one composited clip and no repaint.

   The aperture is ONE custom property, written on .tun-scene once per
   frame by js/tunnel.js and read by the three layers that must only
   exist inside the opening: the ink, the grid and the 3D stage. One
   style write, three composited clips, no repaint. Without the drawing
   (old markup, or the legacy branch of the module) the property is
   never set and js/tunnel.js drives the circular iris of before on
   .tun-ink directly, which wins over `var(--tun-clip)` as an inline
   style. */
.tun-ink {
  position: absolute;
  inset: 0;
  background: var(--tun-ink);
}
.tun-ink,
.tun-grid,
.tun-stage { clip-path: var(--tun-clip, none); }

/* The technical grid of the workshop, faded out at the edges. */
.tun-grid {
  position: absolute;
  inset: 0;
  opacity: 0;
  background-image:
    linear-gradient(rgba(244, 247, 249, 0.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(244, 247, 249, 0.045) 1px, transparent 1px);
  background-size: 96px 96px;
  -webkit-mask-image: radial-gradient(ellipse at center, #000 20%, transparent 70%);
  mask-image: radial-gradient(ellipse at center, #000 20%, transparent 70%);
}

.tun-stage { position: absolute; inset: 0; }

/* LA BRUME DE L'OUVERTURE (js/tunnel.js la cree, dans l'encre, et n'ecrit
   que son opacite).

   Felix, 22/09 : quand les lamelles s'ecartent on ne doit QUE deviner le
   couloir, une lueur et des taches, et les photographies ne se decouvrent
   qu'avec la plongee. Un `filter: blur` sur le monde 3D coutait une passe de
   flou plein ecran par image et aurait aplati le preserve-3d ; c'est donc une
   lueur : un degrade fige, rasterise une fois, dont seule l'opacite bouge, ce
   que le compositeur fait sans repeindre.

   Elle est DANS .tun-ink, donc deja decoupee sur l'octogone et deja relevee
   avec lui : aucun clip-path, aucun transform de plus a calculer par image.
   Elle eclaire les tirages par derriere, et comme ils montent lentement en
   opacite (js/tunnel.js, lightWorld) la lueur les traverse et les delave
   d'abord, avant qu'ils ne se posent dessus. */
.tun-ink > .tun-haze {
  display: none;
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background:
    radial-gradient(54% 50% at 50% 45%,
      rgba(230, 238, 243, 0.78),
      rgba(202, 215, 224, 0.42) 38%,
      rgba(120, 140, 152, 0.16) 70%,
      rgba(11, 18, 22, 0) 100%);
}
html.tun-zoom .tun.is-live .tun-ink > .tun-haze {
  display: block;
  will-change: opacity;
}

/* -----------------------------------------------------------------------------
   2.A THE WAY IN: FIG. 02, the station sheet itself

   There is only one camera drawing on the site, the hybrid body seen from
   the front in the header of #camera (index.html, `.poste-svg-cam`). It is
   traced by js/stations.js as the station scene, blades shut; then, over
   the one screen of ordinary scroll between the station pin and this pin,
   js/tunnel.js turns its blades open, flies the whole panel into the lens,
   and cuts the corridor on the octagon the blades leave.

   Three things are written from JS during that push, and nothing else:
     --cam-phi   on `.poste-figure`: the rotation of every blade about its
                 own pivot on the barrel. It reaches the ghost clone too,
                 which is spent by then and hidden.
     --tun-clip  on `.tun-scene`: the aperture polygon, in the coordinates
                 of the three layers that carry the clip. While the push
                 runs those layers are `fixed`, so that frame IS the
                 viewport, which is the only frame the aperture can be
                 computed in without guessing.
     a matrix    on the sheet itself (`.tun-zoom-stage`), which carries
                 the lens onto the middle of the screen and blows it up.
                 Vague 5 : c'est desormais le viewBox d'un clone de la
                 planche (.tun-lens) et la matrice ne porte plus que son
                 lettrage (.tun-lens-txt), voir plus bas.
                 Only the sheet: the paper of the panel is a flat colour
                 and the giant number a watermark, so flying them costs a
                 full screen of raster per frame and shows nothing.
   -------------------------------------------------------------------------- */

/* Each blade is drawn once in the frame of blade 0 and placed by a static
   rotate() on its parent group, so all eight share one pivot in local units
   and one rotation value. Paper fill, so a blade hides the two it overlaps
   instead of showing its own back edge through them; nothing dark ever ends
   up under a blade, because the ink is cut on the hole they leave. */
#camera .poste-figure .cam-blade,
#camera .poste-figure .cam-blade-m {
  transform-box: view-box;
  transform-origin: 130px 100px;
  transform: rotate(var(--cam-phi, 0deg));
}
#camera .poste-figure .cam-blade { fill: var(--paper); }

/* The seam of the cycle (index.html, #cam-blades-b): the shape of blade 7
   inside the mask that lets the echo of blades 0 to 2 show over it only.
   Its stroke is widened past the one of the blade, in screen pixels like
   every traced line, so the edge of blade 7 is covered to its last
   antialiased pixel wherever blade 0 lies over it. */
#camera .poste-figure .cam-blade-m { vector-effect: non-scaling-stroke; }

/* NO CORRIDOR (no JavaScript, no GSAP, reduced motion, a viewport too
   short, four cores or fewer): `html.tun-fx` is never set, nothing opens
   the iris, and a camera drawn with its lens shut for ever would read as
   a mistake. So the blades rest open and the sober grid of section 4
   follows, which is the whole fallback. */
html:not(.tun-fx) #camera .poste-figure .cam-blade,
html:not(.tun-fx) #camera .poste-figure .cam-blade-m { --cam-phi: 62deg; }

/* The paper of a blade, when the station scene traces it: js/stations.js
   fades `fill-opacity` in with the stroke of that very blade, so the paper
   lands as its outline closes instead of all eight turning white at once
   when the beat ends (the pop of 23/09). At the first frame of the scene
   every blade is still at 0, so no paper disc sits across the giant 02 the
   panel arrives on. Nothing here: the fallback above, where nothing is
   traced, keeps its filled blades. */

/* `html.tun-zoom` is set for 0 < progress < 1 of the push only, so at both
   ends of it the page is exactly what the stylesheets say it is. */

/* The section must not grow a horizontal scrollbar while a panel ten times
   its own size is being flown through it. Declared before the push rather
   than during it (`html.tun-fx` goes on at boot), so no style change can
   land mid scrub. `overflow-x: clip` is the one clip value that leaves
   overflow-y visible, which the pinned scenes of this section need. */
html.tun-fx #camera.poste { overflow-x: clip; }

/* css/stations.css keeps the panel's own `overflow: hidden` so the giant
   number cannot spill; while the sheet is being flown through it, the
   sheet is exactly what must spill, and without this it was cut off dead
   straight along the bottom edge of the panel. */
html.tun-zoom #camera .poste-header.is-scene { overflow: visible; }

html.tun-zoom .tun-zoom-stage {
  transform-origin: 0 0;
  will-change: transform, opacity;
}

/* The corridor is what is seen THROUGH the lens, so during the push its
   three layers must be the viewport, not the block that is still rising up
   the page. `position: fixed` is NOT how: ScrollTrigger leaves an identity
   `transform` on the element it pins, and that alone makes the panel the
   containing block of any fixed child, so the layers hung off the block
   instead of the screen and the aperture came out a full panel height too
   low. They are lifted by a transform instead, `--tun-lift`, written once
   per frame with the aperture itself. Their own box is then the viewport,
   which is the frame getScreenCTM() answers in. `contain: paint` and the
   panel's `overflow` would both clip them back in, so both are lifted for
   the duration. */
html.tun-zoom .tun.is-live .tun-scene {
  /* `contain` must go entirely, not down to `layout style`: `contain:
     layout` alone still makes the panel the containing block of a fixed
     child, and the three layers then hang off the block as it travels up
     the page instead of off the viewport. That is the whole aperture in
     the wrong frame. */
  contain: none;
  overflow: visible;
  /* css/stations.css gives ScrollTrigger's pin spacer z-index 1, so
     without this the panel we are flying into paints OVER the hole cut
     in the corridor and the aperture stays paper coloured. The drawing
     keeps reading around the hole because the hole IS the hole: the ink
     stops exactly on the edge the blades leave. */
  z-index: 2;
  /* The paper of the corridor panel would cover the station sheet we are
     flying into: while the push runs the panel is only its ink octagon. */
  background: transparent;
}
/* THE THREE LAYERS, not the ink alone. The ink used to be the only one
   lifted and clipped, because the grid was still at opacity 0 and not one
   photograph had been painted while the push ran: the blades opened on
   black and the corridor only appeared once the pin took over. Felix,
   22/09, on the third capture: as soon as the octagon opens you have to
   see the corridor inside it. js/tunnel.js now lights the world with the
   first degrees of the blades, so the grid and the stage have something
   to show and must be cut on the same octagon as the ink, or the
   photographs spill over the drawing around the lens. */
html.tun-zoom .tun.is-live .tun-ink,
html.tun-zoom .tun.is-live .tun-stage {
  transform: translate3d(0, var(--tun-lift, 0px), 0);
  will-change: transform;
}
/* The grid is the exception and stays out of it: a full viewport of repeating
   gradient under a radial mask, cut on a new polygon on every frame, is the
   most expensive layer of the whole move, and it has nothing to show while the
   blades open. It lights up when the pin takes over (js/tunnel.js). */
html.tun-zoom .tun.is-live .tun-grid { clip-path: none; }

/* The sheet used to drop `non-scaling-stroke` at 1.25 times its size, so its
   lines thickened with the push: cheaper to rasterise, and, the reasoning
   went, what coming closer looks like. Felix, 22/09, looking at the two
   frames side by side: the drawing goes from a clean 1.2 px line to a thick
   one in the space of one class toggle, and the whole sheet reads as a blob.
   So the line stays FINE and constant through the whole dive, which is what
   `non-scaling-stroke` gives for free and what the rest of the site does. The
   push is capped at 2.6 (js/tunnel.js, ZOOM.scap), not the ten of the
   mock-up, and the shedding below is what pays for it instead. */

/* And the sheet sheds what it no longer shows. Past a certain size the
   cotes, the cartouche, the knurl of the focus ring and every hairline of the
   body are too big to read, and the browser rasterises all ninety of them
   again at every scale of the dive: it is the single most expensive thing in
   the whole move.

   Mais ce n'etait pas un fondu : un seuil sur l'echelle posait `display:
   none` sur tout le lot d'un coup, et un tiers du dessin s'eteignait en un
   cran de molette alors que la planche entiere tenait encore a l'ecran
   (audit visuel du 22/09, constat 1). Rien ne disparait plus d'un coup :
   `--cam-shed` descend de 1 a 0 sur pres d'un tiers de la marche de zoom
   (js/tunnel.js, shedIn / shedEnd), et c'est seulement une fois a zero,
   donc quand plus rien n'est visible, que le lot sort du paint.

   stroke-opacity et fill-opacity, pas opacity : ces traits portent deja leur
   propre `opacity` en attribut de presentation (0,5, 0,6, 0,7), qu'une regle
   CSS `opacity` ecraserait, alors que ces deux la se multiplient avec elle.

   L'ancienne regle `cam-deep` (le corps et la bague AF a l'echelle 2,2)
   est partie avec : le zoom plafonne a 1,95 (js/tunnel.js, scap), le seuil
   n'etait jamais atteint et la regle etait morte, verifiee sur les 856
   echantillons de l'audit. */
#camera .poste-svg-cam .poste-svg-dims,
#camera .poste-svg-cam > g[fill="currentColor"],
#camera .poste-svg-cam [stroke-width="0.4"],
#camera .poste-svg-cam [stroke-width="0.45"],
#camera .poste-svg-cam [stroke-width="0.5"] {
  stroke-opacity: var(--cam-shed, 1);
  fill-opacity: var(--cam-shed, 1);
}
#camera .poste-svg-cam.cam-shed-out .poste-svg-dims,
#camera .poste-svg-cam.cam-shed-out > g[fill="currentColor"],
#camera .poste-svg-cam.cam-shed-out [stroke-width="0.4"],
#camera .poste-svg-cam.cam-shed-out [stroke-width="0.45"],
#camera .poste-svg-cam.cam-shed-out [stroke-width="0.5"] { display: none; }

/* LE CALQUE OBJECTIF (js/tunnel.js, section 4.B). Pendant la plongee la
   planche est remplacee, a l'echelle 1 et au pixel pres, par un clone de la
   taille de l'ecran dont on anime le viewBox : la lentille grandit jusqu'a
   depasser l'ecran pour le prix d'un ecran de vecteurs par image, quelle que
   soit l'echelle. Il porte les classes de la planche (et vit dans la meme
   figure), donc les regles des lamelles et du delestage ci-dessus
   l'habillent aussi. Sa boite et son viewBox sont ecrits par le script. */
#camera .poste-figure > .tun-lens {
  display: none;
  pointer-events: none;
  overflow: hidden;
  max-width: none;
  max-height: none;
}
html.tun-zoom #camera .poste-figure > .tun-lens.is-on {
  display: block;
  will-change: transform;
}
#camera .tun-lens .draw { vector-effect: non-scaling-stroke; }
/* Son lettrage a part : le texte SVG se remet en page a chaque changement
   d'echelle d'ecran (viewBox ou transform sur le <svg>), pas sous un
   transform pose sur une boite HTML qui l'entoure. Les six etiquettes sont
   donc dans cette boite, portee par la matrice de l'ancienne planche. */
#camera .poste-figure > .tun-lens-txt {
  display: none;
  position: absolute;
  pointer-events: none;
  transform-origin: 0 0;
}
html.tun-zoom #camera .poste-figure > .tun-lens-txt.is-on {
  display: block;
  will-change: transform, opacity;
}

/* Promoted only while the push runs: the fade of the cartouche is then
   composited instead of repainting a block of text on every frame. */
html.tun-zoom #camera .poste-intro { will-change: opacity, filter; }
/* Le gros numero part maintenant pendant l'ouverture (js/tunnel.js, markIn) :
   promu lui aussi, son fondu ne repeint pas des glyphes de 40 rem. */
html.tun-zoom #camera .poste-header .poste-mark { will-change: opacity; }

/* The world is never given an opacity of its own: below 1 it would flatten
   the preserve-3d group and the back photographs would be painted over the
   front ones. The fade in is a multiplier applied to every frame instead. */
.tun-world {
  position: absolute;
  inset: 0;
  perspective: 1100px;
  perspective-origin: 50% 50%;
  transform-style: preserve-3d;
}

.tun-ph {
  position: absolute;
  left: 50%;
  top: 50%;
  transform-style: preserve-3d;
  will-change: transform, opacity;
  contain: strict;
  backface-visibility: hidden;
  border-radius: 6px;
  overflow: hidden;
  background: #11181d;
  visibility: hidden;
}
.tun-ph.is-in { visibility: visible; }

.tun-ph img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* A one pixel ring instead of a drop shadow: the frames read as prints on a
   table without a blurred rectangle being rasterised sixty times a frame. */
.tun-ph::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 6px;
  box-shadow: inset 0 0 0 1px rgba(244, 247, 249, 0.08);
}
.tun-ph-hero::after { box-shadow: inset 0 0 0 1px rgba(199, 155, 86, 0.35); }

/* ---- the caption of a series ---- */

/* isolate, so the scrim of a caption (z-index: -1 below) stays inside this
   layer and never slides behind the photographs. */
.tun-caps { position: absolute; inset: 0; pointer-events: none; isolation: isolate; }

.tun-cap {
  position: absolute;
  left: var(--tun-gut);
  right: var(--tun-gut);
  bottom: 9vh;
  margin: 0;
  color: var(--tun-fg);
  opacity: 0;
  visibility: hidden;
  transform: translate3d(0, 24px, 0);
}

/* A title sits over whatever print happens to be flying past. One flat
   gradient, painted once, carries it: no backdrop filter, no blur. It rides
   the opacity of the caption, so it is only there while the title is. */
.tun-cap::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: calc(-1 * var(--tun-gut) - 2rem);
  top: -2.5rem;
  bottom: calc(-9vh - 2rem);
  width: min(78vw, 900px);
  background: linear-gradient(100deg,
    rgba(11, 18, 22, 0.94) 0%,
    rgba(11, 18, 22, 0.72) 38%,
    rgba(11, 18, 22, 0) 82%);
}

.tun-cap-ref {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--tun-brass);
  margin-bottom: 0.6rem;
}
.tun-cap h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.2rem, 6vw, 5.4rem);
  line-height: 0.95;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--tun-fg);
  /* The words rise behind their own line, so the mask has to clip. */
  overflow: hidden;
}
.tun-cap h3 span { display: inline-block; will-change: transform; }
.tun-cap p {
  margin: 0.8rem 0 0;
  font-size: clamp(0.95rem, 1.3vw, 1.15rem);
  color: var(--tun-dim);
  max-width: 38ch;
}

/* ---- the counter, bottom right ---- */

.tun-hud {
  position: absolute;
  right: var(--tun-gut);
  bottom: 9vh;
  margin: 0;
  opacity: 0;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--tun-dim);
  text-align: right;
  pointer-events: none;
  isolation: isolate;
}
/* Same flat scrim as the captions: the counter also has prints flying
   behind it. */
.tun-hud::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: -3rem;
  right: calc(-1 * var(--tun-gut) - 1.5rem);
  top: -1.75rem;
  bottom: calc(-9vh - 1rem);
  background: linear-gradient(260deg,
    rgba(11, 18, 22, 0.9) 0%,
    rgba(11, 18, 22, 0.55) 45%,
    rgba(11, 18, 22, 0) 88%);
}
.tun-hud b { color: var(--tun-fg); font-weight: 500; }
.tun-hud i {
  display: block;
  width: 160px;
  height: 1px;
  margin: 0.6rem 0 0 auto;
  background: var(--tun-hair);
  position: relative;
}
.tun-hud i::after {
  content: "";
  position: absolute;
  left: 0;
  top: -1px;
  height: 3px;
  width: var(--tun-p, 0%);
  background: var(--tun-brass);
}

/* Under about 520 px the caption and the counter would sit on top of each
   other: the counter moves up out of the way. */
@media (max-width: 520px) {
  .tun-hud { bottom: auto; top: 4vh; }
  .tun-hud i { width: 96px; }
  /* The counter has moved to the top, so its scrim has to move with it. */
  .tun-hud::before { top: -2rem; bottom: -0.75rem; }
}

/* ---- the three films ---- */

.tun-films {
  position: absolute;
  inset: 0;
  perspective: 1100px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.tun-films-cap {
  position: absolute;
  left: var(--tun-gut);
  top: 9vh;
  right: var(--tun-gut);
  color: var(--tun-fg);
}
.tun-films-cap span {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--tun-brass);
}
.tun-films-cap h3 {
  margin: 0.4rem 0 0;
  font-family: var(--font-display);
  font-size: clamp(1.8rem, 4.5vw, 3.8rem);
  letter-spacing: -0.02em;
  line-height: 1;
  font-weight: 700;
}

.tun-film {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(24vw, 300px);
  aspect-ratio: 9 / 16;
  padding: 0;
  border: 0;
  border-radius: 26px;
  overflow: hidden;
  background: #000;
  cursor: pointer;
  transform: translate(-50%, -50%);
  transform-style: preserve-3d;
  box-shadow: inset 0 0 0 1px rgba(244, 247, 249, 0.12);
  color: var(--tun-fg);
}
.tun-film video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}
.tun-film:focus-visible { outline: 2px solid var(--tun-brass); outline-offset: 3px; }

/* No play mark and no pointer: the phones are looping previews, not a
   player. Felix does not want the films opened full size from here. */
.tun-film { cursor: default; }

/* Three phones side by side have to fit a 390 px frame: 30vw each against
   the 0.315 slot of js/tunnel.js leaves a small gap between them. */
@media (max-width: 760px) {
  .tun-film { width: min(30vw, 130px); border-radius: 14px; }
}

/* -----------------------------------------------------------------------------
   3. The sober fallback
   -------------------------------------------------------------------------- */

.tun.is-live .tun-flat { display: none; }

.tun-flat { margin: 0; }
.tun-set { margin: clamp(2rem, 6vh, 3.5rem) 0 0; }
.tun-set-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem 1rem; }
.tun-set-head h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.3rem, 3vw, 1.9rem);
  font-weight: 700;
  letter-spacing: -0.01em;
}
.tun-set-ref {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--brass);
}
.tun-set-lede { margin: 0.35rem 0 0; color: var(--ink-soft); max-width: 60ch; }
.tun-set ul {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 0.6rem;
}
.tun-set li { margin: 0; }
.tun-set img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  border-radius: 6px;
  background: var(--paper-2);
}
.tun-set li:first-child img { aspect-ratio: auto; }

.tun-flat-films { margin: clamp(2rem, 6vh, 3.5rem) 0 0; }
.tun-flat-films ul {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
}
.tun-flat-films video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 9 / 16;
  border-radius: 10px;
  background: #000;
}
.tun-flat-films figcaption {
  margin-top: 0.4rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
}


/* Centre mark of the shut iris (23/09). Visible once the station is drawn
   and only while no blade has turned; never where the blades rest open. */
#camera .cam-hub {
  fill: var(--paper);
  stroke: currentColor;
  vector-effect: non-scaling-stroke;
  opacity: 0;
  transition: opacity 0.35s ease-out;
}
#camera .poste-svg-cam.is-live .cam-hub,
html:not(.stn) #camera .cam-hub { opacity: 1; }
#camera .poste-figure.cam-turning .cam-hub,
html:not(.tun-fx) #camera .cam-hub { opacity: 0; }

#camera .cam-tipfix {
  fill: none;
  stroke: currentColor;
  vector-effect: non-scaling-stroke;
  opacity: 0;
  transition: opacity 0.35s ease-out;
}
#camera .poste-svg-cam.is-live .cam-tipfix,
html:not(.stn) #camera .cam-tipfix { opacity: 1; }
#camera .poste-figure.cam-turning .cam-tipfix,
html:not(.tun-fx) #camera .cam-tipfix { opacity: 0; }
