/* =========================================================
   OUR WORKS - brutalist editorial / analog CRT landing page

   v2. The stack is the supplied clean plate - a photographed
   pile of CRTs with every screen dark - keyed off its backdrop
   at load and composited onto the paper. Each tube's glass was
   measured off the plate, and a live screen is mapped onto it
   with a projective transform, so the content sits in the real
   perspective of the photograph.

   Design canvas: the plate's own 1536 x 1024.
   ========================================================= */

:root{
  /* belt and braces with the color-scheme meta in the head - this page is
     light only, and must not be auto-darkened */
  color-scheme: only light;

  /* ---- SHARED WITH THE DECK ----
     The Tape Archive's palette is authoritative for the merged site, so these
     moved DOWN from the wall's original lighter set (#e6e3dd / #121212 /
     #ea1f27). tape-archive.css declares no tokens of its own — it reads these.

     Consequence, not yet actioned: tools/bake-plate.js chose its shadow
     STRENGTH by eye against the OLD lighter sheet. On this darker floor the
     baked plate-shadow.png reads slightly weak and wants re-baking. */
  --paper:    #d7d3c9;   /* dirty off-white, one step darker than before */
  --ink:      #100f0e;   /* charcoal black */
  --ink-soft: #3c3b39;   /* deep grey (the OUR type) */
  --red:      #e02418;   /* aggressive signal red */
  --grey:     #4a4843;
  --grey-lt:  #807d74;

  --nav-h: 58px;
  --foot-h: 44px;

  --disp:'Anton','Haettenschweiler','Arial Narrow',Impact,sans-serif;
  --ui:'Archivo','Helvetica Neue',Arial,sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,'Courier New',monospace;
  --noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");

  --stage-w: 1536;
  --stage-h: 1024;

  /* The pile's shadow is a baked image, not a filter - see .wall__shade below.
     Must equal SHADOW_PAD in tools/bake-plate.js. */
  --shadow-pad: 300px;
}

/* Lenis owns the scroll for the whole document now (see js/app.js). */
html{ -webkit-text-size-adjust:100%; text-size-adjust:100%;
      scrollbar-width:thin; scrollbar-color:var(--ink) var(--paper); }
