/* ============================================================
   RAW.RETRO — tape-era black & white.

   Same structural idea as the rest of the build: every composed
   section sits on a 16:9 "stage" of definite size, and every
   position and type size on it is authored in percent-of-stage
   or `cqw`. That is what keeps a composition rigid — one
   coordinate system, scaled as a single piece, never a mix of
   viewport units and container percentages that disagree the
   moment the aspect ratio changes.
   ============================================================ */

/* ---- brand kit ---------------------------------------------------------
   Straight out of BRANDING/FONTS, and each face keeps the job its folder
   implies rather than being used wherever it happens to look good:

     MAIN    VCR OSD Mono      the camcorder overlay itself — OSD, controls,
                               labels, captions. It is a signal-burnt face,
                               so it belongs on things the *device* wrote.
     B0DY    Share Tech Mono   the one face here that survives being set as
                               running text, so it carries all body copy.
     PIXELZ  Silkscreen        small pixel labels and the colophon.
     NUMBERZ DSEG7 Classic     a real seven-segment face, used ONLY for
                               numbers a device would actually have lit —
                               the boot counter, the timecode, the channel.

   VCR does the headline work as well as the OSD — it is the brand's main
   face and the whole page is meant to read as one burnt-in signal. */
@font-face{font-family:'VCR';src:url('../assets/fonts/vcr.ttf') format('truetype');font-weight:400;font-display:swap}
@font-face{font-family:'Share';src:url('../assets/fonts/sharetech.ttf') format('truetype');font-weight:400;font-display:swap}
@font-face{font-family:'Silk';src:url('../assets/fonts/silkscreen.ttf') format('truetype');font-weight:400;font-display:swap}
@font-face{font-family:'Silk';src:url('../assets/fonts/silkscreen-bold.ttf') format('truetype');font-weight:700;font-display:swap}
@font-face{font-family:'Press';src:url('../assets/fonts/pressstart.ttf') format('truetype');font-weight:400;font-display:swap}
@font-face{font-family:'DSEG7';src:url('../assets/fonts/dseg7.woff2') format('woff2'),url('../assets/fonts/dseg7.woff') format('woff');font-weight:700;font-display:swap}

:root{
  --light:#ffffff;
  --dark:#000000;
  --on-light:#000000;
  --on-dark:#ffffff;

  /* paper = the tone actually painted behind the artwork.

     Two kinds of artwork sit on it, and they need opposite handling:

     · The WORDMARK is white artwork on transparency. It needs no blend mode at
       all — only a polarity flip when the paper does.
       --mark-inv: 0 = leave it white (dark paper), 1 = make it black (light).

     · The CUT-OUTS (dice, stars) are dark artwork printed on an opaque white
       rectangle — dice.png carries no alpha channel at all. Dropped in as-is
       they are white boxes. The rule is the same whichever way the page is:
       the artwork must end up LIGHTER than the paper and `screen` onto it, or
       DARKER than the paper and `multiply` into it. Anything else leaves the
       source rectangle sitting on top of the composition. */
  --paper:var(--dark);
  --mark-inv:0;
  --el-inv:1;
  --el-blend:screen;

  --f-vcr:'VCR',ui-monospace,'Courier New',monospace;
  --f-body:'Share',ui-monospace,'Courier New',monospace;
  --f-pix:'Silk','Press',ui-monospace,monospace;
  --f-seg:'DSEG7','VCR',ui-monospace,monospace;

  --ease:cubic-bezier(.16,1,.3,1);
  --gutter:clamp(1rem,3.5vw,3rem);
  --fade-t:7svh;         /* seam bleed, per side — 0 where there is no seam */
  --fade-b:7svh;

  /* OSD ink. Composites with `difference`, so one neutral grey reads the same
     over black paper, over white paper and over the footage itself — no
     per-section theming, and it can never vanish into a blown-out highlight. */
  --osd:rgba(150,150,150,.5);
}

