/* ==========================================================================
   The curtain.
   Shared by both ends of the transition, with one class deciding direction:
     .curtain--close  on stevencaputo.com/curtain/   valance down, then panels in
     .curtain--open   on lookingglass                panels out, then valance up

   No JavaScript. The close page hands off with a meta refresh once the screen
   is covered, so the swap happens behind the cloth.
   ========================================================================== */

.curtain {
  /* The valance keeps a fixed 5.085:1 ratio, so its rendered height is a pure
     function of viewport width — which makes the rail a calculable position
     rather than a magic number. */
  --valance-ratio: 5.085;
  --valance-h: calc(100vw / var(--valance-ratio));
  --rail: calc(var(--valance-h) * 0.17);

  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  overflow: hidden;
}

/* Panels are sized in percentages and the artwork is stretched to fill, rather
   than kept at its own ratio. Curtain folds are vertical, so stretching reads
   as heavier or lighter gathering — never as distortion — and it guarantees the
   two halves meet in the middle at any window shape. */
.curtain__panel {
  position: absolute;
  /* Hang from the valance's rail, not from the top of the window. At top:0 the
     panels showed above the valance in the corners, where its artwork is
     thinnest — the cloth appeared to float in front of the pelmet instead of
     hanging from it. --rail is a fraction of the valance's own height, so the
     two stay locked together at every window width. */
  top: var(--rail);
  bottom: 0;
  width: 52%;                    /* 4% of overlap where they meet */
  background-repeat: no-repeat;
  background-size: 100% 100%;
  /* The artwork is a curtain, not a blackout: its outer fifth is gathered
     drape with real gaps, and no amount of stretching makes it solid. A dark
     fill behind each panel is what a stage does — the gaps read as the wings
     rather than as the page showing through. It travels with the panel, so
     nothing darkens until the cloth arrives. */
  background-color: #0a0605;
  will-change: transform;
}

.curtain__panel--left  { left: 0; }
.curtain__panel--right { right: 0; }

.curtain__panel--left {
  background-image: image-set(url("/assets/img/curtain-left.webp?c=2") type("image/webp"));
}
.curtain__panel--right {
  background-image: image-set(url("/assets/img/curtain-right.webp?c=2") type("image/webp"));
}

@media (max-width: 48rem) {
  .curtain__panel--left {
    background-image: image-set(url("/assets/img/curtain-left-sm.webp?c=2") type("image/webp"));
  }
  .curtain__panel--right {
    background-image: image-set(url("/assets/img/curtain-right-sm.webp?c=2") type("image/webp"));
  }
}

/* The valance keeps its own 5.08:1 ratio and spans the full width. It sits
   above the panels so their cropped top edge is never visible. */