html.lenis,html.lenis body{ height:auto; }
.lenis.lenis-smooth{ scroll-behavior:auto!important; }
::selection{ background:var(--red); color:#fff; }
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

*{ box-sizing:border-box; }
html,body{ margin:0; padding:0; }
html{ background:var(--paper); }

body{
  min-height:100%;
  background:var(--paper);
  color:var(--ink);
  font-family:var(--ui);
  overflow-x:hidden;
  cursor:none;
  -webkit-font-smoothing:antialiased;
}
/* the pager holds the page still while the deck is armed */
body.locked{ overflow:hidden; }

.sr-only{
  position:absolute; width:1px; height:1px; margin:-1px;
  padding:0; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}
.defs{ position:absolute; width:0; height:0; }

/* =========================================================
   PAPER / GRAIN / VIGNETTE
   No mix-blend-mode on any full-viewport layer: a blended layer
   covering the screen forces the compositor to re-blend
   everything beneath it whenever anything moves.
   ========================================================= */
/* The sheet darkens down the frame, and it has to: the pile stands at ~87% of
   the viewport height, and how grounded it looks depends as much on how dark
   the surface under it is as on the shadow itself. A correct shadow over too
   bright a floor still reads as floating - which is exactly what happened here.
   The stops track the reference render's own sheet, sampled down its far-left
   margin clear of the pile: ~226 at the top, ~205 at mid, ~191 at the feet.
   The previous ramp bottomed out at 212, and no shadow can make up 21 levels of
   missing floor. */
/* Every stop here shifted down ~15 levels with --paper (#e6e3dd → #d7d3c9).
   The ramp is not decorative: it was sampled off the reference render's own
   sheet, and the pile only reads grounded because the floor darkens under it.
   Move --paper without moving these and the pile floats again. */
.paper{
  position:fixed; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(120% 90% at 22% 8%, #e3dfd5 0%, rgba(227,223,213,0) 55%),
    radial-gradient(100% 80% at 82% 92%, #c0baad 0%, rgba(192,186,173,0) 60%),
    linear-gradient(180deg,#dbd7cd 0%, #d1cdc2 30%, #bcb7ac 62%, #b0aca1 100%);
}
.paper::after{
  content:""; position:absolute; inset:-2%;
  background-image:var(--tex-plaster);
  background-size:900px 900px;
  opacity:.55;
}
.grain{
  position:fixed; inset:-50px; z-index:60; pointer-events:none;
  background-image:var(--tex-ink);
  background-size:220px 220px;
  opacity:.5;
  animation:grainshift 1.2s steps(1,end) infinite;
  will-change:transform;
}
@keyframes grainshift{
  0%{transform:translate3d(0,0,0)}      25%{transform:translate3d(-14px,9px,0)}
  50%{transform:translate3d(11px,-12px,0)} 75%{transform:translate3d(-7px,-6px,0)}
  100%{transform:translate3d(0,0,0)}
}
.vignette{
  position:fixed; inset:0; z-index:59; pointer-events:none;
  background:radial-gradient(105% 105% at 50% 46%, rgba(0,0,0,0) 52%, rgba(20,18,16,.30) 100%);
}

/* =========================================================
   SCROLL TRACK + SCALED STAGE
   ========================================================= */
/* The scene container. Its height is the sum of its children, NOT a fixed vh:
   .stage-wrap (100vh) + .wall-runway + .deck-track. The wall sticks to the top
   of this box, so it stays pinned behind the deck instead of unpinning when the
   wall's own runway ends.

   z-index here (rather than none) makes .stage-wrap a stacking context, which
   contains the wall's internal z-indexes - .stage-px at 20, .typelayer--front
   at 34 - underneath the deck at 3. Without it those would compete globally and
   the front type layer would punch through the deck. */
/* Spacers only now — every visual lives in a fixed layer above. */
.scroll-track{ position:relative; }

/* The wall's scroll budget, and the ONLY thing fit() measures for trackMax.

   90vh, down from 240vh. The pile lands at p=0.12 and goes live at p=0.22, and
   NOTHING scroll-driven happens after that - the ignition runs on its own clock
   and hovering needs no scroll at all. At 240vh that left 1.9 viewport-heights
   of scrolling in which the only thing that moved was the lettering drifting
   26px, which reads as the page having seized. At 90vh the same thresholds land
   at 10.8vh and 19.8vh and the push-in starts at 40.5vh - see HANDOFF_START. */
/* 140vh, up from 90. The retired carousel rose as a DOM layer, so a short
   runway was right. The deck is a FIXED canvas that exists from load, so the
   runway has to be longer than a viewport or #deckTrack is already on screen
   at scroll 0 and the shelf arrives before the wall has been seen at all. */
.wall-runway{ height:140vh; }

/* The deck rises over the wall, then pins for 100vh of dwell. At maximum scroll
   .deck-pin's bottom meets the viewport bottom, so the document ends on a full
   deck with no dead space under it. */
/* 120vh = 100vh of rise + 20vh of dwell once pinned. The dwell was 100vh, which
   is another viewport-height of scrolling with nothing to show for it - "scroll
   delivers the deck and stops" wants a beat, not a dead screen. */
.deck-track{ position:relative; height:120vh; }
/* the hero sticks here while the pager owns the gesture; app.js arms on the
   ScrollTrigger that fires when this pins, and releases on scroll-down */
.deck-pin{ position:sticky; top:0; height:100vh; width:100%; }

/* The track and its children are pure spacers, but they live inside main (z5)
   and so sit OVER the fixed wall (z2) for hit-testing across the whole
   viewport. The tube hover survived that because it is raycast maths on
   window coordinates; .type-hit is a real DOM target, so these swallowed the
   pointerenter that lifts OUR WORKS off the pile. Nothing in the track is
   interactive except .hero-meta, which opts back in (tape-archive.css). */
.scroll-track, .wall-runway, .deck-track, .deck-pin{ pointer-events:none; }

/* the wall stops taking the pointer the moment the deck owns the screen.
   .tube sets pointer-events:auto, so it has to be turned off at the tube. */
body.deck-in .tube, body.deck-in .type-hit{ pointer-events:none !important; }
body.deck-in .hud, body.deck-in .scroll-cue{ opacity:0; }

/* ---- what the pile costs once the deck owns it ----
   The zoom scales a subtree holding 26 tubes that each carry an
   feDisplacementMap warp, and a filtered subtree is re-rastered at every new
   scale - 26 SVG filter passes per step, which is the stutter.

   The warp is a sub-pixel barrel distortion on glass that is, by this point,
   sitting under a 66% shade, at 34% brightness, behind the deck's scrim. It is
   not perceptible there and it is the single most expensive thing on screen, so
   it goes. !important because frame() writes filter inline, and an important
   author rule outranks a normal inline declaration.

   The sets keep PLAYING behind the deck - roll, snow and the hold bar are what
   stop the backdrop reading as a still - so the looping animations are only
   paused while the zoom is actually in motion, not for the whole time the deck
   is up. Nobody reads a rolling bar during a one-second push-in, and it keeps
   26 repainting layers off the compositor's back exactly when it is busiest. */
body.deck-zoom .tube{ filter:none !important; }

/* THE expensive one. The lettering carries #photocopy - an feTurbulence at
   numOctaves 3 feeding two feDisplacementMap passes - plus #brushrough on the
   script and the reveal mask, all of it covering the full stage area. Re-raster
   that at sixty different scales and the push-in stutters no matter what the
   tubes are doing; the 26 tube warps above are the cheap half of the problem.

   The grunge is a paper texture on lettering that sits at .42 opacity behind
   the deck's scrim by then. Dropping it there is invisible. */
body.deck-zoom .typelayer g{ filter:none !important; }

/* The mask is the write-on reveal itself, so it can only go once the write-on
   has actually finished - hence .write-done, set by intro(). Dropping it
   unconditionally broke reloading part-way down the page: deck-zoom is applied
   on the first scroll callback, the mask went with it, and the lettering just
   appeared instead of drawing itself. */
body.deck-zoom.write-done .typelayer g{ mask:none !important; }
/* ---- what pauses during the push-in, and what does not ----
   All three of these animate translate3d, which is free on its own. It is not
   free HERE: they live inside .stage, and .stage is being scaled, so every
   animating layer under it is re-rasterised at each new scale — 26 tubes times
   three layers. Pausing all three is what kept the push-in smooth, and dropping
   that outright made it visibly jittery.
   I tried pausing only .noise, on the theory that its .13s step rate was where
   all the cost was and the slow roll and hold-bar could keep running. It was
   not enough — the push-in still jittered. All three go back to paused.

   This is a real trade and worth stating: for the length of the travel the
   screens hold. What makes it acceptable now is that the travel is SHORT — the
   zone snap resolves the push-in in about 0.4s and never lets you park inside
   it, which is not true of the version this pause was first written for. */
body.deck-moving .tube__roll,
body.deck-moving .noise,
body.deck-moving .hbar{ animation-play-state:paused !important; }
/* promoted only while it is actually moving - a permanent layer here would
   pin the whole plate in memory for the entire wall section */
body.deck-zoom .stage{ will-change:transform; }
/* the plate/shade push-back lives further down, immediately after the
   .type-lift rules it has to override on source order. */
/* placed outright by fit(): translate + scale from the top-left, so the pile
   can be positioned by its own bounds rather than the plate's */
.stage{
  position:absolute; left:0; top:0;
  width:calc(var(--stage-w) * 1px);
  height:calc(var(--stage-h) * 1px);
  transform-origin:0 0;
}

/* =========================================================
   TYPE PINS
   The lettering lives outside .stage-wrap - see index.html. Fixed rather than
   sticky so three overlapping full-height layers do not add 200vh of flow, and
   because the scene runs the whole document anyway.

   .type-stage is a copy of .stage's box, and paintType() gives it the same
   translate+scale on the wall. On the deck that transform is interpolated to
   the header instead, which is what makes it one asset scaling rather than two
   assets crossfading.
   ========================================================= */
/* ---- THE STACK (the one thing to keep straight in the merged site) ----
     0  .paper            the sheet
     1  .type-pin--back   OUR WORKS, behind the pile
     2  .wall-scene       the CRT pile and its screens
     3  .type-pin--front  the lettering lifted in front of the pile
     4  #gl               the WebGL shelf — ABOVE the wall, so tapes are in front
     5  main              page content, scrolls over all of it
    30  .type-pin--front  the lettering over EVERYTHING — the hover lift, and
                          the push-in, where it has to clear the cassettes
   Every one of these is fixed or positioned, and each creates its own stacking
   context, so nothing inside them can escape — which is exactly why the type
   pins are siblings of the wall rather than children.

   The back pin used to be promoted to z30 for the push-in so OUR WORKS would
   read over the cassettes. A z-index cannot tween, so that was a single frame:
   the lettering was behind the tubes, and then it was in front of everything,
   with nothing in between. Now the FRONT pin — which already sits above the
   pile and already crossfades, because that is exactly what the hover lift
   does — is simply raised above main and the canvas, and its opacity is
   driven by the push-in. Same layers, same mechanism, no jump. */
.type-pin{ position:fixed; inset:0; overflow:hidden; pointer-events:none; }
.type-pin--back { z-index:1; }
.type-pin--front{ z-index:30; }

/* The wall's visual layer. Fixed, not sticky: it covered the viewport for the
   whole track anyway, and fixed keeps it out of <main>'s flow so the canvas
   can sit above it. It fades out entirely once the practice blocks start —
   past the deck there is no pile, only the paper and the shelf. */
.wall-scene{
  position:fixed; inset:0; z-index:2; overflow:hidden;
  pointer-events:none;
  will-change:opacity;
}
.wall-scene .stage-wrap{ position:absolute; inset:0; width:100%; height:100%; overflow:hidden; }
/* the pile's tubes are the one thing in here that takes the pointer */
.wall-scene .tube{ pointer-events:auto; }
body.deck-in .wall-scene .tube{ pointer-events:none !important; }

.type-stage{
  position:absolute; left:0; top:0;
  width:calc(var(--stage-w) * 1px);
  height:calc(var(--stage-h) * 1px);
  transform-origin:0 0;
}

/* =========================================================
   TYPE LAYER
   ========================================================= */
.typelayer{
  position:absolute; inset:0;
  display:grid; place-items:center;
  pointer-events:none;
  will-change:transform;
}
.typelayer svg{ width:100%; height:100%; overflow:visible; }

.typelayer--back{
  z-index:3; opacity:0;
  transition:opacity 1.1s cubic-bezier(.16,.84,.3,1);
}
.typelayer--back.is-in{ opacity:1; }
/* Handed over to the front copy the moment the push-in starts, and handed back
   on the way up. One transition each, on their own clock — by the time the pile
   has travelled any distance the crossfade is over and exactly ONE of the two
   layers is painting, which is what the single promoted layer used to do. */
body.deck-zoom .typelayer--back.is-in{
  opacity:0; transition:opacity .4s cubic-bezier(.4,0,.2,1);
}

/* Promoted up front: this copy carries the same turbulence filter as the one
   behind the wall, and Chrome will not rasterise it while it sits at opacity
   0 - without this the whole filtered layer renders on the first frame of
   the lift, mid-transition. */
.typelayer--front{
  z-index:34; opacity:0;
  transition:opacity .55s cubic-bezier(.4,0,.2,1);
  will-change:opacity;
}
body.type-lift .typelayer--front{ opacity:1; }
/* the push-in brings it forward too, slightly quicker than the hover lift so it
   has clearly landed before the pile has gone anywhere */
body.deck-zoom .typelayer--front{ opacity:1; transition:opacity .4s cubic-bezier(.4,0,.2,1); }

.t-our{
  font-family:'Anton', 'Archivo Black', Impact, 'Haettenschweiler', sans-serif;
  fill:var(--ink-soft); letter-spacing:-.012em;
}
.t-works{
  font-family:'Yellowtail', 'Kaushan Script', 'Brush Script MT', cursive;
  fill:var(--red);
}
.t-shadow{ fill:#ffffff; opacity:.55; }
.pen{ fill:var(--red); opacity:0; }

/* The reveal itself lives in script.js - a threshold sliding across a
   geodesic distance field baked through the ink. Nothing to style here but
   the pen that rides its front. */
/* keep in step with WRITE_EASE in script.js */
.is-writing .pen{ animation:penrun 2200ms cubic-bezier(.1,0,.1,1) forwards; }
@keyframes penrun{ 0%{opacity:0} 6%{opacity:1} 92%{opacity:1} 100%{opacity:0} }

/* the open void between the towers - the only place the type reads through */
.type-hit{
  position:absolute;
  left:512px; top:20px; width:500px; height:640px;
  z-index:5; pointer-events:none;
}
body.wall-live .type-hit{ pointer-events:auto; }

/* =========================================================
   THE PLATE
   ========================================================= */
/* Carries the pointer parallax for the whole stack. Sits where .wall used to
   in the stacking order, so the type still reads behind it and the front type
   layer still lifts over it.

   pointer-events:none is NOT optional. This box covers the whole stage at
   z-index 20, and .type-hit - the void between the towers that lifts the type
   forward - sits under it at z-index 5. Left hittable, the wrapper eats every
   pointerenter aimed at the void and the lift never fires. The tubes are
   unaffected: .tube sets pointer-events:auto and a descendant may opt back in,
   which is the same arrangement .wall and .screens already rely on. */
.stage-px{
  position:absolute; inset:0; z-index:20;
  pointer-events:none;
  will-change:transform;
}

.wall{
  position:absolute; inset:0; z-index:20;
  pointer-events:none;
  opacity:0;
  transform:translate3d(0, 54px, 0) scale(.978);
  transition:
    opacity 1s cubic-bezier(.2,.75,.25,1),
    transform 1.25s cubic-bezier(.14,.86,.24,1);
}
.wall.wall-in{ opacity:1; transform:none; }
.wall__plate{
  position:absolute; inset:0;
  width:100%; height:100%;
  display:block;
}
.wall__plate{ transition:filter .42s ease; }
/* The whole pile darkens when the type lifts. One filtered element, and
   because the plate is keyed the dimming lands on the sets and nowhere on
   the sheet. The shadow is on its own element now, so it stays put while the
   pile dims - which is what a shadow on a floor does. */
body.type-lift .wall__plate{
  filter:brightness(.5) contrast(1.03) saturate(.85);
}
/* ---- the wall pushed back behind the deck ----
   Same specificity as the .type-lift rule above, so it has to sit AFTER it to
   win - once the deck is in, the pile stays pushed back regardless of whatever
   hover state the wall was left in. Brightness and saturation only, no blur: a
   blur here would re-rasterise a 1536x1024 plate scaled to the viewport on
   every frame of the transition. */
body.deck-in .wall__plate{
  filter:brightness(.34) contrast(1.02) saturate(.55);
}

/* ---------- the ground shadow ----------
   A pre-baked image (tools/bake-plate.js), not a CSS filter: it never changes,
   and the live version would be three wide Gaussians over the whole plate on an
   element that rescales with the viewport.

   It is a pool on the FLOOR, so it is wide and vertically squashed - a patch of
   ground in perspective. It fans ~120 plate px sideways past the pile and has
   recovered within ~67 px downward. The canvas is padded by --shadow-pad on the
   sides and below so that fan is not sheared off at the plate's edge. */
.wall__shade{
  position:absolute;
  left:calc(-1 * var(--shadow-pad)); top:0;
  width:calc(100% + var(--shadow-pad) * 2);
  height:calc(100% + var(--shadow-pad));
  display:block;
  pointer-events:none;
}

/* =========================================================
   THE SCREENS
   One element per tube, mapped onto the measured glass with a
   projective transform, so it inherits the photograph's own
   perspective instead of approximating it.
   ========================================================= */
.screens{ position:absolute; inset:0; z-index:22; pointer-events:none; }

.tube{
  position:absolute; left:0; top:0;
  transform-origin:0 0;
  overflow:hidden;
  border-radius:var(--rad, 10px);
  background:#050505;
  pointer-events:auto;
  opacity:0;
  transition:opacity .35s ease;
}
.tube.lit{ opacity:1; }

/* Flush with the tube. The barrel warp samples INWARD, so it can never expose
   an uncovered edge - overscanning here only pushed content out of frame, and
   clipped the first character off the left of the text panels. */
.tube__view{ position:absolute; inset:0; }
.tube__scroller{
  position:absolute; left:0; top:0; width:100%;
  transform:translate3d(0, calc(var(--sy,0) * -1px), 0);
  will-change:transform;
}

/* phosphor, curvature, glare - matched to the plate's own glass */
/* A short burst of snow as a tube takes the cursor - the set catching the
   signal. Inset past the edges so the jitter never exposes a corner; it is
   driven from JS with the Web Animations API so re-entering the same tube
   restarts it cleanly, with no class thrash and no forced layout. */
.tube__burst{
  position:absolute; inset:-20%;
  background-image:var(--tex-static);
  background-size:150px 150px;
  opacity:0; pointer-events:none;
  z-index:4;
}
.tube__scan{
  position:absolute; inset:-2px; pointer-events:none;
  background:repeating-linear-gradient(180deg,
    rgba(0,0,0,.42) 0 1px, rgba(0,0,0,0) 1px 3px);
  opacity:.55;
}
.tube__glass{
  position:absolute; inset:0; pointer-events:none;
  background:
    linear-gradient(112deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.03) 15%, rgba(255,255,255,0) 32%),
    radial-gradient(118% 118% at 50% 50%, rgba(0,0,0,0) 54%, rgba(0,0,0,.66) 100%),
    linear-gradient(rgba(0,0,0,.22), rgba(0,0,0,.22));
}
.tube__roll{
  position:absolute; left:0; right:0; top:0; height:38%;
  background:linear-gradient(180deg, rgba(255,255,255,0), rgba(255,255,255,.06), rgba(255,255,255,0));
  pointer-events:none;
  /* ---- why this is stepped and not linear ----
     Every tube carries an feDisplacementMap warp, and an SVG filter is
     re-evaluated whenever anything inside it moves. A linear roll moves
     every frame, on all 26 tubes: 1560 filter passes a second to slide a
     6%-white gradient band. Stepped to 24 fps the band travels 1% of the
     tube per step — under the glass, the glare and the shade, that is not
     a thing anyone can see, and it is two thirds of the wall's cost gone.
     24 rather than a rounder number because a scan you can just about
     catch is what a tube looks like on camera anyway. */
  animation:roll 7.5s steps(180, end) infinite; animation-play-state:paused;
}
.tube.lit .tube__roll{ animation-play-state:running; will-change:transform; }
@keyframes roll{ from{ transform:translate3d(0,-105%,0) } to{ transform:translate3d(0,285%,0) } }
.tube__chroma{
  position:absolute; inset:0; pointer-events:none; opacity:0;
  background:
    linear-gradient(90deg, rgba(255,60,90,.42), rgba(0,0,0,0) 6%),
    linear-gradient(270deg, rgba(90,190,255,.38), rgba(0,0,0,0) 6%);
}
.tube__flash{
  position:absolute; left:0; right:0; top:50%; height:2px;
  background:#fff; transform:translateY(-50%);
  opacity:0; pointer-events:none;
  box-shadow:0 0 22px 6px rgba(255,255,255,.9);
}
.tube.power .tube__flash{ animation:powerOn 640ms cubic-bezier(.2,.8,.2,1) forwards; }
@keyframes powerOn{
  0%{opacity:0;height:0} 12%{opacity:1;height:2px} 40%{opacity:1;height:3px}
  70%{opacity:.85;height:100%} 100%{opacity:0;height:100%}
}
.tube.power .tube__view{ animation:contentIn 700ms 120ms cubic-bezier(.2,.8,.2,1) both; }
@keyframes contentIn{
  0%{transform:scaleY(.02); filter:brightness(3) blur(1px)}
  55%{transform:scaleY(1.04); filter:brightness(1.5) blur(.4px)}
  100%{transform:scaleY(1); filter:none}
}
/* the shade that dims the tubes when the type lifts. Only opacity animates -
   a compositor property, so no re-raster and no new surfaces. */
.tube__shade{
  position:absolute; inset:0; z-index:8; pointer-events:none;
  background:rgba(9,8,6,.66);
  opacity:0; transition:opacity .42s ease;
}
body.type-lift .tube__shade{ opacity:1; }
body.deck-in .tube__shade{ opacity:1; }

/* the glow a lit tube throws onto the bezel around it */
.bloom{
  position:absolute; pointer-events:none; z-index:21;
  opacity:0;
  background:radial-gradient(58% 58% at 50% 50%,
    rgba(228,226,218,.20), rgba(228,226,218,.06) 55%, rgba(228,226,218,0) 78%);
}

/* =========================================================
   SCREEN CONTENT
   ========================================================= */
.scr{ position:absolute; inset:0; background:#070707; color:#e9e7e2; }

.card{
  position:absolute; inset:0; padding:.055em .07em;
  display:flex; flex-direction:column; z-index:3; pointer-events:none;
}
.card__n, .card__t, .card__c{ max-width:62%; }
.card__n{ font-weight:600; font-size:.062em; letter-spacing:.16em; color:#cfcbc4; }
.card__t{
  font-family:'Anton',sans-serif; font-size:.105em; line-height:1.02;
  margin-top:.008em; color:#f2efe9; text-transform:uppercase;
  text-shadow:0 0 6px rgba(255,255,255,.18);
}
.card__c{
  margin-top:.035em; font-size:.044em; line-height:1.5; letter-spacing:.13em;
  color:#a09b93; text-transform:uppercase; white-space:pre-line; font-weight:500;
}
.card__arrow{
  position:absolute; right:.06em; bottom:.055em; color:var(--red);
  font-size:.062em; letter-spacing:-.04em;
  text-shadow:0 0 6px rgba(234,31,39,.7);
}
.tube.hot .card__arrow{ animation:arrowBlink .5s steps(1,end) infinite; }
@keyframes arrowBlink{ 0%,60%{opacity:1} 61%,100%{opacity:.25} }
.card::before{
  content:""; position:absolute; inset:0; z-index:-1;
  background:
    linear-gradient(90deg, rgba(4,4,4,.96) 0%, rgba(4,4,4,.92) 42%, rgba(4,4,4,.30) 68%, rgba(4,4,4,.06) 100%),
    linear-gradient(180deg, rgba(4,4,4,.5) 0%, rgba(4,4,4,0) 30%, rgba(4,4,4,0) 70%, rgba(4,4,4,.6) 100%);
}

.scr--text{ background:#0a0a0a; display:flex; align-items:flex-start; padding:.075em .085em; }
.scr--text .lines{
  font-family:'Anton',sans-serif; text-transform:uppercase;
  font-size:.105em; line-height:1.16; color:#efece6;
}
.scr--text.small .lines{ font-size:.083em; line-height:1.3; }

.scr--contact{ padding:.07em .075em; background:#0a0a0a; }
.scr--contact h3{
  margin:0 0 .05em; font-family:'Anton',sans-serif; font-size:.09em;
  text-transform:uppercase; color:#f0ede7;
}
.scr--contact dl{ margin:0; font-size:.042em; letter-spacing:.14em; text-transform:uppercase; }
.scr--contact dt{ color:#8d8880; margin-top:.4em; }
.scr--contact dd{ margin:.08em 0 0; color:#e9e6e0; font-weight:600; font-size:.046em; }

.scr--static{ background:#0b0b0b; }
.noise{
  position:absolute; inset:-25%;
  background-image:var(--tex-static);
  background-size:180px 180px;
  opacity:.85;
  animation:noiseJit .13s steps(1,end) infinite; animation-play-state:paused;
}
.tube.lit .noise{ animation-play-state:running; will-change:transform; }
@keyframes noiseJit{
  0%{transform:translate3d(0,0,0)} 20%{transform:translate3d(-18px,12px,0)}
  40%{transform:translate3d(14px,-16px,0)} 60%{transform:translate3d(-9px,-8px,0)}
  80%{transform:translate3d(17px,7px,0)} 100%{transform:translate3d(0,0,0)}
}
.hbar{
  position:absolute; inset:0; pointer-events:none;
  /* stepped for the same reason as .tube__roll above — a hold bar that
     ratchets is closer to the real thing than one that glides */
  animation:hbar 4.2s steps(101, end) infinite; animation-play-state:paused;
}
.hbar::after{
  content:""; position:absolute; left:-10%; right:-10%; top:0; height:5%;
  background:rgba(255,255,255,.14);
}
.tube.lit .hbar{ animation-play-state:running; }
@keyframes hbar{ from{ transform:translate3d(0,106%,0) } to{ transform:translate3d(0,-16%,0) } }

.scr--mark{ display:grid; place-items:center; background:#080808; }
.scr--mark svg{ width:74%; height:74%; }

/* =========================================================
   MOCK WEBSITE inside the tube
   Sizes are in em so a 190px tube and a 90px tube both read.
   ========================================================= */
.site{ width:100%; background:#0c0c0c; color:#e7e4de; font-size:.036em; filter:grayscale(1) contrast(1.18); }
.site--red{ filter:grayscale(.35) contrast(1.2); }
/* a real capture of a hosted build, scrolled like the mock site above */
.tube__strip{ display:block; width:100%; height:auto; filter:grayscale(1) contrast(1.18); }
/* a pale build (kinari paper) blows out against the wall of black glass */
.tube__strip--dim{ filter:grayscale(1) contrast(1.18) brightness(.6); }
/* titles may carry a deliberate break, e.g. MATCHA / ON YU */
.card__t{ white-space:pre-line; }
.site__nav{
  display:flex; align-items:center; justify-content:space-between;
  padding:1.6em 2em; border-bottom:1px solid rgba(255,255,255,.14);
}
.site__logo{ font-family:'Anton',sans-serif; font-size:1.5em; }
.site__menu{ display:flex; gap:1.4em; color:#9c968e; letter-spacing:.1em; }
.site__hero{ padding:3.4em 2em 3em; border-bottom:1px solid rgba(255,255,255,.1); }
.site__h1{
  font-family:'Anton',sans-serif; font-size:4.4em; line-height:.94;
  text-transform:uppercase; color:#f4f1eb;
}
.site__h1 span{ color:var(--red); }
.site__sub{ margin-top:1.2em; color:#a19c94; letter-spacing:.09em; line-height:1.6; }
.site__band{ position:relative; height:13em; overflow:hidden; background:#141414; }
.site__band svg{ position:absolute; inset:0; width:100%; height:100%; opacity:.9; }
.site__band--tall{ height:18em; }
/* the panel a tube shows at rest: as tall as the glass, so the label reads
   against picture rather than against copy */
.site__band--hero{ height:28em; background:#101010; }
.site__band--hero svg{ width:66%; height:76%; left:34%; top:12%; }
.site__rows{ padding:2em; display:flex; flex-direction:column; gap:1.2em; }
.site__row{
  display:flex; justify-content:space-between; gap:1.4em;
  padding-bottom:1em; border-bottom:1px solid rgba(255,255,255,.09);
  color:#b8b3ab; letter-spacing:.08em;
}
.site__row b{ color:#efece6; font-weight:600; }
.site__grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:1em; padding:2em; }
.site__cell{ aspect-ratio:4/3; background:#171717; position:relative; overflow:hidden; }
.site__cell::after{
  content:""; position:absolute; inset:0;
  background-image:var(--tex-static); background-size:90px 90px; opacity:.5;
}
.site__cell.red{ background:#3a0d0f; }
.site__quote{
  padding:3em 2em; font-family:'Anton',sans-serif; font-size:2.6em;
  line-height:1.1; text-transform:uppercase; color:#efece6;
  border-top:1px solid rgba(255,255,255,.12); border-bottom:1px solid rgba(255,255,255,.12);
}
.site__foot{ padding:2em; display:flex; justify-content:space-between; color:#8b867e; letter-spacing:.12em; }
.site__mark{ color:var(--red); }

/* =========================================================
   SCROLL CUE / HUD / CURSOR
   ========================================================= */
.scroll-cue{
  position:fixed; left:50%; bottom:34px; transform:translateX(-50%);
  z-index:55; display:flex; flex-direction:column; align-items:center; gap:9px;
  transition:opacity .5s ease, transform .5s ease; pointer-events:none; opacity:0;
}
.scroll-cue.show{ opacity:1; }
.scroll-cue.hide{ opacity:0; transform:translateX(-50%) translateY(14px); }
.scroll-cue__label{ font-size:9px; letter-spacing:.42em; font-weight:600; color:#4a4844; text-indent:.42em; }
.scroll-cue__rail{ width:1px; height:52px; background:rgba(30,28,26,.28); position:relative; overflow:hidden; }
.scroll-cue__rail i{
  position:absolute; left:0; width:1px; height:18px; background:var(--red);
  animation:cueRun 1.7s cubic-bezier(.5,0,.5,1) infinite;
}
@keyframes cueRun{ 0%{top:-20px} 100%{top:54px} }
.scroll-cue__sig{ color:var(--red); font-size:9px; letter-spacing:-.05em; animation:arrowBlink 1.1s steps(1,end) infinite; }

.hud{
  position:fixed; left:0; right:0; top:0; z-index:56;
  display:flex; justify-content:space-between;
  padding:16px 22px; pointer-events:none;
  font-size:9px; letter-spacing:.3em; font-weight:600; color:#4a4844;
  text-transform:uppercase;
  transition:opacity .45s ease;
}
.hud .rec{
  display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--red);
  margin-right:7px; vertical-align:1px; animation:arrowBlink 1.3s steps(1,end) infinite;
}

.cursor{
  position:fixed; left:0; top:0; z-index:80; pointer-events:none;
  width:26px; height:26px; margin:-13px 0 0 -13px;
  transition:transform .18s cubic-bezier(.2,.8,.3,1);
  will-change:transform;
}
.cursor::before, .cursor::after{
  content:""; position:absolute; left:50%; top:50%; background:var(--red);
  transform:translate(-50%,-50%);
}
.cursor::before{ width:13px; height:1px; }
.cursor::after{ width:1px; height:13px; }
.cursor span{
  position:absolute; inset:0; border:1px solid rgba(234,31,39,.55); border-radius:50%;
  transform:scale(.42); transition:transform .25s cubic-bezier(.2,.8,.3,1), border-color .25s ease;
}
body.cursor-hot .cursor span{ transform:scale(1); border-color:rgba(234,31,39,.95); }
body.cursor-hot .cursor{ transform:rotate(45deg); }

/* =========================================================
   REDUCED MOTION / TOUCH
   ========================================================= */
@media (prefers-reduced-motion: reduce){
  .grain{ animation:none; }
  .tube__roll, .noise, .hbar, .scroll-cue__rail i{ animation:none !important; }
  .cursor{ display:none; }
  body{ cursor:auto; }
}
@media (hover:none){
  body{ cursor:auto; }
  .cursor{ display:none; }
}