body.is-invert{
  --light:#000000;
  --dark:#ffffff;
  --on-light:#ffffff;
  --on-dark:#000000;
  --mark-inv:1;
  /* paper is now white, so the cut-outs flip: keep the artwork dark and
     multiply it into the page instead of screening it out of it */
  --el-inv:0;
  --el-blend:multiply;

  /* --paper MUST be re-declared here, not just inherited.
     It is defined on :root as `var(--dark)`, and a custom property that
     references another one is resolved AGAINST THE ELEMENT IT IS DECLARED ON —
     so :root's --paper had already collapsed to #000000 and kept that value all
     the way down, no matter what body did to --dark afterwards. The stages
     stayed black while --mark-inv correctly flipped the wordmark to black, and
     the logo disappeared into its own background. */
  --paper:var(--dark);
}

*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{
  margin:0;background:var(--dark);color:var(--on-dark);
  font-family:var(--f-body);line-height:1.5;
  overflow-x:hidden;-webkit-font-smoothing:antialiased;
}
body.is-booting{overflow:hidden}
img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
::selection{background:currentColor;color:var(--dark)}
:focus-visible{outline:2px solid currentColor;outline-offset:4px}

/* One fixed backdrop behind everything, with its tone interpolated on scroll,
   so where two sections differ the tones bleed instead of meeting at a hard
   seam. Each stage paints its own opaque paper over it and fades that paper
   out at the viewport edges, which is where the bleed shows through. */
.backdrop{position:fixed;inset:0;z-index:-1;background:var(--dark)}
.sec{position:relative;overflow:hidden}

/* ============ TAPE TEXTURE ============
   Two fixed overlays over the whole document. Both are `difference`, for the
   same reason as the OSD: they must read identically on either polarity, and
   a plain white-on-black overlay would disappear the instant invert flipped. */
.grain{
  position:fixed;inset:-50%;z-index:90;pointer-events:none;
  opacity:.05;mix-blend-mode:difference;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  animation:grain 1.1s steps(4) infinite;
}
@keyframes grain{
  0%  {transform:translate(0,0)}
  25% {transform:translate(-2%,1.5%)}
  50% {transform:translate(1.5%,-1%)}
  75% {transform:translate(-1%,-1.5%)}
  100%{transform:translate(0,0)}
}
.scan{
  position:fixed;inset:0;z-index:91;pointer-events:none;
  opacity:.26;mix-blend-mode:difference;
  background:repeating-linear-gradient(
    to bottom,
    transparent 0 2px,
    rgba(255,255,255,.16) 2px 3px
  );
}
/* The tracking bar — one slow band of disturbance rolling up the screen, the
   way an unaligned tape head reads.

   Kept deliberately faint. `difference` INVERTS whatever is under it, which is
   the right instinct over a dark page but turns ugly over a bright source: at
   the strength this started on, the band crossing the PSP's screen flipped the
   picture to a flat grey wash and read as a bug rather than as tape. The
   machine already carries its own scanlines, phosphor mask and tear in the CRT
   shader, so at this level the page overlay only has to agree with it. */
.scan::after{
  content:'';position:absolute;left:0;right:0;height:14svh;
  background:linear-gradient(to bottom,transparent,rgba(255,255,255,.035) 40%,rgba(255,255,255,.05) 55%,transparent);
  animation:track 11s linear infinite;
}
@keyframes track{from{top:110%}to{top:-20%}}

/* ============ BOOT ============ */
.boot{
  position:fixed;inset:0;z-index:200;background:var(--dark);color:var(--on-dark);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.5rem;
  transition:opacity .55s var(--ease),visibility .55s;
}
.boot.is-done{opacity:0;visibility:hidden}
/* SMPTE bars — the thing a tape actually opens on */
.boot__bars{display:flex;width:min(300px,58vw);height:34px}
.boot__bars i{flex:1;background:var(--c);opacity:.85}
.boot__img{width:min(300px,58vw);filter:invert(var(--mark-inv))}
/* The dim track is a pseudo-element rather than `opacity` on .boot__bar —
   opacity on the parent would fade the FILL along with the track and the
   counter would climb against an invisible bar. */
