/* ═══════════════════════════════════════════════════════════════════
   WHITE·WARP WEB STUDIO — 3D SHOWROOM (preview build)
   Forza-style rotating turntable for the work-showcase (#chWork).
   Owns the sticky .work-pin; the legacy ring is hidden but kept in DOM.
   ═══════════════════════════════════════════════════════════════════ */

/* Hide the old ring-of-cards in showroom mode (kept in DOM for shared JS) */
body.showroom #stage,
body.showroom .orbit-line,
body.showroom .ring { display: none !important; }

/* The showroom fills the pinned WORK viewport */
.showroom-stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
  touch-action: pan-y;               /* horizontal gestures are ours; vertical scroll stays native */
  -webkit-user-select: none;
  user-select: none;
}
.showroom-stage canvas#showroomCanvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  cursor: grab;
}
body.showroom.sr-dragging .showroom-stage canvas#showroomCanvas { cursor: grabbing; }

/* ─── HTML overlay: name / tag / meta / visit ─────────────────────── */
.showroom-ui {
  position: absolute;
  left: clamp(20px, 5vw, 64px);
  bottom: clamp(76px, 12vh, 120px);
  z-index: 3;
  max-width: min(46ch, 82vw);
  pointer-events: none;             /* let drags pass through to the canvas… */
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .5s ease, transform .5s ease;
}
body.showroom.sr-ready .showroom-ui { opacity: 1; transform: none; }
.showroom-ui .sr-visit { pointer-events: auto; }  /* …except the link */

.sr-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  letter-spacing: var(--trk-lbl);            /* was .22em */
  color: rgba(255, 255, 255, .62);
  margin-bottom: 14px;
}
.sr-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--sr-accent, #1877F2);
  box-shadow: 0 0 12px var(--sr-accent, #1877F2);
}
/* THE SHOWROOM JOINS THE SHELL'S TYPE SYSTEM (2026-08-15). These were
   Space Grotesk / Inter while the shell around them was Helvetica —
   two faces on one screen, split down the middle of the page. Master's
   Helvetica Neue directive is site-wide, so the project title and copy
   move onto --font-shell and the shared tracking law. Everything in the
   Geist Mono register below (VISIT LIVE, PREV/NEXT, 01/21, the meta
   row) is untouched — that voice stays mono, per the directive. */
