/* ── Margaret Unknown — version 0 ──────────────────────────────────────────
   Palette is black / white / red. Red is reserved for the active joker,
   small interaction details and active headings. It never fills the screen.
   ------------------------------------------------------------------------ */

:root {
  --black: #060607;
  --ink:   #0b0b0d;
  --white: #f2f0ec;
  --bone:  #cfcac2;
  --grey:  #8b8781;
  --red:   #d81f26;
  --red-d: #8e1116;

  /* What the fogged pair settles to once the hand is dealt. Mirrors `FOG_REST`
     in js/carrier.js so the CSS carrier and the shader composite the same
     picture. Zero here meant the backdrop spent the whole visit as frame-17
     twice over, with frame-16 gone. */
  --fog-rest: .35;

  /* The size a card settles at once a section is open and the deck has moved to
     the top of the page — small, because it is navigation by then.

     THERE IS NO --card-h, DELIBERATELY. It used to be
     `--card-h: calc(var(--card-w) * 1.42)` declared here, and that is a trap: a
     custom property's var() is substituted where the property is DECLARED, not
     where it is used. So --card-h computed once against this --card-w and that
     resolved length inherited everywhere. Overriding --card-w further down the
     tree widened the cards and left their height behind, and they came out
     lying on their sides. Height comes from `aspect-ratio` at the point of use
     now, so it cannot drift from the width again. */
  --card-w: clamp(88px, 12vw, 132px);
  --card-ratio: 1.42;                  /* a plain number: nothing to substitute */

  --ease-out: cubic-bezier(.16,.84,.44,1);
  --ease-in:  cubic-bezier(.7,0,.84,0);

  --font: ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, Consolas, monospace;
  --display: "Times New Roman", Times, "Liberation Serif", serif;
}

/* ── THREE SIZES, ONE DESCENT ──────────────────────────────────────────────
   The cards get smaller at every step, and each step is a step down:

     deck   176px   the pile, idle and spread. The card is the whole page.
     hand    138px   the deal. Between the two — it is still the event, but it
                     is on its way to being a menu.
     open    104px   the top menu. Navigation, and it should get out of the way.

   The deal is deliberately not the same size as the pile it came out of and not
   the same size as the menu it becomes; sitting between them is what makes it
   read as a stage rather than as either of its neighbours.

   The vw terms let all three scale down together on narrower screens — five
   176px cards want about 1100px once gaps and rail padding are counted. Below
   700px the rail becomes a scroll-snapping strip and sets its own size, so this
   is scoped above that and cannot override it.
   ------------------------------------------------------------------------ */
@media (min-width: 701px) {
  body[data-stage="deck"] { --card-w: clamp(96px, 11.5vw, 176px); }
  body[data-stage="hand"] { --card-w: clamp(90px, 9.2vw, 138px); }
  body[data-stage="open"] { --card-w: clamp(76px, 7vw, 104px); }
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0;
  min-height: 100%;
  background: var(--black);
  color: var(--white);
  font-family: var(--font);
  /* 16.5px, ten per cent up from the 15px this was built at. The artist's
     reading of the finished page: "everything is a bit hard to read because so
     fine." Every size in the panel is in `rem`, so one number moves all of them
     together and none of the relationships inside it change. The cards do not
     move with it — `--card-w` and the deck geometry are px and vw — which is why
     this is safe to turn. */
  font-size: 16.5px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  overflow-x: hidden;
}

a { color: var(--white); text-decoration: none; border-bottom: 1px solid var(--red-d); }
a:hover, a:focus-visible { color: var(--red); border-bottom-color: var(--red); }

:focus-visible {
  outline: 2px solid var(--red);
  outline-offset: 3px;
}

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

.skip {
  position: absolute; left: -999px; top: 0; z-index: 100;
  background: var(--red); color: #fff; padding: .6rem 1rem; border: 0;
}
.skip:focus { left: 0; }