.boot__bar{width:min(300px,58vw);height:2px;position:relative;overflow:hidden}
.boot__bar::before{content:'';position:absolute;inset:0;background:currentColor;opacity:.2}
.boot__bar span{position:absolute;inset:0 100% 0 0;background:currentColor;transition:right .16s linear}
.boot__meta{
  width:min(300px,58vw);display:flex;justify-content:space-between;align-items:baseline;
  font-family:var(--f-vcr);font-size:.8rem;letter-spacing:.16em;opacity:.6;
}
.boot__pct{font-family:var(--f-seg);letter-spacing:.06em}

/* ============ FIXED CONTROLS ============ */
.ctl{
  position:fixed;right:var(--gutter);bottom:calc(var(--gutter) - .3rem);z-index:95;
  display:flex;flex-direction:column;align-items:flex-end;gap:.25rem;
  mix-blend-mode:difference;
  opacity:0;transition:opacity .8s var(--ease) .2s;
}
.is-lit .ctl{opacity:1}
.ctl__b{
  background:none;border:0;padding:.18rem 0;cursor:pointer;
  font-family:var(--f-vcr);font-size:clamp(.7rem,1vw,.84rem);letter-spacing:.1em;
  color:#fff;line-height:1.35;position:relative;text-transform:uppercase;
}
.ctl__b::after{content:'';position:absolute;left:0;right:100%;bottom:.1rem;height:1px;background:currentColor;transition:right .35s var(--ease)}
.ctl__b:hover::after,.ctl__b[aria-pressed="true"]::after{right:0}

/* ============ OSD ============
   The camcorder overlay, pinned to the viewport corners rather than to any
   section — a real one is burnt into the signal, not into the scene. */
.osd{
  position:fixed;inset:0;z-index:94;pointer-events:none;
  font-family:var(--f-vcr);font-size:clamp(.66rem,1vw,.8rem);letter-spacing:.14em;
  color:#9a9a9a;mix-blend-mode:difference;
  opacity:0;transition:opacity 1s var(--ease) .4s;
}
.is-lit .osd{opacity:1}
.osd>*{position:absolute}
.osd__tl{top:var(--gutter);left:var(--gutter);display:flex;align-items:center;gap:.45rem}
.osd__tr{top:var(--gutter);right:var(--gutter);font-family:var(--f-seg);letter-spacing:.04em}
.osd__tr i{
  font-family:var(--f-vcr);font-style:normal;
  font-size:.78em;letter-spacing:.1em;margin-left:.45em;
}
.osd__bl{bottom:var(--gutter);left:var(--gutter)}
.osd__br{bottom:var(--gutter);right:var(--gutter);display:none}   /* the controls own this corner */
.osd__rec{width:.55em;height:.55em;border-radius:50%;background:currentColor;animation:rec 1.6s steps(2) infinite}
@keyframes rec{0%,49%{opacity:1}50%,100%{opacity:.15}}

/* ============ STAGE ============
   COVER, not contain: the compositions are full-bleed, so on a window wider
   than 16:9 the stage grows past the viewport and the section crops it.
   Contain would leave bare backdrop down both sides.

   Absolutely placed rather than grid-aligned — once the stage overflows its
   section, `align-self` silently falls back to `start` and the crop anchor
   jumps to the top. The mask is anchored to the VIEWPORT, not the stage box,
   so the seam fade stays on screen however much of the stage is cropped away. */