.sr-name {
  font-family: var(--font-shell);
  font-weight: 700;
  font-size: clamp(2rem, 5.4vw, 3.9rem);   /* 32-62px → d3 band */
  line-height: var(--lh-d3);
  letter-spacing: var(--trk-d3);
  color: #fff;
  margin: 0 0 .35em;
  text-wrap: balance;
}
.sr-tag {
  font-family: var(--font-shell);
  font-size: clamp(.95rem, 1.6vw, 1.15rem);
  line-height: 1.4;
  letter-spacing: var(--trk-lead);
  color: rgba(255, 255, 255, .78);
  margin: 0 0 1.1em;
}
.sr-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  font-size: 11.5px;
  letter-spacing: var(--trk-lbl-lg);         /* was .12em */
  color: rgba(255, 255, 255, .5);
  margin-bottom: 1.5em;
}
.sr-meta span { position: relative; }
.sr-meta span + span::before {
  content: "";
  position: absolute; left: -10px; top: 50%;
  width: 3px; height: 3px; border-radius: 50%;
  background: currentColor; transform: translateY(-50%);
}
.sr-actions { display: flex; }
.sr-visit {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  letter-spacing: var(--trk-lbl-lg);         /* was .12em */
  color: #fff;
  text-decoration: none;
  padding: 12px 22px;
  border: 1px solid rgba(255, 255, 255, .22);
  border-radius: 999px;
  background: rgba(255, 255, 255, .04);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition: border-color .3s ease, background .3s ease, transform .3s ease;
}
.sr-visit:hover {
  border-color: var(--sr-accent, #1877F2);
  background: color-mix(in srgb, var(--sr-accent, #1877F2) 16%, transparent);
  transform: translateY(-2px);
}
.sr-visit .arr { transition: transform .3s ease; }
.sr-visit:hover .arr { transform: translate(2px, -2px); }
.sr-visit[data-dead="1"] { opacity: .45; pointer-events: none; }

/* ─── Prev / Next arrows ──────────────────────────────────────────────────────────────
   REDESIGNED 2026-08-13. Scroll no longer browses the showroom (the wheel engine is gone
   from showroom.js), so these two buttons — plus swipe and the SEE ALL grid — are now the
   ONLY way anyone discovers project 2 through 21. That makes them load-bearing UI, and two
   quiet 52px glass chevrons parked at the screen edge are what nobody presses.

   WHAT CHANGED, AND WHY EACH PART EARNS ITS PLACE:
     · THE BUTTON NAMES ITS DESTINATION. Each arrow carries a PREV/NEXT label and the
       actual name of the project it will bring (written every cycle by showroom.js). The
       affordance is not "there is a direction", it is "there is EVERGREEN PATHOLOGY over
       here" — which is the only honest way to say "there is more" to someone who cannot
       see the other twenty.
     · NOT A CIRCLE, NOT GLASS. The generic answer to "carousel arrows" is a translucent
       blurred disc; that is what was here. These are flat-edged instrument plates with a
       hairline rule and Geist Mono labels, the same register as the LED rail and the
       01/21 counter, so they read as part of this machine rather than a stock widget.
     · ONE-TIME NUDGE. `sr-nudge` runs three slow beats on entry and is killed forever by
       `body.sr-interacted` (already set by requestCycle). It draws the eye once; it does
       not become another pulsing thing on the page.
     · Accent is `--sr-accent` (the live project's colour, WW blue by default) — the same
       token the rest of this surface already uses. No new palette, no new face. */
.sr-arrow {
  position: absolute;
  z-index: 4;
  display: flex; align-items: center; gap: 12px;
  padding: 0;
  height: 54px;
  color: rgba(255, 255, 255, .9);
  background: rgba(8, 11, 17, .62);
  border: 1px solid rgba(255, 255, 255, .18);
  border-radius: 6px;
  cursor: pointer;
  text-align: left;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: background .22s ease, border-color .22s ease, transform .22s ease,
              box-shadow .22s ease, opacity .5s ease;
  opacity: 0;
}
body.showroom.sr-ready .sr-arrow { opacity: 1; }
/* the chevron keeps its own square cell so the plate can grow without moving it */
.sr-arw {
  flex: 0 0 auto;
  width: 52px;
  align-self: stretch;
  display: grid; place-items: center;
  font-size: 26px; line-height: 1;
  color: #fff;
  transition: transform .22s cubic-bezier(.23,1,.32,1), color .22s ease;
}
.sr-peek {
  display: none;                       /* desktop turns this on; mobile stays icon-only */
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  padding-right: 16px;
}
.sr-peek-lbl {
  font-size: 9px; letter-spacing: var(--trk-lbl-sm);   /* was .2em */
  color: var(--sr-accent, #1877F2);
  line-height: 1;
}
.sr-peek-name {
  /* a PROJECT NAME inside a fitted plate — the word step, not a label
     step. It was already tighter than any size band would give it, so it
     goes DOWN to word rather than up. was .07em */
  font-size: 11px; letter-spacing: var(--trk-lbl-word);
  color: rgba(255, 255, 255, .74);
  line-height: 1.1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sr-sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.sr-arrow:hover, .sr-arrow:focus-visible {
  background: rgba(8, 11, 17, .82);
  border-color: var(--sr-accent, #1877F2);
  box-shadow: 0 0 26px -6px color-mix(in srgb, var(--sr-accent, #1877F2) 90%, transparent);
  outline: none;
}
.sr-arrow:hover .sr-peek-name, .sr-arrow:focus-visible .sr-peek-name { color: #fff; }
.sr-arrow:hover .sr-arw { color: var(--sr-accent, #1877F2); }
.sr-prev:hover .sr-arw, .sr-prev:focus-visible .sr-arw { transform: translateX(-3px); }
.sr-next:hover .sr-arw, .sr-next:focus-visible .sr-arw { transform: translateX(3px); }
.sr-arrow:active { transform: scale(.975); }

/* MOBILE (default): icon-only, split to the two edges, thumb-height rather than dead
   centre — 56% of the stage clears the title block (bottom: 92px) at 375×812. */
.sr-prev { left: clamp(14px, 3vw, 40px);  top: 56%; transform: translateY(-50%); }
.sr-next { right: clamp(14px, 3vw, 40px); top: 56%; transform: translateY(-50%); }
.sr-prev:active, .sr-next:active { transform: translateY(-50%) scale(.94); }

/* DESKTOP: both plates stacked on the RIGHT, destination visible without hovering. */
@media (min-width: 768px) {
  .sr-arrow { transform: none; width: clamp(196px, 18vw, 248px); }
  .sr-prev, .sr-next { left: auto; right: clamp(22px, 3vw, 46px); }
  .sr-prev { top: calc(50% - 62px); }        /* ‹ on top */
  .sr-next { top: calc(50% + 8px); }         /* › below, 16px gap */
  .sr-prev:active, .sr-next:active { transform: scale(.975); }
  .sr-peek { display: flex; }
}

/* THE NUDGE — three beats on entry, then never again. Killed by the first interaction of
   any kind (requestCycle adds .sr-interacted), so it can never nag. */
@keyframes sr-nudge-next {
  0%, 62%, 100% { transform: translateX(0); }
  72%           { transform: translateX(6px); }
  84%           { transform: translateX(0); }
}
@keyframes sr-nudge-prev {
  0%, 62%, 100% { transform: translateX(0); }
  72%           { transform: translateX(-6px); }
  84%           { transform: translateX(0); }
}
body.showroom.sr-ready:not(.sr-interacted) .sr-next .sr-arw { animation: sr-nudge-next 2.6s ease-in-out 3; }
body.showroom.sr-ready:not(.sr-interacted) .sr-prev .sr-arw { animation: sr-nudge-prev 2.6s ease-in-out 3; }

/* ─── Index rail + hint ───────────────────────────────────────────── */
.sr-rail {
  position: absolute;
  right: clamp(20px, 5vw, 64px);
  bottom: clamp(76px, 12vh, 120px);
  z-index: 3;
  font-size: 13px;
  letter-spacing: var(--trk-lbl-lg);         /* the 01 / 21 counter; was .14em */
  color: rgba(255, 255, 255, .55);
  opacity: 0;
  transition: opacity .5s ease;
}
body.showroom.sr-ready .sr-rail { opacity: 1; }
.sr-rail .sr-idx { color: #fff; font-size: 20px; }
.sr-rail .sr-sep { margin: 0 6px; opacity: .5; }
.sr-hint {
  position: absolute;
  /* 26px → 78px. The fixed .chrome-bottom band is 60px tall and sits at the foot of the
     viewport, which is exactly where the stage's foot is while the WORK pin is up — so at
     26px the hint was printing straight over the dot rail inside that band. 78px clears it
     and still sits below the title block (bottom: 92px on mobile). */
  left: 50%; bottom: 78px;
  transform: translateX(-50%);
  z-index: 3;
  font-size: 10.5px;
  letter-spacing: var(--trk-lbl);            /* was .28em */
  color: rgba(255, 255, 255, .4);
  opacity: 0;
  transition: opacity .6s ease;
  pointer-events: none;
}
body.showroom.sr-ready .sr-hint { opacity: 1; }
body.showroom.sr-interacted .sr-hint { opacity: 0; }

/* ─── Static fallback (no WebGL) ──────────────────────────────────── */
.showroom-fallback[hidden] { display: none !important; }  /* CSS display must not override the hidden attribute */
.showroom-fallback {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 26px;
  padding: 8vh 6vw;
  background:
    radial-gradient(120% 80% at 50% 30%, rgba(24, 119, 242, .10), transparent 60%),
    linear-gradient(180deg, #070A0F 0%, #04060A 100%);
}
.showroom-fallback img {
  max-width: min(760px, 88vw);
  max-height: 58vh;
  width: auto;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, .1);
  box-shadow: 0 30px 90px rgba(0, 0, 0, .6);
  object-fit: cover;
}
.showroom-fallback .fb-name {
  font-family: var(--font-shell);
  font-weight: 700;
  font-size: clamp(1.6rem, 4vw, 2.6rem);
  letter-spacing: var(--trk-d4);
  color: #fff;
  text-align: center;
}
.showroom-fallback .fb-tag {
  font-size: 12px;
  letter-spacing: var(--trk-lbl-lg);         /* was .16em */
  color: rgba(255, 255, 255, .55);
  text-align: center;
  margin-top: 8px;
}

/* ─── Mobile ──────────────────────────────────────────────────────── */
@media (max-width: 640px) {
  .showroom-ui { left: 18px; right: 18px; bottom: 92px; max-width: none; }
  .sr-name { font-size: clamp(1.8rem, 8vw, 2.4rem); }
  .sr-rail { bottom: 92px; }
  .sr-arrow { height: 48px; }
  .sr-arw { width: 48px; font-size: 24px; }   /* 48px square = above the 44pt touch floor */
  /* HINT IS OFF ON TOUCH. Measured at 375×812: the only band that clears both the title
     block (bottom 92px) and the 60px bottom chrome is y 735–748, and the shell's dot rail
     already occupies y 727–748 — 3,544 px² of text-on-text, whichever way the hint is
     nudged. There is no free row. On touch the legend is also half-wrong ("← →" needs a
     keyboard), and the two arrow plates plus the SEE ALL pill now carry the affordance on
     their own, so the honest fix is to drop it rather than to keep sliding it around.
     (The dot rail itself lays out 571px wide inside a 375px viewport — a pre-existing
     overflow in the shell, left alone here.) */
  .sr-hint { display: none; }
}

/* ═══ THE ASSEMBLY OWNS THE OVERLAY WHILE THE ROOM IS BUILDING ═══════════════════════
   showroom.js adds `.sr-assembling` to `.work-pin` for exactly as long as the scroll-driven
   build is running (0 <= a < 1) and takes it off the frame it completes.

   WHY THE CLASS EXISTS AT ALL. styles.css ships the earlier single-shot entrance:
   `.work-pin.is-actcut { transition: transform 1150ms }` plus one resting offset per
   element, all firing together with the circle. The assembly replaces that with a per-
   element travel scrubbed by scroll position — and a scrub through a 1150ms transition is
   not a scrub, it is 1.15 seconds of lag behind the wheel. So the transition is switched
   off for precisely the elements this drives, and only while it is driving them. The moment
   the build finishes the class comes off, the transitions come back, and styles.css's own
   `.is-entered` values are already the rest pose, so nothing moves at the hand-off.

   The `transform` declarations below are not resets — they are showroom.css's OWN base
   transforms (the centring translates from lines above) re-asserted so that styles.css's
   entrance offsets do not leave a permanent 30px shove on a plate this file is now moving
   itself. The actual travel is carried by the independent `translate` property, which
   composes on top of whatever `transform` the cascade resolves — so :hover and :active keep
   working mid-build instead of being flattened. .showroom-ui is the exception and is driven
   through `transform` in JS; app.js measures that property to merge the two full stops.

   Specificity is a tie with styles.css (three classes) and showroom.css loads second. */
.work-pin.sr-assembling .showroom-ui,
.work-pin.sr-assembling .sr-rail,
.work-pin.sr-assembling .sr-hint,
.work-pin.sr-assembling .sr-prev,
.work-pin.sr-assembling .sr-next,
.work-pin.sr-assembling .sr-allbtn,
.work-pin.sr-assembling canvas#showroomCanvas { transition: none !important; }

.work-pin.sr-assembling .sr-rail { transform: none; }
.work-pin.sr-assembling .sr-hint { transform: translateX(-50%); }
.work-pin.sr-assembling .sr-prev,
.work-pin.sr-assembling .sr-next { transform: translateY(-50%); }
/* The 3D scene assembling IS the canvas's entrance. A viewport scale on top of it would be
   the "everything arrives at once" move this replaces — and scaling the canvas DOWN is also
   how you uncover the pin behind its own edges. */
.work-pin.sr-assembling canvas#showroomCanvas { transform: none; }

@media (min-width: 768px) {
  /* the plates lose their centring translate at this breakpoint (see .sr-arrow above) */
  .work-pin.sr-assembling .sr-prev,
  .work-pin.sr-assembling .sr-next { transform: none; }
}

/* Reduced motion — the JS also stops the auto-spin; keep the hint honest.
   The arrows still work and still NAME their destination; only the motion goes. The nudge
   is the one thing that must be silenced outright — an unrequested attention-grab is
   precisely what this setting is asking us not to do. */
@media (prefers-reduced-motion: reduce) {
  .showroom-ui, .sr-arrow, .sr-rail, .sr-arw { transition: none; }
  body.showroom.sr-ready:not(.sr-interacted) .sr-next .sr-arw,
  body.showroom.sr-ready:not(.sr-interacted) .sr-prev .sr-arw { animation: none; }
  .sr-prev:hover .sr-arw, .sr-next:hover .sr-arw,
  .sr-prev:focus-visible .sr-arw, .sr-next:focus-visible .sr-arw { transform: none; }
}

/* ═══ PER-PROJECT DETAIL VIEW — click a card → step into the project ═══
   ONE camera, TWO depths. Both are separate perspective containers carrying an
   IDENTICAL perspective + perspective-origin + tilt, so they compose as a single 3D
   space. Because they share one camera, a shift of perspective-origin parallaxes each
   depth by an amount proportional to its z — the vacuum is physically derived, not faked.

     WALL    z −880   ONE curved PROJECTION WALL, full bleed. Faceted into N vertical
                      segments by JS (buildWall) whose far end progressively wraps AROUND
                      the viewer — flat at the near/left edge, CONCAVE by the right end,
                      i.e. the inside of a curved cinema screen (item 13; it used to bend
                      the other way and read as a convex barrel).
                      Its SURFACE carries the registration marks (+ crosshairs, small
                      ticks, dashed rules) AND the project's full-page capture, played
                      CLEAN and dimmed (item 15 deleted the LED dot matrix). The marks are
                      baked into the wall: they are static relative to it and move only
                      when the wall moves.
     THEATER z +140   the 16:9 hero — the crisp LIVE site, ~1020px in front of the wall.
     INFO             bare floating text, right. No card, no panel, no edge strip.

   REMOVED 2026-08-11 (Master): the old VOID (z −1700) and FORE (z +620) layers. Nothing
   floats IN FRONT of the display any more — the plus-marks live on the wall, behind. The
   flat vignette that used to be its own .sr-d-veil div is now .sr-detail::after, so the
   scene really is two layers and not two-plus-a-div. */
.sr-detail {
  position: fixed; inset: 0; z-index: 900; overflow: hidden;
  background: #03050a;
  opacity: 0; pointer-events: none;
  transition: opacity .5s ease;
  /* STATIC camera. These were pointer-driven; Master killed that (item 6) — nothing writes
     them at runtime now, and solveWall() reads them as the one camera it must satisfy. */
  --sr-po-x: 54%; --sr-po-y: 50%;
  /* ITEM 23 — A 45° CAMERA. Master, confirmed: he wants to look at the wall and the card
     OBLIQUELY, not head-on, with the composition weighted left and open space on the right
     for the floating text. 9° was very nearly face-on (the measured angle between the view
     axis and the wall's near-end normal was 9°); this is 45°.

     PERSPECTIVE GOES UP WITH THE ANGLE, and that is not a stylistic choice — it is what
     makes 45° survivable. A plane yawed by θ spreads over ±(W/2)·sin θ in depth: at 9° that
     is ±0.078W, at 45° it is ±0.354W, i.e. 4.5× the depth range for the same wall. With a
     1400px perspective the near end of a full-bleed wall would have landed IN FRONT of the
     theater. Raising the perspective to 2200 flattens the convergence enough that the wall
     needs less width to cover the frame AND sits in a shallower depth band — the 45° read
     comes from the ROTATION, which is untouched, not from the amount of foreshortening.
     The wall also moves back (−640 → −1100) to buy clearance for the same reason. */
  --sr-persp: 2200px;
  --sr-tilt: rotateY(55deg) rotateX(3deg);
  --sr-wall-z: -1340px;
  --sr-front-z: 140px;

  /* ── THE LED PANEL (2026-08-13, Master) — see .sr-d-gl for the full argument.
     STILL THE ONE PLACE THE PITCH IS DECIDED, even though the lattice is now drawn by the
     wall's fragment shader: showroom.js reads these three off .sr-detail's computed style
     in glBuild() and hands them to the shader, so the mobile override below still works
     exactly as it always did.
     --ledp  one lamp pitch, in SCREEN px — at the wall's near edge. It used to be constant
             across the whole frame by construction because the lattice was drawn flat in
             front; the shader lattice now lays the lamps out ON the surface, so they
             foreshorten with it and the pitch is held near this value by octave-doubling
             rather than by being screen-space. See the lattice section of GL_FRAG.
     --modp  one cabinet module. MUST be an integer multiple of --ledp or the module
             seam lands mid-lamp instead of on a gutter.
     --ledg  the gutter between two lamps. ⚠️ THE OLD RULE HERE — "it MUST scale with
             --ledp, so it is always --ledp/8" — IS RETIRED (2026-08-17). It was written to
             hold the geometric fill constant across a pitch change, but its consequence is
             that coarsening the pitch THICKENS the line, which is the exact complaint
             Master has now raised twice. The gutter is instead pinned at ONE DEVICE PIXEL
             (1px at dpr 1) and left there; constant fill across a pitch or breakpoint
             change is now the job of --ledgo below, which changes the lattice's weight
             without touching its geometry. The corner-kill radii and the half-gutter
             offset are still ratios of --ledp — those genuinely are shape, not weight.
             (The old note here read "1px of 8px = 87% fill". That was wrong arithmetic and
             is corrected below: 1 of 8 is 76.6% by area. 87.9% is 0.5 of 8.)
     --ledgo how black the black is. See the bottom of this block — it is the mildness dial.
     (--leddur is GONE. It timed a CSS keyframe that translated the old .sr-d-led layer one
     module to the left; the 8 px/s ratio it existed to protect is now a literal in
     glRender(). Nothing reads a duration any more.)

     PITCH 4px → 8px (Master, 2026-08-13): "the wall is like too much pixelated. I think
     you can make the squares bigger, like the one from alche.studio… but still you can
     make it look like it's a display." Rendered at 4 / 8 / 12 / 16 and judged on the real
     screenshots: at 4 the lamps are below the eye's resolution at this distance and the
     surface reads as dark noise, not a screen; at 12 and 16 the lamps read beautifully but
     the picture inside them stops reading (the capture becomes blocks). 8 is the coarsest
     pitch that is unmistakably a display AND still carries the site it is showing.
     HONEST NOTE — Master also expected this to fix the video stutter ("so that we don't
     have much processing happening"). It does not. Measured on the same machine, same
     clip: swapping the pitch to 16px moved the wall video's effective playback rate from
     0.287× to 0.156× — i.e. no improvement, inside the noise. The LED layer is one
     rasterise-once composited plane; it is not what starves the decoder. The real cause was
     the 22 facet canvases, and it is fixed by the WebGL rebuild (see .sr-d-gl). This change
     is an aesthetic one and is worth making on its own terms.

     GUTTER 2px → 1px (Master, 2026-08-13): "the black lines which are running are too
     thick. It should be very mild." He is objecting to the right thing, and the number says
     so. A 2px gutter on an 8px pitch covers 2/8 of the cell in EACH axis, so the black
     lattice owns 1 − (6/8)² = 43.8% of the wall's area — nearly half the surface is grid,
     not picture. That does not read as a screen showing a site, it reads as a cage drawn
     over one. The PITCH IS UNTOUCHED at 8px: he asked for the bigger squares and likes
     them; only the black between them changes.

     Rendered at 1 / 0.75 / 0.5 on a frozen frame of the same clip (so only the variable
     moved) and judged on the real screenshots, near AND far, at 4× zoom:

       gutter   fill/axis   area fill   black area   the read
       2px       75.0%       56.3%       43.8%       a black cage over a grey picture
       1px       87.5%       76.6%       23.4%       mild screen-door; lamps still separate
       0.75px    90.6%       82.1%       17.9%       milder, but the line is now grey not black
       0.5px     93.75%      87.9%       12.1%       a flat image with faint scratches

     1px WINS, and not only on taste. At dpr 1 the gutter is drawn --ledg × dpr device px
     wide, so 1px is exactly ONE device pixel — the thinnest line that is a real gutter
     rather than an antialiasing artefact. Below that the shader's `bar()` can only render
     partial coverage, i.e. a GREY line whose contrast then changes with the display's dpr:
     0.75px is ~75% covered on Master's 1× screen but a solid 1.5 device px on a 2× one, so
     the wall would not look the same on two machines. 0.5px additionally loses the display
     read outright at the near end — the exact failure the fill-ratio note above warns about.
     Far end verified separately: the per-axis LOD doubles the wall-space pitch and scales
     the gutter WITH it (Gx = Px · --ledg/--ledp in GL_FRAG), so a thinner gutter survives
     the octave cross-fade instead of vanishing — checked on a 4× crop of the receding end.

     ── PASS 3, AND THE ONE THAT ACTUALLY ANSWERED IT (Master, 2026-08-17) ────────────────
     "the lines in the LED are very thick. It should be very mild, so that the video which
     is playing on the back is very crisp and in 4K." SECOND time he has said this — the
     2px → 1px pass above did not fix it, and the table above explains why it could not:
     it measures GEOMETRIC BLACK AREA, which cannot see the two other black terms in the
     shader at all. Modelled term for term against GL_FRAG (gutter × 2 axes, corner kill,
     cabinet seam, composited the way `over()` actually composites them), the fraction of
     the FILM'S LIGHT the lattice eats — call it ink — decomposes like this at 8/1/.72:

         gutter   17.2%          corner kill   11.8%          cabinet seam   1.0%
         total ink 24.3%   (geometric black area, for reference, 23.4%)

     THE CORNER KILL IS 49% OF THE LATTICE'S WEIGHT AND NOBODY HAD COUNTED IT. Worse, it
     is a RATIO of the cell (radii 0.3875→0.5875 in cell-normalised units), so it is scale
     invariant: it stays at 11.8% at ANY pitch. That is the whole reason two passes of
     geometry moved the number so little, and it is why "make the squares bigger" alone
     cannot work — measured, same model:

       candidate            geom black   ink    what moved
       P8  G1   ledgo .72      23.4%    24.3%   SHIPPED BEFORE THIS PASS
       P12 G1   ledgo .72      16.0%    20.1%   pitch alone: −7.4 geom but only −4.2 ink
       P8  G1   ledgo .42      23.4%    15.7%   contrast alone: geom UNCHANGED, ink −8.6
       P12 G1   ledgo .45      16.0%    13.6%   both  ← SHIPPED
       P16 G1   ledgo .45      12.1%    12.1%   both, coarser
       P12 G1   ledgo .34      16.0%    10.6%   both, milder still

     Pitch alone (row 2) buys 4.2 points of ink for a whole octave of coarseness. Contrast
     alone (row 3) buys 8.6 for free — it does not touch the geometry, so the gutter stays
     exactly ONE device pixel at dpr 1 and the dpr-dependence the 1px note above warns
     about never arises. Contrast is strictly the better lever and it had never been tried.

     SHIPPED 12 / 1 / .45. Judged on real screenshots of the same frozen video frame, near
     and far, at 4×: .34 is milder on paper but the wall stops being a display and the
     seams go with it; 16px starts blocking the picture the way the 2026-08-13 note warned;
     8px at .42 is mild but still has 8px of lines crossing every inch of the film. 12 with
     the ink at .45 is the one where the picture reads first and the display reads second,
     which is the order Master asked for. The 1px gutter is DELIBERATELY UNCHANGED — the
     argument for it above is still correct, and this pass supersedes none of it. */
  --ledp: 12px;
  --ledg: 1px;
  --modp: 96px;
  /* --ledgo — how BLACK the black between the lamps is. Unitless 0…1, read by glBuild()
     and handed to the shader as uLedGA, which scales all three dark terms together (the
     gutter, the corner kill at ×0.611, the cabinet seam at ×0.694 — the ratios they always
     had). .72 is the old hard-coded value and reproduces the previous render exactly.
     This is the mildness dial: change THIS, not the gutter, when the lattice is too heavy. */
  --ledgo: .45;
}
.sr-detail[hidden] { display: none; }
.sr-detail.on { opacity: 1; pointer-events: auto; }

/* coplanar perspective containers — identical camera on both. Only ONE is left: the
   THEATER. The wall stopped being a CSS 3D subtree on 2026-08-13 and is now the WebGL
   canvas below, which reproduces this exact camera (perspective + perspective-origin +
   tilt) in its projection matrix — see the block comment above buildWall() in showroom.js
   for the derivation. The two therefore still compose as one 3D space. */
.sr-d-space {
  position: absolute; inset: 0; pointer-events: none;
  perspective: var(--sr-persp);
  perspective-origin: var(--sr-po-x) var(--sr-po-y);
}
.sr-d-space.front { z-index: 3; }

/* ══ .sr-d-gl — THE WALL ══════════════════════════════════════════════════════════════
   ONE full-bleed WebGL canvas at z-index 1, standing exactly where .sr-d-space.wall used to
   stand. Everything the old wall subtree carried lives in its fragment shader now: the
   panel substrate, the capture/recording, the glass falloff, and the lamp lattice with its
   cabinet seams and registration marks.

   WHY THIS REPLACED 22 CANVASES IN A preserve-3d SUBTREE — measured, real Chrome on
   Master's GPU, one fresh browser per condition:

       22 facets (shipped)                              7.3 fps decode, 21.5% dropped
       12 facets                                        7.6 fps       , 22.5%
       11 of 22 hidden (same count, HALF the area)     10.6 fps       , 17.9%
       all canvases hidden (zero area)                 19.5 fps       ,  6.2%

   Facet COUNT is inside the noise. Composited screen AREA is the whole lever, and a wall
   that is full bleed by requirement cannot have less of it — so the wall had to stop being
   a composited DOM layer. It is now one draw call into one canvas; the compositor sees a
   single unchanging layer and the decoded video frame goes to a GPU texture instead of
   through 22 drawImage() blits.

   The fade stays on the canvas rather than on a parent: opacity on a plain canvas is one
   compositor property on one layer (the reason it could NOT live on the old .sr-d-wall was
   that an opacity < 1 silently kills transform-style: preserve-3d, which no longer applies
   to anything here).

   The LAMP PARAMETERS stay in the cascade above (--ledp / --ledg / --modp, plus the mobile
   override) — showroom.js reads them off .sr-detail's computed style and hands them to the
   shader, so this file is still the one place the pitch is decided. --leddur is gone: the
   drift is 8 px/s in JS, which is the ratio that block always asked to be preserved. */
.sr-d-gl {
  position: absolute; inset: 0; z-index: 1;
  display: block; width: 100%; height: 100%;
  pointer-events: none;
  opacity: 0; transition: opacity .9s ease .1s;
}
.sr-detail.on .sr-d-gl { opacity: 1; }

/* The single hidden <video> the wall's THREE.VideoTexture is bound to. It must stay in the
   DOM and rendered (not display:none) or Chromium throttles the decoder — so it is 2px in
   the corner at ~zero opacity rather than hidden. */
.sr-d-vidsrc {
  position: absolute; left: 0; bottom: 0; width: 2px; height: 2px;
  opacity: 0.01; pointer-events: none; z-index: 0;
}


/* ── THEATER — the 16:9 hero, floating ~1020px in front of the wall ── */
.sr-d-theater {
  position: absolute; top: 50%; left: 42%;
  width: min(46vw, 790px); aspect-ratio: 16 / 9;
  transform: translate(-50%, -50%) translateZ(var(--sr-front-z)) var(--sr-tilt) scale(.94);
  transform-origin: 50% 50%;
  overflow: hidden;
  background: #070a10;
  box-shadow:
    0 4px 10px rgba(0,0,0,.5),
    0 40px 90px rgba(0,0,0,.72),
    0 90px 190px rgba(0,0,0,.6),
    0 0 0 1px rgba(255,255,255,.09);
  opacity: 0;
  transition: opacity .55s ease .18s, transform .8s cubic-bezier(.16,1,.3,1) .12s;
}
.sr-detail.on .sr-d-theater {
  opacity: 1;
  transform: translate(-50%, -50%) translateZ(var(--sr-front-z)) var(--sr-tilt) scale(1);
}

/* (the old FORE layer lived here — DELETED 2026-08-11. Master: nothing drifts in front
   of the display. Its crosshairs now live on the wall SURFACE, behind the theater.) */

/* THREE STACKED MEDIA, one card. Bottom to top: the still capture (always there, the
   floor), the HERO LOOP, the LIVE frame. Only one of the top two is ever mounted at a
   time — except the deliberate few seconds where the loop bridges a slow frame — so the
   z-order is about which wins during that fade, not about compositing three videos. */
/* the still image inside the theater (JS drives translateY + scale) */
.sr-d-media { position: absolute; top: 0; left: 0; width: 100%; z-index: 0; will-change: transform; transform-origin: 0 0; }
.sr-d-media img { display: block; width: 100%; height: auto; }

/* ── HERO LOOP — the card's own first screen, moving ──
   Master 2026-08-13: "just the front page, it should be live, not just a picture." When the
   live frame cannot run (small card / reduced motion / a host that refuses framing), this
   short seamless loop of the hero plays instead, so the card is only ever a STILL when the
   site's hero is genuinely still. It covers the card rather than letter-boxing: the clip is
   captured at the card's own 16:9, and `object-position: 50% 0` keeps the top of the hero —
   the logo, the headline — anchored if a clip is ever off-ratio.
   NO clip-path here, deliberately: the card already has overflow:hidden and a clip-path on
   a video container is a known way to lose the compositor fast path. */
.sr-d-hero {
  position: absolute; inset: 0; z-index: 1;
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 0;
  background: #070a10;
  pointer-events: none;                     /* same as the frame — click-outside still closes */
  opacity: 0; transition: opacity .5s ease;
}
.sr-d-hero.ready { opacity: 1; }

/* the LIVE site — mounted inside the theater at a true 16:9 viewport, then fitted */
.sr-d-frame {
  position: absolute; top: 0; left: 0; border: 0; display: block; z-index: 2;
  transform-origin: 0 0; will-change: transform;
  pointer-events: none;                     /* clicks pass through → click-outside still closes */
  opacity: 0; transition: opacity .5s ease;
  background: #0a0d13;
}
.sr-d-frame.ready { opacity: 1; }

/* ── VEIL — a FLAT vignette in the vacuum between the two depths. Not a layer: it is a
      pseudo-element of the scene itself, so the DOM holds exactly two depth layers. It
      buys depth, and text legibility on the right where the wall curves away. ── */
/* ITEM 16 — the veil no longer BLACKS OUT the right-hand third. Master: "you made the
   screen behind [the theater], but I want it full screen even behind the description text."
   The wall geometry always did reach that far (an elementFromPoint grid found a facet under
   every one of 1025 sample points), so the wall was never the problem — THIS was. The old
   right-hand ramp ran to rgba(3,5,10,.9) against a radial already at .82, i.e. a combined
   98% black over the last few hundred px, which is a curtain, not a vignette. It is now a
   57% falloff at the very edge, so the LED wall and its bend read THROUGH the info text.
   The contrast the text needs comes from a local scrim on .sr-d-info instead — a gradient
   that dies out within ~40px of the text, not a panel and not a full-height wash. */
.sr-detail::after {
  content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background:
    radial-gradient(122% 106% at 36% 48%, rgba(3,5,10,0) 40%, rgba(3,5,10,.15) 74%, rgba(3,5,10,.42) 100%),
    linear-gradient(90deg, rgba(3,5,10,.34) 0%, rgba(3,5,10,0) 20%, rgba(3,5,10,0) 62%, rgba(3,5,10,.11) 84%, rgba(3,5,10,.26) 100%);
}

/* ── INFO — bare floating text in the vacuum on the right. No card. No strip. ── */
.sr-d-info {
  position: absolute; z-index: 4;
  right: clamp(22px, 5vw, 92px); top: 50%; transform: translateY(-50%);
  width: min(360px, 32vw);
  text-shadow: 0 2px 26px rgba(0,0,0,.94), 0 1px 5px rgba(0,0,0,.88);
  opacity: 0; translate: 0 14px;
  transition: opacity .6s ease .3s, translate .6s cubic-bezier(.16,1,.3,1) .3s;
}
/* LOCAL scrim (item 16) — the contrast the text needs, and nothing wider. It is bound to
   the text box (inset, not viewport), it is a gradient that reaches zero before the frame
   edge, and it is z-index −1 INSIDE .sr-d-info's own stacking context, so it sits over the
   wall but under every glyph. This is what replaces the full-height black curtain the veil
   used to be: the wall's lamps and its bend stay visible all the way to the right edge. */
.sr-d-info::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  inset: -34px -40px -34px -58px;
  background: radial-gradient(112% 96% at 64% 50%, rgba(3,5,10,.74) 0%, rgba(3,5,10,.5) 48%, rgba(3,5,10,0) 100%);
}
.sr-detail.on .sr-d-info { opacity: 1; translate: 0 0; }
.sr-d-kicker {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: var(--trk-lbl); /* was .24em */
  color: var(--sr-accent, #1877F2); margin-bottom: 14px;
}
.sr-d-kicker::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--sr-accent, #1877F2); box-shadow: 0 0 12px var(--sr-accent, #1877F2);
}
.sr-d-info h2 {
  font-family: var(--font-shell); font-weight: 700;
  font-size: clamp(1.9rem, 3.2vw, 3.1rem); line-height: 1.0; letter-spacing: var(--trk-d4);
  color: #fff; margin: 0 0 .28em;
}
.sr-d-tag  { font-family: var(--font-shell); font-size: clamp(.95rem, 1.4vw, 1.08rem); line-height: 1.4; letter-spacing: var(--trk-lead); color: rgba(255,255,255,.86); margin: 0 0 1.1em; }
.sr-d-desc { font-family: var(--font-shell); font-size: .94rem; line-height: 1.66; letter-spacing: var(--trk-sm); color: rgba(255,255,255,.62); margin: 0 0 1.5em; }
.sr-d-meta { display: flex; flex-wrap: wrap; gap: 8px 16px; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: var(--trk-lbl); color: rgba(255,255,255,.48); margin-bottom: 1.7em; } /* was .16em */
.sr-d-meta span { position: relative; }
.sr-d-meta span + span::before { content: "·"; position: absolute; left: -10px; opacity: .5; }
.sr-d-actions { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
/* A real BUTTON, not a text link — Master couldn't find the old underlined mono label. */
.sr-d-visit {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--font-mono); font-size: 11.5px; letter-spacing: var(--trk-lbl-lg); /* VISIT LIVE; was .18em */
  color: #fff; text-decoration: none;
  padding: 13px 24px; border-radius: 999px;
  border: 1px solid rgba(255,255,255,.42);
  background: rgba(255,255,255,.05);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  transition: background .25s ease, border-color .25s ease, color .25s ease, transform .25s cubic-bezier(.16,1,.3,1);
  white-space: nowrap;
}
.sr-d-visit:hover {
  background: var(--sr-accent, #1877F2);
  border-color: var(--sr-accent, #1877F2);
  color: #fff; transform: translateY(-2px);
}
.sr-d-visit .arr { display: inline-block; transition: transform .25s ease; }
.sr-d-visit:hover .arr { transform: translate(2px, -2px); }
/* not-live state — the row still occupies its place so the area never looks broken */
.sr-d-visit[data-dead] {
  color: rgba(255,255,255,.4); border-style: dashed; border-color: rgba(255,255,255,.2);
  background: none; backdrop-filter: none; -webkit-backdrop-filter: none; cursor: default;
}
.sr-d-visit[data-dead]:hover { background: none; border-color: rgba(255,255,255,.2); color: rgba(255,255,255,.4); transform: none; }
.sr-d-close {
  background: none; border: none; padding: 6px 0; cursor: pointer;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: var(--trk-lbl); /* ESC · CLOSE; was .18em */
  color: rgba(255,255,255,.46); transition: color .25s ease;
}
.sr-d-close:hover { color: #fff; }
.sr-d-live {                                            /* tiny LIVE / STILL state chip, text only */
  font-family: var(--font-mono); font-size: 10px; letter-spacing: var(--trk-lbl); /* was .2em */
  color: rgba(255,255,255,.4); margin-top: 22px;
}

/* ── Mobile: flatter space, theater on top, text underneath, image instead of iframe ── */
@media (max-width: 760px) {
  /* same two depths, gentler — a hard skew on a 375px screen just eats the wall.
     buildWall() reads the same 760px breakpoint and drops to 9 facets / a 30° arc. */
  .sr-detail {
    /* item 23: oblique on the phone too, but 30° not 55° — the theater is stacked on TOP
       here and the text sits under it, so a hard 55° would foreshorten the card to a sliver
       on a 375px screen. Perspective goes up with the angle for the same depth-spread reason
       as the desktop block. */
    --sr-persp: 1700px; --sr-tilt: rotateY(30deg) rotateX(2deg); --sr-po-x: 50%;
    --sr-wall-z: -560px; --sr-front-z: 70px;
    /* finer pitch on a small screen, and 12 lamps per module either way (72 = 12 × 6) so
       the seam still lands on a gutter. The drift is the SAME 8 px/s as the desktop — it is
       a constant in glRender() now rather than a module-per-duration keyframe.
       Coarsened 3px → 6px in the same pass as the desktop 4 → 8, keeping the phone exactly
       three quarters of the desktop pitch it has always been.
       Gutter thinned 1.5px → 0.75px alongside the desktop 2 → 1. The sub-device-pixel
       worry that ruled 0.75px out on the DESKTOP does not apply here — a phone runs at
       dpr 2-3, so 0.75 CSS px is 1.5-2.25 device px, a real line.
       Coarsened again 6px → 9px with the desktop 8 → 12 (2026-08-17), holding the phone at
       three quarters of the desktop pitch as it always has been, and 72 = 8 × 9 keeps the
       cabinet seam on a gutter — same 8 lamps per module the desktop now has.
       The GUTTER IS NOT COARSENED WITH IT, on purpose: `--ledg` is no longer pegged to
       --ledp/8 (see the desktop block — that rule is retired). Scaling the gutter with the
       pitch is the one thing Master has twice told us not to do. Constant fill across the
       breakpoint is now kept by `--ledgo`, which this block deliberately does NOT override
       so the phone inherits the desktop's .45 and reads as the same panel. */
    --ledp: 9px; --ledg: 0.75px; --modp: 72px;
  }
  .sr-d-theater { top: 34%; left: 50%; width: 86vw; }
  .sr-d-info {
    right: 20px; left: 20px; width: auto; top: auto; bottom: clamp(28px, 9vh, 72px); transform: none;
  }
  .sr-detail.on .sr-d-info { translate: 0 0; }
  .sr-d-info h2 { font-size: clamp(1.6rem, 7vw, 2.2rem); }
  .sr-d-desc { display: none; }                          /* keep the small screen breathable */
  .sr-detail::after {
    background:
      radial-gradient(120% 70% at 50% 30%, rgba(3,5,10,0) 30%, rgba(3,5,10,.55) 78%, rgba(3,5,10,.92) 100%),
      linear-gradient(180deg, rgba(3,5,10,.4) 0%, rgba(3,5,10,0) 30%, rgba(3,5,10,.82) 74%, rgba(3,5,10,.96) 100%);
  }
}

/* ── Reduced motion: no auto-scroll (JS), no parallax, no blow-up ── */
@media (prefers-reduced-motion: reduce) {
  .sr-detail, .sr-d-space, .sr-d-theater, .sr-d-info, .sr-d-frame { transition: none !important; }
  .sr-detail.on .sr-d-theater { transform: translate(-50%, -50%) translateZ(var(--sr-front-z)) var(--sr-tilt); }
  /* FREEZE THE DRIFT, KEEP THE WALL. The panel surface is structure, not motion — removing
     it would take the screen away, which is not what the setting asks for. Only the
     right-to-left travel stops. (Reduced motion also drops the wall to the still capture
     rather than the scroll video — see openDetail.)
     This is now enforced in glRender(), which does not advance GL.drift under reduceMotion,
     rather than by an `animation: none` override — there is no CSS animation left to
     cancel. The canvas's own opacity transition is cancelled by the rule at the top of this
     block, which already names .sr-d-space; add .sr-d-gl to it: */
  .sr-d-gl { transition: none !important; }
}

/* ═══ ITEM 25 — THE FAST PATH: "SEE ALL N PROJECTS" + the grid it opens ═══
   Master: with 21 cards, "if they don't want to see all the websites, they don't have to."
   The carousel is untouched and stays the hero; this is a second, 2-click route to any
   project. WW system throughout: Space Grotesk for the title, Inter for copy, Geist Mono
   for the label, #1877F2 as the only accent. ── */
.sr-allbtn {
  /* TOP-LEFT of the stage, parked BELOW the fixed site header.
     It was top-right, which is exactly where .chrome-top's nav lives — the header is
     `position: fixed; z-index: 90`, so it is not a child of the stage and never appeared in
     a stage-relative collision check. At 1291x625 the header's real rect is [0,0,1276,77]
     spanning the FULL width, and the pill landed inside it between the STUDIO and
     START A PROJECT chips, with only the letters "AL" visible between them.
     --sr-hdr is written from the header's measured height at runtime (see setHeaderVar),
     so this clears it at any viewport instead of trusting a guessed constant. The left
     column is free from the header's underside down to the project title block at y=344. */
  position: absolute; z-index: 5;
  top: calc(var(--sr-hdr, 78px) + 26px); left: clamp(20px, 3vw, 64px); right: auto;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 12px 20px;
  font-family: var(--font-mono);
  font-size: 11.5px; letter-spacing: var(--trk-lbl-lg); color: #fff; /* SEE ALL; was .16em */
  cursor: pointer;
  background: rgba(10, 14, 22, .52);
  border: 1px solid color-mix(in srgb, #1877F2 55%, transparent);
  border-radius: 999px;
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  /* the GLOW: one soft accent halo, and a hairline inner lift so the pill still reads as
     a surface rather than a sticker. Understated on purpose — Master asked for glowing,
     not gaudy. */
  box-shadow: 0 0 0 1px rgba(255,255,255,.05) inset, 0 0 22px -4px rgba(24,119,242,.55);
  opacity: 0;
  transition: opacity .5s ease, transform .28s ease, border-color .28s ease, box-shadow .28s ease;
}
body.showroom.sr-ready .sr-allbtn { opacity: 1; animation: sr-allpulse 3.4s ease-in-out infinite; }
.sr-allbtn:hover, .sr-allbtn:focus-visible {
  border-color: #1877F2;
  box-shadow: 0 0 0 1px rgba(255,255,255,.08) inset, 0 0 34px -2px rgba(24,119,242,.85);
  transform: translateY(-2px);
  outline: none;
}
.sr-allbtn-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: #1877F2; box-shadow: 0 0 10px #1877F2;
  flex: 0 0 auto;
}
@keyframes sr-allpulse {
  0%, 100% { box-shadow: 0 0 0 1px rgba(255,255,255,.05) inset, 0 0 18px -5px rgba(24,119,242,.45); }
  50%      { box-shadow: 0 0 0 1px rgba(255,255,255,.05) inset, 0 0 30px -3px rgba(24,119,242,.80); }
}

/* ─── the grid overlay ─── */
.sr-grid[hidden] { display: none; }
.sr-grid {
  position: fixed; inset: 0; z-index: 940;
  overflow-y: auto; overscroll-behavior: contain;
  padding: clamp(22px, 5vh, 56px) clamp(18px, 5vw, 72px) clamp(40px, 8vh, 90px);
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(24,119,242,.10), transparent 62%),
    rgba(4, 6, 11, .96);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  opacity: 0; transition: opacity .34s ease;
}
.sr-grid.on { opacity: 1; }
.sr-grid-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 20px; flex-wrap: wrap; margin-bottom: clamp(20px, 4vh, 40px);
}
.sr-grid-kicker {
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: var(--trk-lbl); color: #1877F2; margin-bottom: 10px; /* was .24em */
}
.sr-grid-title {
  font-family: var(--font-shell); font-weight: 700;
  font-size: clamp(1.7rem, 4vw, 2.9rem); line-height: 1; letter-spacing: var(--trk-d4);
  color: #fff; margin: 0;
}
.sr-grid-close {
  background: none; border: none; cursor: pointer; padding: 8px 0;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: var(--trk-lbl); /* was .18em */
  color: rgba(255,255,255,.5); transition: color .25s ease;
}
.sr-grid-close:hover, .sr-grid-close:focus-visible { color: #fff; outline: none; }
.sr-grid-list {
  display: grid; gap: clamp(14px, 1.6vw, 26px);
  grid-template-columns: repeat(auto-fill, minmax(236px, 1fr));
}
.sr-gcard {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: 0; border: 1px solid rgba(255,255,255,.10); border-radius: 12px;
  background: rgba(255,255,255,.025); overflow: hidden;
  transition: border-color .26s ease, transform .26s ease, box-shadow .26s ease;
}
.sr-gcard:hover, .sr-gcard:focus-visible {
  border-color: var(--gacc, #1877F2);
  transform: translateY(-3px);
  box-shadow: 0 14px 40px rgba(0,0,0,.55), 0 0 26px -10px var(--gacc, #1877F2);
  outline: none;
}
.sr-gshot { display: block; aspect-ratio: 16 / 10; overflow: hidden; background: #080b12; }
.sr-gshot img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .4s ease; }
.sr-gcard:hover .sr-gshot img { transform: scale(1.04); }
.sr-gmeta { display: block; padding: 13px 15px 15px; }
.sr-gname {
  display: block; font-family: var(--font-shell); font-weight: 700;
  font-size: 1rem; letter-spacing: var(--trk-body); color: #fff; margin-bottom: 5px;
}
.sr-gtag {
  display: block; font-family: var(--font-shell); font-size: .8rem;
  line-height: 1.4; letter-spacing: var(--trk-xs); color: rgba(255,255,255,.56);
}
body.sr-grid-open { overflow: hidden; }

@media (max-width: 640px) {
  /* same top-left rule on a phone — the bottom of the stage is a full-width band of
     chrome (chapter label, dots, SCROLL hint) with no free corner */
  /* 11.5 -> 10px drops it out of the lg band into the base step; was .12em */
  .sr-allbtn { top: calc(var(--sr-hdr, 62px) + 16px); left: 16px; right: auto; padding: 10px 15px; font-size: 10px; letter-spacing: var(--trk-lbl); }
  .sr-grid-list { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
}
@media (prefers-reduced-motion: reduce) {
  .sr-allbtn { animation: none; }
  .sr-grid, .sr-gcard, .sr-gshot img { transition: none; }
}