/* ── backdrop ──────────────────────────────────────────────────────────────
   Two photographs, four layers. Each copy creeps inward on its own slow zoom and
   swells on its own long fade, and the four periods (67s, 79s, 59s, 97s) share
   no common multiple — so the combination never repeats and the picture is
   always mid-change without ever appearing to cut.

   SCALE. The creep used to run 1.02 -> 1.46 and 1.58 -> 1.06, on top of `cover`.
   That meant the backdrop was never once at its natural size: at the far end of
   the cycle a 1918px-wide viewport was showing the photograph blown up to
   3030px, about 29% of the frame, upscaled past the source's own 2400px. It read
   as permanently zoomed in, which is exactly what it was.

   It now runs 1.02 -> 1.09. The floor is 1.02 rather than 1.00 because the
   layers also translate by up to +-0.8%, and a layer at exactly `cover` would
   show its own edge as it drifted. At 1.02 on a 1918x923 viewport the frame is
   1956px wide against a 2400px source: downscaled, so it is sharp, and the whole
   width of the photograph is on screen.

   Two images, downloaded once, composited on the GPU.
   ------------------------------------------------------------------------ */
.bg {
  position: fixed; inset: 0; z-index: 0;
  overflow: hidden;
  background: #10161a;
}

.bg__frame {
  position: absolute; inset: 0;
  background-size: cover;
  /* Anchored low: these frames are 3:2 against a much wider viewport, so `cover`
     crops top and bottom — and the island, the spire and the horizon all sit in
     the lower third. Centring throws the subject away. */
  background-position: 50% 72%;
  will-change: transform, opacity;
}

/* the clear pair: always present, breathing against each other.

   LINEAR, NOT EASED. On cycles this long (59-97s) an ease-in-out spends its
   slow tails in seconds, not milliseconds: each frame decelerates to a halt and
   sits at its extreme for the better part of twenty seconds. Read on screen
   that is not "breathing", it is stuck — the four copies freeze in a fixed
   displacement and the composite stops moving. Linear keeps every frame in
   motion for the whole cycle, which is the only reason the drift is invisible. */
.bg__frame--clear-a {
  background-image: url("../assets/img/backdrop/frame-17.jpg");
  animation: creep-a 67s linear infinite alternate,
             tide-a  79s linear infinite;
}
.bg__frame--clear-b {
  background-image: url("../assets/img/backdrop/frame-17.jpg");
  background-position: 42% 64%;
  animation: creep-b 97s linear infinite alternate,
             tide-b   59s linear infinite;
}

/* the fogged pair: holds the landing, then lifts when the hand is dealt */
.bg__frame--fog-a {
  background-image: url("../assets/img/backdrop/frame-16.jpg");
  background-position: 50% 52%;
  animation: creep-b 59s linear infinite alternate,
             tide-a  97s linear infinite;
  transition: filter 3.4s var(--ease-out);
}
.bg__frame--fog-b {
  background-image: url("../assets/img/backdrop/frame-16.jpg");
  background-position: 58% 44%;
  animation: creep-a 79s linear infinite alternate,
             tide-b   67s linear infinite;
  transition: filter 4.6s var(--ease-out);
}

/* ── dealing the hand LIFTS the fog; the clear pair keeps moving ──────────
   The fog settles to `--fog-rest` rather than to nothing, so both photographs
   stay in the composite for the whole visit. Mirrors `FOG_REST` in carrier.js;
   the two paths have to agree or the fallback stops being a fallback.

   FILTER, NOT OPACITY, AND THIS IS A BUG FIX. These layers animate `opacity`
   through the tide keyframes, and a CSS animation outranks a normal
   declaration — so the `opacity: 0` that used to live here never won. Measured
   at stage `open`, a fog layer still computed 0.95: the CSS carrier has never
   lifted its fog at all, and only the shader ever did. It went unnoticed
   because the shader is canonical and this path is only reached when WebGL
   fails. `filter: opacity()` multiplies the rendered result instead, which no
   animation here touches, and gives exactly the shader's `tide * FOG_REST`. */
body[data-stage="hand"] .bg__frame--fog-a,
body[data-stage="open"] .bg__frame--fog-a,
body[data-stage="hand"] .bg__frame--fog-b,
body[data-stage="open"] .bg__frame--fog-b { filter: opacity(var(--fog-rest)); }

/* ── the creep: slow, small, and never at the same rate twice ─────────────
   The drift does the work now, not the zoom. Keep the translations under half
   the scale's headroom or a layer will show its own edge. */
@keyframes creep-a {
  from { transform: scale(1.02) translate3d(-.5%, -.4%, 0); }
  to   { transform: scale(1.09) translate3d(.8%, .6%, 0); }
}
@keyframes creep-b {
  from { transform: scale(1.09) translate3d(.7%, .5%, 0); }
  to   { transform: scale(1.02) translate3d(-.6%, -.4%, 0); }
}