.stage{
  --stage-w:max(100vw, calc(100svh * 16 / 9));
  --stage-h:max(100svh, calc(100vw * 9 / 16));

  /* Centred on the viewport with explicit half-size margins, NOT `margin:auto`
     and not a transform.

     `margin:auto` is the obvious way to centre an absolutely positioned box and
     it is WRONG here. Per CSS 2.1 §10.3.7, when left/right are both 0 and the
     box is wider than its container, equal auto margins would have to be
     negative — and the spec says that in that case margin-left is set to zero
     and margin-right absorbs the remainder. The stage therefore pinned to the
     left edge and the whole composition slid right by half the overflow. On a
     1200x799 window the stage is 1420 wide, so everything sat 110px right of
     centre; it only ever looked correct at exactly 16:9, where the overflow is
     zero. The same bug applied vertically, which also put the seam mask in the
     wrong place.

     A transform is not an option either: [data-rise] owns `transform` on this
     element for the hero reveal and would cancel the centring. */
  position:absolute;
  left:50%;
  top:50%;
  margin-left:calc(var(--stage-w) / -2);
  margin-top:calc(var(--stage-h) / -2);
  width:var(--stage-w);
  height:var(--stage-h);
  container-type:size;
  background:var(--paper);
  --vtop:calc(50% - 50svh);
  --vbot:calc(50% + 50svh);
  -webkit-mask-image:linear-gradient(to bottom,
      transparent var(--vtop),
      #000 calc(var(--vtop) + var(--fade-t)),
      #000 calc(var(--vbot) - var(--fade-b)),
      transparent var(--vbot));
          mask-image:linear-gradient(to bottom,
      transparent var(--vtop),
      #000 calc(var(--vtop) + var(--fade-t)),
      #000 calc(var(--vbot) - var(--fade-b)),
      transparent var(--vbot));
}
.hero .stage{--fade-t:0}          /* no seam above the first page */
.contact .stage{--fade-b:0}       /* none below the last */

.lay{position:absolute;margin:0;z-index:5;line-height:1;transform:translateY(var(--ty,0))}

/* ---- cut-outs -----------------------------------------------------------
   z-index 1 against .lay's 5: the artwork always sits BEHIND the type, so a
   piece may overlap the wordmark freely and reads as a layered composition
   rather than as a collision.

   `max-width:none` is load-bearing. The base `img{max-width:100%}` clamps any
   cut-out authored wider than its stage — and when only ONE axis is clamped
   while the other is an explicit length, the aspect ratio breaks and the
   artwork renders stretched. These are art-directed pieces meant to bleed
   past the frame; let them. */
.el{
  position:absolute;pointer-events:none;z-index:1;max-width:none;
  filter:invert(var(--el-inv));
  mix-blend-mode:var(--el-blend);
  transform:translateY(var(--ty,0));
  user-select:none;-webkit-user-drag:none;
}

/* The angle is its own animatable custom property so the parallax translate on
   the same element stays live, instead of being frozen into the keyframes the
   moment the animation takes over `transform`. */
@property --spin{syntax:'<angle>';inherits:false;initial-value:0deg}
@keyframes spin-cw {to{--spin:360deg}}
@keyframes spin-ccw{to{--spin:-360deg}}

/* ---- 01 front ----
   The wordmark IS the hero: it is a hand-drawn graffiti lockup and anything
   set next to it competes. So it gets the middle of the stage at size, and
   the only other thing on the page is one line of tape-deck caps beneath. */
.hero{height:100svh;display:grid;place-items:center}
/* The mark is 1.506:1, so at 44cqw it stands 29.2cqw tall — which on a 16:9
   stage is 51.9cqh, running roughly 22%–74% of the height. The tagline then
   sits at 82% with real air beneath the drips.

   Both are placed with `top`, never by translating the tagline down by some
   multiple of ITS OWN height: that percentage resolves against the line box,
   so it moved every time the type size or the webfont changed, and it was
   what put the tagline through the middle of the logo. */
/* The cut-outs. Every position is percent OF THE STAGE and every size is a
   percent of its WIDTH, so the whole arrangement scales as one piece with the
   wordmark instead of drifting against it.

   The stars turn, very slowly and against each other — one revolution takes
   over two minutes, so it reads as drift rather than as an animation. */
/* Each piece gets a zone the type is not using. Worked out from the real
   aspect ratios, since `width` is a percent of the stage's WIDTH and the
   resulting height has to be reasoned about as a percent of its HEIGHT:
   star-b 1167x1170, star-a 481x603, dice 658x648.

     wordmark  x 29-71%   y 22-74%
     tagline   x 29-71%   y 82%
     star-b    x -13-20%  y 0-59%     upper left, bleeding off the edge
     star-a    x 73-98%   y -7-49%    upper right, bleeding off the top
     dice      x 6-23%    y 62-92%    lower left, under star-b

   The dice started centre-right at 57% and ran straight through the tagline.
   Cut-outs sit behind the type, so it stayed legible, but white halftone under
   wide-tracked VCR is unreadable however the z-order falls. */