.curtain__valance {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  aspect-ratio: 2161 / 425;
  z-index: 1;
  /* Two layers, artwork first so it paints on top of the second.

     The second is the rail strip. Above the rail the valance artwork is
     transparent at the far corners and the page showed through the top edge,
     so that band needs filling — but only that band, not the whole box, which
     would flatten the scalloped hem the swag hangs on.

     This was a ::before at z-index -1, which does not work: `z-index: 1` above
     makes .curtain__valance a stacking context, and inside one a negative
     child still paints above the parent's own background image. The strip sat
     across the top of the swag as a black bar. A background layer is the only
     way to get genuinely underneath. */
  background-image:
    image-set(url("/assets/img/curtain-top.webp?c=2") type("image/webp")),
    linear-gradient(#0a0605, #0a0605);
  background-size: 100% 100%, 100% calc(var(--rail) + 1px);
  background-position: 0 0, 0 0;
  background-repeat: no-repeat, no-repeat;
  will-change: transform;
}

@media (max-width: 48rem) {
  /* Both layers, or the strip is dropped along with the full-size artwork. */
  .curtain__valance {
    background-image:
      image-set(url("/assets/img/curtain-top-sm.webp?c=2") type("image/webp")),
      linear-gradient(#0a0605, #0a0605);
  }
}

/* ── the cue ─────────────────────────────────────────────────────────────
   Six seconds, counted from the moment the power button is pressed:

     0.0 - 1.0   valance lowers
     1.0 - 2.0   panels close
     2.0 - 4.0   held shut  (the page swap happens at 3.0, mid-hold)
     4.0 - 5.0   panels open
     5.0 - 6.0   valance rises

   Split across two documents: the first two seconds run on the hand-off page,
   the last two on lookingglass. Its clock starts at its own load, so its
   delays are measured from 3.0 rather than from 0.

   The easing carries the weight. Cloth arriving overshoots a little and settles
   back, the way a heavy drape swings past its rest before hanging still. Cloth
   leaving accelerates away instead — it is being pulled, not falling. */

.curtain {
  --ease-arrive: cubic-bezier(0.24, 0.72, 0.20, 1.045);
  --ease-depart: cubic-bezier(0.52, 0.02, 0.76, 0.36);
}

.curtain--close .curtain__valance {
  transform: translateY(-101%);
  animation: curtain-valance-down 1000ms var(--ease-arrive) 0ms forwards;
}
.curtain--close .curtain__panel--left {
  transform: translateX(-101%);
  animation: curtain-left-in 1000ms var(--ease-arrive) 1000ms forwards;
}
.curtain--close .curtain__panel--right {
  transform: translateX(101%);
  animation: curtain-right-in 1000ms var(--ease-arrive) 1000ms forwards;
}

@keyframes curtain-valance-down { to { transform: translateY(0); } }
@keyframes curtain-left-in      { to { transform: translateX(0); } }
@keyframes curtain-right-in     { to { transform: translateX(0); } }

/* Opening. Delays are from this page's load, which lands at 3.0 on the cue
   above — so 1000ms here is 4.0 on the clock, and 2000ms is 5.0. */

.curtain--open .curtain__panel--left {
  transform: translateX(0);
  animation: curtain-left-out 1000ms var(--ease-depart) 1000ms forwards;
}
.curtain--open .curtain__panel--right {
  transform: translateX(0);
  animation: curtain-right-out 1000ms var(--ease-depart) 1000ms forwards;
}
.curtain--open .curtain__valance {
  transform: translateY(0);
  animation: curtain-valance-up 1000ms var(--ease-depart) 2000ms forwards;
}

@keyframes curtain-left-out   { to { transform: translateX(-101%); } }
@keyframes curtain-right-out  { to { transform: translateX(101%); } }
@keyframes curtain-valance-up { to { transform: translateY(-101%); } }

/* ── the blackout ───────────────────────────────────────────────────────
   The swap costs ~150 ms of black no matter what: the browser tears down one
   document and paints another, and no stylesheet reaches that gap. Measured
   over two screen recordings it lands at 2.99–3.14 on this page's clock —
   exactly where the refresh fires, which is the one thing about it that is
   reliable.

   So rather than fight it, arrive there already black. The light dies as the
   panels settle, holds through the swap, and comes back as they part. A gap
   inside a deliberate blackout is not a gap; it is the dark.

   Timings, on each page's own clock:
     close   1700 → 2500   fade down, done 500 ms before the refresh at 3000
     open       0 → 1150   hold black (the panels start moving at 1000)
             1150 → 1950   fade up, finishing as the panels clear

   The margin at 2500 is what absorbs a slow navigation. If the hand-off takes
   longer than measured the cloth simply sits black a moment more, which is the
   failure everyone wants. */
.curtain__dim {
  position: absolute;
  inset: 0;
  z-index: 2;                    /* over the panels (auto) and valance (1) */
  background: #000;
  opacity: 0;
  pointer-events: none;
}

/* This page has the same problem at both ends. The click costs its own paint
   gap — ~33 ms of black between the landing page going away and this one
   arriving — and the old version landed at full brightness on the far side of
   it, so the gap read as a blink right before the valance moved. Opening black
   and coming up over 120 ms means the gap is the front of a fade rather than a
   hole in the middle of one. The valance has travelled about a tenth of its
   way by then.

   One animation for both ends, keyed off a 2500 ms span:
     0        opacity 1   arrive black, continuous with the gap
     120 ms   opacity 0   up to full, valance still near the top
     1700 ms  opacity 0   hold, through the whole close
     2500 ms  opacity 1   down to black, 500 ms before the refresh */
.curtain--close .curtain__dim {
  opacity: 1;
  animation: curtain-dim-close 2500ms linear 0ms forwards;
}
.curtain--open .curtain__dim {
  opacity: 1;                    /* this page opens in the dark */
  animation: curtain-dim-out 800ms ease-out 1150ms forwards;
}

@keyframes curtain-dim-close {
  0%    { opacity: 1; animation-timing-function: ease-out; }
  4.8%  { opacity: 0; animation-timing-function: linear; }
  68%   { opacity: 0; animation-timing-function: ease-in; }
  100%  { opacity: 1; }
}
@keyframes curtain-dim-out { to { opacity: 0; } }

/* Once open the curtain must stop being a full-screen stacking context. */
.curtain--open { animation: curtain-retire 0ms linear 3000ms forwards; }
@keyframes curtain-retire { to { visibility: hidden; } }

/* Someone who does not want motion should not sit through a theatre cue. The
   hand-off still happens; the destination simply starts with the cloth gone. */
@media (prefers-reduced-motion: reduce) {
  .curtain--close .curtain__valance,
  .curtain--close .curtain__panel--left,
  .curtain--close .curtain__panel--right {
    animation-duration: 1ms;
    animation-delay: 0ms;
  }
  .curtain--open { display: none; }
  /* No blackout either — it exists to cover motion nobody is watching here. */
  .curtain__dim { animation: none; opacity: 0; }
}

/* Only reachable if the refresh never fires — a stalled tab, or a browser with
   meta refresh turned off. It sits under the cloth, so it is invisible in the
   normal case and available in the abnormal one. */
.curtain-fallback {
  position: fixed;
  left: 50%;
  bottom: 2rem;
  transform: translateX(-50%);
  margin: 0;
  z-index: 1;
  font: 400 0.8rem/1.6 "Iowan Old Style", Palatino, Georgia, serif;
  letter-spacing: 0.18em;
  font-variant: small-caps;
}
.curtain-fallback a { color: #c9a24a; }

/* Every curtain URL above and below carries ?c=2. It is not a cache-buster for
   changed art — the art has not changed. It is there because the images were
   first served under Cross-Origin-Resource-Policy: same-origin, and a browser
   that cached them then keeps enforcing that header for a month. The warm below
   was refused with ERR_BLOCKED_BY_RESPONSE.NotSameOrigin on exactly the four
   files that had been fetched before the header changed, and sailed through on
   the one that had not. A new key is a response with the new header.

   The warm and the real use must carry the same key or the warm primes an entry
   nothing asks for. That includes the standby card, whose key lives in
   lookingglass's index.html.

   ── warming the far side ───────────────────────────────────────────────
   The second half of the cue runs on lookingglass, which is a different
   origin: cold DNS, cold TLS, and its own copy of the artwork. That is the
   flash before the curtains open — the panels are already in place, painted
   in their fallback black, and the velvet arrives mid-animation.

   This page has three seconds in which nothing happens, so it spends them
   fetching what the next page will need. Landing at 3.0 with the panels not
   moving until 4.0 gives four seconds of budget for ~700K, and it warms the
   connection either way.

   Only /curtain/ carries this element. On lookingglass the same stylesheet
   is inert here, which keeps the two copies byte-identical.

   The requests are cross-origin, so they need two things to be true:
     stevencaputo.com  img-src includes lookingglass.stevencaputo.com
     lookingglass      Cross-Origin-Resource-Policy: same-site
   If either is missing the fetch is refused and nothing breaks — the
   transition simply flashes the way it did before. */
.curtain-warm {
  position: fixed;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  z-index: 0;
  pointer-events: none;
  opacity: 0.01;
  background-repeat: no-repeat;
  background-size: 1px 1px;
  background-image:
    url("https://lookingglass.stevencaputo.com/assets/img/curtain-top.webp?c=2"),
    url("https://lookingglass.stevencaputo.com/assets/img/curtain-left.webp?c=2"),
    url("https://lookingglass.stevencaputo.com/assets/img/curtain-right.webp?c=2"),
    /* Both sizes of the card. Which one the browser picks there comes out of a
       srcset — device pixel ratio and a 40rem breakpoint, neither of which this
       rule can see — and guessing wrong warms the wrong file. 70K for both is
       cheaper than being right half the time. */
    url("https://lookingglass.stevencaputo.com/assets/img/standby.webp?c=2"),
    url("https://lookingglass.stevencaputo.com/assets/img/standby-sm.webp?c=2");
}

/* The panels are chosen by a plain media query, not a srcset, so this mirrors
   them exactly. */
@media (max-width: 48rem) {
  .curtain-warm {
    background-image:
      url("https://lookingglass.stevencaputo.com/assets/img/curtain-top-sm.webp?c=2"),
      url("https://lookingglass.stevencaputo.com/assets/img/curtain-left-sm.webp?c=2"),
      url("https://lookingglass.stevencaputo.com/assets/img/curtain-right-sm.webp?c=2"),
      url("https://lookingglass.stevencaputo.com/assets/img/standby.webp?c=2"),
      url("https://lookingglass.stevencaputo.com/assets/img/standby-sm.webp?c=2");
  }
}

/* ── the hand-off page is a continuation, not an arrival ────────────────
   The landing page warms itself up on load: theme.css runs t1-ignite on
   <main> from opacity 0 and brightness 0.4, and main.css raises
   .plate__content from opacity 0. Replaying those on the hand-off meant the
   screen faded up out of black while the valance was already dropping — an
   odd darkness before the cue. Here the page is already on; only the cloth
   should move. */
.is-handoff main,
.is-handoff .plate__content {
  animation: none;
}

/* The same trap, one rule further on. `.ch[data-lit]::after` fires a frame of
   snow on every channel change — and a fresh document counts as a change, so
   the hand-off page opened by blasting white static across the tube for ~180ms
   just as the valance began to drop. Measured off a screen recording: the
   centre of the frame went from luminance 54 to 79 and took ~300ms to fall
   back. Nothing changed channel; the page merely loaded. */
.is-handoff .ch[data-lit]::after {
  animation: none;
}