/* ── the tide: each layer swells and recedes on its own long cycle ───────── */
@keyframes tide-a {
  0%   { opacity: .95; }
  22%  { opacity: .38; }
  41%  { opacity: .72; }
  60%  { opacity: .18; }
  78%  { opacity: .66; }
  100% { opacity: .95; }
}
@keyframes tide-b {
  0%   { opacity: .34; }
  19%  { opacity: .78; }
  37%  { opacity: .22; }
  55%  { opacity: .88; }
  72%  { opacity: .41; }
  100% { opacity: .34; }
}

/* ── renditions ────────────────────────────────────────────────────────────
   Three widths, chosen against the DEVICE pixels the layer actually needs:
   viewport width x devicePixelRatio x the creep's 1.09 ceiling.

     -sm  1600   a 430px phone at 3x needs ~1400
     ---  2400   1918px at 1x needs ~2100  (the default)
     -lg  3130   1440px at 2x needs ~3140; this is the master's full width

   The -lg pair is 1.5 MB each, so it is kept away from phones: a dense display
   only gets it once the viewport is wide enough to be a real screen.
   ------------------------------------------------------------------------ */
@media (max-width: 900px) {
  .bg__frame--clear-a,
  .bg__frame--clear-b { background-image: url("../assets/img/backdrop/frame-17-sm.jpg"); }
  .bg__frame--fog-a,
  .bg__frame--fog-b   { background-image: url("../assets/img/backdrop/frame-16-sm.jpg"); }
}

@media (min-width: 1700px),
       (min-width: 700px) and (min-resolution: 1.5dppx) {
  .bg__frame--clear-a,
  .bg__frame--clear-b { background-image: url("../assets/img/backdrop/frame-17-lg.jpg"); }
  .bg__frame--fog-a,
  .bg__frame--fog-b   { background-image: url("../assets/img/backdrop/frame-16-lg.jpg"); }
}

/* ── the carwash, surfacing ────────────────────────────────────────────────
   Above everything — the module, the cards, the popup — because it passes OVER
   the site rather than behind it. `pointer-events: none` is what makes that
   safe: it can never take a click or block a card, so nothing below it changes
   behaviour while it is there.

   `visibility` rides with the opacity so a fully faded-out video is not merely
   invisible but out of the way entirely, and it is transitioned with a delay so
   it only disappears once the fade has finished. */
.carwash {
  position: fixed; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  pointer-events: none;
  z-index: 200;
  opacity: 0;
  visibility: hidden;
  transition: opacity 2.6s var(--ease-out), visibility 0s linear 2.6s;
  /* Screen keeps the clip's own blacks out of the way, so what comes over the
     page is the light in it rather than a grey film across everything. */
  mix-blend-mode: screen;
}
.carwash[data-on="true"] {
  opacity: var(--peak, .15);
  visibility: visible;
  transition: opacity 2.2s var(--ease-out), visibility 0s;
}

@media (prefers-reduced-motion: reduce) {
  .carwash { display: none; }
}

.noscript {
  position: relative; z-index: 3;
  max-width: 60ch; margin: 3rem auto; padding: 1.5rem;
  background: rgba(6,6,7,.9); border: 1px solid var(--red-d);
}

@media (prefers-reduced-motion: reduce) {
  /* no drift and no flicker: hold the fogged frame still */
  .bg__frame { animation: none !important; transform: scale(1.04); }
  .bg__frame--clear-b, .bg__frame--fog-b { opacity: 0; }
  .bg__frame--fog-a { opacity: 1; }
  /* Same settle as everywhere else. Here the animations are off, so `opacity`
     would in fact win — but the filter is used anyway so there is one rule
     describing this, not two that can disagree. */
  body[data-stage="hand"] .bg__frame--fog-a,
  body[data-stage="open"] .bg__frame--fog-a { filter: opacity(var(--fog-rest)); }
  * { scroll-behavior: auto !important; }
}

/* ── EXPERIMENT, STAGE 2: the WebGL carrier (?carrier=shader) ──────────────
   The canvas sits exactly where `.bg` does. The CSS carrier is NOT removed and
   NOT altered — the single hook below hides it, and only once js/carrier.js has
   put a real frame on screen. If the shader never gets that far, or loses its
   context later, the class comes off and the approved backdrop is simply there.
   Deleting this block and js/carrier.js leaves the site untouched. */
.carrier-canvas {
  position: fixed; inset: 0; z-index: 0;
  display: block; pointer-events: none;
}
body.carrier-shader .bg { visibility: hidden; }