.el--star-a{left:73%;top:-7%;width:25%}
.el--star-b{left:-13%;top:0%;width:33%}
.el--dice  {left:6%;top:62%;width:17%}
.el--star-a,.el--star-b{transform:translateY(var(--ty,0)) rotate(var(--spin))}
.el--star-a{animation:spin-cw 128s linear infinite}
.el--star-b{animation:spin-ccw 173s linear infinite}

.lay--mark{
  left:50%;top:50%;width:42cqw;max-width:none;
  transform:translate(-50%,calc(-54% + var(--ty,0)));
  filter:invert(var(--mark-inv));
  user-select:none;-webkit-user-drag:none;
}
.lay--tag{
  left:50%;top:82%;transform:translate(-50%,var(--ty,0));
  font-family:var(--f-vcr);font-size:1.1cqw;letter-spacing:.42em;
  white-space:nowrap;opacity:.55;text-indent:.42em;
}

.cue{position:absolute;bottom:1.4rem;left:50%;transform:translateX(-50%);z-index:6;padding:.6rem}
.cue i{display:block;width:1px;height:34px;background:currentColor;opacity:.3;position:relative;overflow:hidden}
.cue i::after{content:'';position:absolute;inset:0;background:currentColor;animation:sd 2s cubic-bezier(.65,0,.35,1) infinite}
@keyframes sd{0%{transform:translateY(-100%)}60%,100%{transform:translateY(100%)}}

/* ---- 02 the machine ---- */
.machine{height:100svh;display:flex;flex-direction:column;position:relative;background:var(--dark)}
.machine__stage{position:relative;flex:1;min-height:0;z-index:2}
#pspCanvas{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:pan-y}
/* deliberately NOT inverted: the screen and its bloom are emitted light, and
   inverting turns every highlight into a black smear */
.machine__loading{position:absolute;inset:auto 0 50% 0;text-align:center;z-index:3;transition:opacity .5s}
.machine__loading.is-off{opacity:0;pointer-events:none}
.spin{display:inline-block;width:11px;height:11px;border:1px solid currentColor;border-top-color:transparent;border-radius:50%;opacity:.5;animation:spin .9s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* Anchored to BOTH gutters, not just the left. The caption is arbitrary-length
   copy out of data.js, and left-anchored it simply grew until it wrapped to
   three lines on a phone. Bounded here and ellipsised below, it can never
   push the ↗ off screen or reflow the row. */
.rail{
  position:absolute;left:var(--gutter);
  /* stop short of the ← → ✕ legend, which is its own absolutely-positioned
     element in the same corner band; the mobile block hides that and takes
     this back to the plain gutter */
  right:calc(var(--gutter) + 9.5rem);
  bottom:clamp(1.6rem,5vh,3.2rem);z-index:6;
  display:flex;align-items:baseline;gap:.5rem;color:var(--on-dark);
}
.rail b{font-family:var(--f-seg);font-size:clamp(1.7rem,3.6vw,2.8rem);font-weight:700;line-height:1;flex:none}
.rail i{font-family:var(--f-vcr);font-style:normal;font-size:.85rem;opacity:.45;flex:none}
.rail__title{
  margin-left:.6rem;font-family:var(--f-vcr);font-size:clamp(.7rem,1.1vw,.9rem);
  letter-spacing:.2em;opacity:.75;align-self:center;
  /* min-width:0 is the load-bearing half — a flex item will not shrink below
     its content's intrinsic width without it, and text-overflow never fires */
  flex:0 1 auto;min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.rail__open{
  margin-left:.8rem;align-self:center;width:34px;height:34px;flex:none;
  display:grid;place-items:center;border:1px solid currentColor;
  font-family:var(--f-vcr);font-size:.95rem;line-height:1;opacity:.75;
  transition:background .25s,color .25s,transform .4s var(--ease),opacity .25s;
}
.rail__open:hover{background:var(--on-dark);color:var(--dark);opacity:1;transform:translate(2px,-2px)}
.rail.is-swap b{animation:flick .28s steps(2) both}
@keyframes flick{0%{opacity:0}50%,100%{opacity:1}}

/* Raised a full control-stack above the bottom edge. The invert/sound block is
   position:fixed in this same corner, and at the shared bottom offset the two
   sat exactly on top of each other. The offset clears two stacked buttons, so
   it still holds if the sound toggle comes back. */
.keys{
  position:absolute;right:var(--gutter);
  bottom:calc(clamp(1.6rem,5vh,3.2rem) + 3.6rem);
  z-index:6;display:flex;gap:.3rem;color:var(--on-dark);
}
kbd{font-family:var(--f-vcr);font-size:.72rem;letter-spacing:.08em;border:1px solid currentColor;opacity:.4;padding:.16rem .4rem;min-width:24px;text-align:center}

/* ---- 03 about ---- */
.about{height:100svh;display:grid;place-items:center}
/* The medium itself, out in the margins. The bio is a centred column running
   x 27-73%, so the tapes and the cards take the two side strips and leave the
   text a clear channel down the middle. Sources are 1100x821 (1.34), so a
   piece W% of the stage width stands W x (16/9) / 1.34 = W x 1.327 % of its
   height.

     dv-a   x -6-20%    y 16-51%    one cassette, upper left, bleeding off
     ms     x -4-18%    y 62-91%    the cards, under it
     dv-b   x 76-106%   y 52-92%    two cassettes, lower right, bleeding off */
.el--dv-a{left:-6%;top:16%;width:26%}
.el--dv-b{left:76%;top:52%;width:30%}
.el--ms  {left:-4%;top:62%;width:22%}

.lay--abouth{
  left:50%;top:24%;transform:translate(-50%,var(--ty,0));
  font-family:var(--f-vcr);font-size:3.4cqw;letter-spacing:.3em;text-indent:.3em;
}
.lay--bio{
  left:50%;top:37%;width:46%;margin-left:-23%;
  font-size:1.55cqw;line-height:1.6;
  /* NOT justified. Every glyph in a monospace face is the same width, so a
     stretched word-space is as wide as a character and the rivers are far more
     visible than they would be in a proportional face. */
  text-align:left;
}
.lay--bio p{margin:0}
.lay--bio p+p{margin-top:.9em}

/* A soft scrim so the copy never has to compete with the cut-outs behind it.

   Painted in `--paper`, not in a fixed colour, so it darkens toward black on
   the dark page and lightens toward white when inverted — in both cases it
   pushes the background AWAY from the text colour rather than toward it.

   Heavily blurred and inset negatively: a hard-edged panel would read as a
   box sitting on the composition, which is exactly the look this page is
   avoiding. Blurring it leaves a cloud with no discernible edge.

   `z-index:-1` puts it behind the text but still inside .lay's own stacking
   context (.lay is z-index 5), so it stays above the cut-outs at z-index 1 —
   which is the whole point. */
.lay--bio::before{
  content:'';position:absolute;inset:-14% -9%;z-index:-1;
  background:var(--paper);
  opacity:.8;
  filter:blur(40px);
  border-radius:46% / 34%;
}

/* ---- 04 contact ---- */
.contact{height:100svh;display:grid;place-items:center}
.lay--contact{
  left:50%;top:22%;transform:translate(-50%,var(--ty,0));
  font-family:var(--f-vcr);font-size:5.2cqw;letter-spacing:.06em;
  text-align:center;line-height:1.05;
}
.lay--links{
  left:50%;top:58%;transform:translate(-50%,var(--ty,0));
  display:flex;gap:9cqw;
}
.lay--links a{
  font-family:var(--f-vcr);font-size:1.5cqw;letter-spacing:.24em;
  display:flex;flex-direction:column;gap:.55em;position:relative;padding-bottom:.5em;
}
.lay--links a i{font-family:var(--f-body);font-style:normal;font-size:.8em;letter-spacing:.06em;opacity:.62}
.lay--links a::after{content:'';position:absolute;left:0;right:100%;bottom:0;height:1px;background:currentColor;transition:right .4s var(--ease)}
.lay--links a:hover::after{right:0}
/* The camera, bottom left — the one object on the page that says what all of
   this was actually shot on. Same cut-out treatment as the dice and the stars,
   so it inverts and blends with everything else.

   Bottom LEFT and bleeding off two edges, the way the stars do on the front —
   a cut-out sitting politely inside the frame reads as a stock photo; one that
   runs off it reads as art direction.

   It stops at x 32%. The links are a centred group spanning roughly x 32-68%
   and the colophon x 37-63%, so the camera clears both: small dim pixel type
   over a photograph is unreadable whichever way the z-order falls. Source is
   1200x896 (1.339), so at 39% of the stage width it stands
   39 x (16/9) / 1.339 = 51.8% of its height, running y 54-106%. */
.el--vx{left:-7%;top:54%;width:39%}

.lay--colophon{
  left:50%;bottom:5%;transform:translate(-50%,var(--ty,0));top:auto;
  font-family:var(--f-pix);font-size:.86cqw;letter-spacing:.14em;opacity:.45;white-space:nowrap;
}

/* ============ MOTION ============
   Nothing here is a page transition — each piece drifts at its own rate as you
   scroll, so a section reads as a moving composition rather than a flat still. */
.el,.lay{will-change:transform}
.rev{opacity:0;filter:blur(7px)}
.rev.is-shown{opacity:1;filter:none;transition:opacity 1.1s var(--ease),filter 1.1s var(--ease)}
/* Anything carrying a polarity must keep it while revealing. `filter` is a
   single property: the bare `filter:none` above would drop the invert on the
   last frame of the transition and flash the artwork the wrong way round —
   for the cut-outs that means the white source rectangles reappearing. */
.lay--mark.rev{filter:invert(var(--mark-inv)) blur(9px)}
.lay--mark.rev.is-shown{filter:invert(var(--mark-inv));transition:opacity 1.1s var(--ease),filter 1.1s var(--ease)}
.el.rev{filter:invert(var(--el-inv)) blur(9px)}
.el.rev.is-shown{filter:invert(var(--el-inv));transition:opacity 1.1s var(--ease),filter 1.1s var(--ease)}

[data-rise]{opacity:0;transform:translateY(18px);filter:blur(6px)}
.is-lit [data-rise]{opacity:1;transform:none;filter:none;transition:opacity .9s var(--ease) .1s,transform 1s var(--ease) .1s,filter .9s var(--ease) .1s}

/* ============ NARROW SCREENS ============
   One coordinate system, same as desktop — only the aspect changes. The stage
   stays absolute and keeps `container-type:size` so `cqw` still resolves;
   making it a flow item would turn it into a flex child inside .machine, where
   it would claim the full height, collapse .machine__stage to zero and stop
   the machine
   rendering at all (resize() bails on a zero-size canvas). */
/* ============ NARROW ============
   One coordinate system, same as desktop — only the aspect changes.

   Every type size below is `min(Ncqw, Mcqh)`. Sizing on cqw alone is correct
   for a phone and wrong for a tablet: at 390x799 the stage is portrait and
   cqw is the scarce axis, but at 810x799 it is nearly square, and the same
   figure resolves to twice the pixels while the height available for it has
   not changed at all. That is what pushed the About copy off the bottom of
   the section and put the tape on top of the heading. The cqh term caps it. */
@media (max-width:820px){
  .keys{display:none}
  .rail{right:var(--gutter)}    /* the legend is gone; take the space back */
  .osd{font-size:.62rem}

  .hero,.about,.contact{height:100svh;min-height:0}
  .stage{
    position:absolute;
    inset:0;                       /* resets the left/top used above */
    margin:0;                      /* and the negative centring margins */
    width:100%;height:100%;
    container-type:size;
    -webkit-mask-image:none;mask-image:none;
  }

  /* Portrait stage: the wordmark takes the middle band (y 34-63%), so the
     pieces go above and below it rather than beside it.

       dice     y  5-19%    top left
       star-a   y  3-32%    top right
       wordmark y 34-63%
       tagline  y 68-70%
       star-b   y 78-110%   bottom left, bleeding off

     star-b used to start at 66% and ran straight under the tagline. The type
     is above it in z-order, but white VCR over a white halftone is invisible
     either way — the first two characters simply disappeared into the star. */
  .el--star-a{left:55%;top:3%;width:45%}
  .el--star-b{left:-30%;top:78%;width:68%}
  .el--dice  {left:7%;top:5%;width:29%}

  .lay--mark{width:min(86cqw,83cqh);transform:translate(-50%,calc(-56% + var(--ty,0)))}
  .lay--tag{
    top:68%;bottom:auto;font-size:min(2.6cqw,1.45cqh);letter-spacing:.26em;text-indent:.26em;
    transform:translate(-50%,var(--ty,0));
  }

  /* Portrait: the bio spans nearly the full width, so there are no side
     strips to work with. The tape goes above the heading and the cards below
     the copy, and the two-cassette piece drops out entirely — three props
     around a full-width column is clutter, not composition. */
  .el--dv-a{left:58%;top:2%;width:42%}
  .el--ms  {left:-8%;top:84%;width:44%}
  .el--dv-b{display:none}

  /* Anchored so the heading+copy group lands centred rather than pinned near
     the top. At 390x799 the group runs 480px, so its top wants to be at
     (799-480)/2 = 160px = 20%; the copy then clears the heading by the same
     ~57px it has on desktop. The old 16%/27% left 191px of dead space under
     the last line. */
  .lay--abouth{top:20%;font-size:min(8cqw,4.4cqh)}
  .lay--bio{left:7%;top:31%;width:86%;margin-left:0;font-size:min(3.9cqw,2.15cqh);text-align:left}

  .lay--contact{top:18%;font-size:min(13cqw,7.2cqh);line-height:1.05}
  .lay--links{top:50%;flex-direction:column;gap:8cqh;align-items:center;text-align:center}
  .lay--links a{font-size:min(4.4cqw,2.45cqh);align-items:center}
  .lay--colophon{font-size:min(2.1cqw,1.16cqh);bottom:6%}
  /* portrait: the links stack, so the camera drops to the very bottom and is
     allowed to bleed off the edge rather than fighting them for the corner */
  .el--vx{left:50%;top:auto;bottom:-6%;width:62%;transform:translate(-50%,var(--ty,0))}
}

/* ============ REDUCED MOTION ============ */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .cue i::after,.rail.is-swap b,.osd__rec,.grain,.scan::after{animation:none}
  .el--star-a,.el--star-b{animation:none;transform:translateY(var(--ty,0))}
  .rev{opacity:1;filter:none;transition:none}
  .lay--mark.rev,.lay--mark.rev.is-shown{opacity:1;filter:invert(var(--mark-inv));transition:none}
  .el.rev,.el.rev.is-shown{opacity:1;filter:invert(var(--el-inv));transition:none}
  .el,.lay{--ty:0 !important}
  [data-rise]{opacity:1;transform:none;filter:none;transition:none}
  .lay--links a::after,.ctl__b::after{transition:none}
}

/* ============ TABLET BAND ============
   601-820px. Same coordinate system as the phone — this only re-anchors.

   The copy reflows much shorter here than on a phone (three short paragraphs
   instead of long ones), so the same top offsets that centre the group at
   390x799 leave 305px of dead space at 810x799. The group is 366px tall at
   this width, so its top wants (799-366)/2 = 216px = 27%.

   The tape also has to move: at 42% wide from the top it reached y 270 and ran
   straight through the first lines of copy. Smaller, and tucked into the
   corner, it clears both the heading and the text. */
@media (min-width:601px) and (max-width:820px){
  .lay--abouth{top:27%}
  .lay--bio{top:38%}
  .el--dv-a{left:68%;top:1%;width:30%}
  .el--ms{left:-6%;top:80%;width:36%}
}
