/* yolfez.com, The Night Lane.
   One painted wall, four openings, and a walk along it driven by the scroll.
   Every door's position comes from the numbers in lane.json, copied into the
   inline custom properties on each article. Nothing is fetched at runtime. */

/* ---------------------------------------------------------------- type */

@font-face {
  font-family: Newsreader;
  src: url(/fonts/newsreader.woff2) format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: Newsreader;
  src: url(/fonts/newsreader-italic.woff2) format('woff2');
  font-weight: 400; font-style: italic; font-display: swap;
}
/* Georgia stands in while Newsreader loads, tuned so the masthead word does
   not jump when the real face arrives. */
@font-face {
  font-family: 'Newsreader stand-in';
  src: local('Georgia'), local('Times New Roman');
  size-adjust: 98%; ascent-override: 75%; descent-override: 27%; line-gap-override: 0%;
}

/* the walk: 0 at the mouth of the lane, 1 at the far end */
@property --walk { syntax: '<number>'; inherits: true; initial-value: 0; }

/* -------------------------------------------------------------- tokens */

:root {
  color-scheme: dark;

  --wall:  oklch(22% 0.03 60);
  --deep:  oklch(12% 0.018 50);
  --cream: oklch(93% 0.028 84);
  --dim:   color-mix(in oklch, var(--cream) 70%, transparent);
  --flame: oklch(86% 0.125 78);
  --accent: oklch(80% 0.13 80);

  --serif: Newsreader, 'Newsreader stand-in', Georgia, 'Times New Roman', serif;
  --gut: clamp(1.1rem, 4vw, 3.25rem);

  /* the lane image is 2688 by 1152 */
  --ratio: 2.3333;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; background: var(--deep); }

body {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(1rem, 0.95rem + 0.2vw, 1.125rem);
  line-height: 1.5;
  color: var(--cream);
  background: var(--deep);
}

p { margin: 0; }
img, video { max-width: 100%; }
[hidden] { display: none !important; }

::selection { background: color-mix(in oklch, var(--flame) 45%, transparent); }

:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--accent) 75%, var(--cream));
  outline-offset: 3px;
}

/* ============================================================== the lane */

.lane {
  /* a floor under the walk: where @property is not supported and before
     lane.js has written a number, every calc() below stays valid */
  --walk: 0;
  --lane-w: max(100vw, calc(100dvh * var(--ratio)));
  --lane-h: max(100dvh, calc(100vw / var(--ratio)));
  --top: calc((100dvh - var(--lane-h)) / 2);
  --shift: calc(var(--walk) * (var(--lane-w) - 100vw) * -1);

  position: fixed;
  inset: 0;
  z-index: 1;
  overflow: clip;
  background: var(--deep);
}

@keyframes walk { from { --walk: 0; } to { --walk: 1; } }

/* the gate names the three things this actually uses. A browser with
   animation-timeline but without timeline-scope would otherwise pass, resolve
   the named timeline to nothing, and freeze the walk at 0 while lane.js, which
   reads the same three, stood down. */
@supports (animation-timeline: scroll()) and (timeline-scope: --a) and (view-timeline-name: --a) {
  @media (min-width: 901px) and (orientation: landscape) and (prefers-reduced-motion: no-preference) {
    body { timeline-scope: --beats; }
    .beats { view-timeline-name: --beats; }
    .lane {
      animation: walk linear both;
      animation-timeline: --beats;
      animation-range: contain 0% contain 100%;
    }
  }
}

/* every walking layer is the whole painted lane, offset by its own factor */
.sky, .wall, .light, .post {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--lane-w);
  height: var(--lane-h);
  pointer-events: none;
}

/* 1. the sky, and a haze that drifts across it ---------------------------- */

.sky {
  z-index: 1;
  translate: calc(var(--shift) * 0.6) var(--top);
  background:
    radial-gradient(70% 40% at 20% 0%, oklch(30% 0.05 265 / 0.55), transparent 70%),
    radial-gradient(60% 34% at 78% 2%, oklch(26% 0.06 285 / 0.5), transparent 72%),
    linear-gradient(180deg, oklch(16% 0.04 268), oklch(10% 0.02 260) 22%, oklch(8% 0.015 50) 40%);
}
.sky::after {
  content: '';
  position: absolute;
  inset: -10% -6% 40%;
  background:
    radial-gradient(38% 60% at 30% 40%, oklch(55% 0.04 250 / 0.16), transparent 70%),
    radial-gradient(30% 52% at 72% 30%, oklch(52% 0.05 280 / 0.14), transparent 72%);
  animation: haze 40s ease-in-out infinite alternate;
}
@keyframes haze {
  from { translate: -3% 0; scale: 1; }
  to   { translate: 4% 1.5%; scale: 1.08; }
}

/* 2. the painted wall, and the ambient loop over it ----------------------- */

.wall { z-index: 2; translate: var(--shift) var(--top); }
.wall picture { display: contents; }
.wall .still,
.wall .ambient {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.wall .ambient {
  opacity: 0;
  transition: opacity 800ms ease;
  /* the loop came back 1.5% smaller than the still and 8 px of 576 higher, so
     it is drawn back onto the still's own grid. Without this the openings all
     float a dozen pixels loose from the wall the moment the video fades in.
     The numbers are lane.json's video.align. */
  transform-origin: 0 0;
  transform: translate(0, 1.39%) scale(1.015);
}
.wall .ambient.on { opacity: 1; }

/* 3, 4. the doors ---------------------------------------------------------

   .doors and .door are deliberately left with no z-index and no transform of
   their own. Either would open a stacking context and trap the name plate
   inside it, and the plate has to paint above the foreground lamp post while
   the art inside the opening paints below it. */

.doors { position: absolute; inset: 0; pointer-events: none; }

.door {
  position: absolute;
  pointer-events: auto;
  left: calc(var(--x) * var(--lane-w) + var(--shift));
  top: calc(var(--y) * var(--lane-h) + var(--top));
  width: calc(var(--w) * var(--lane-w));
  height: calc(var(--h) * var(--lane-h));
}

/* each opening's exact shape is the alpha of its own cut-out, so the art can
   never spill onto the painted wall or leave a gap at the frame */
.door[data-key='numen']       { --cut: url(/art/open-numen.webp);       --focus: 70% 50%; }
.door[data-key='relay']       { --cut: url(/art/open-relay.webp); }
.door[data-key='tell']        { --cut: url(/art/open-tell.webp); }
.door[data-key='study-rival'] { --cut: url(/art/open-study-rival.webp); --focus: 55% 50%; }

/* The reveal: a hard dark ring a few pixels outside the opening's own shape.
   It is what the inside edge of a hole in a thick wall looks like, and it also
   covers the thin magenta seam the compositing step left around every opening
   in the painted wall. The cut-outs themselves are clean, so the ring sits
   under them and only shows where the wall begins. */
.door::before {
  content: '';
  --d: calc(var(--lane-h) * 0.008);
  position: absolute;
  inset: calc(var(--d) * -1);
  z-index: 3;
  pointer-events: none;
  background: oklch(8% 0.012 45 / 0.94);
  /* eight copies of the same cut-out, one step out in each direction, added
     together: a true dilation, so the ring is the same few pixels wide on an
     opening's inside edges as it is on its outside ones. Scaling one copy up
     would leave the mullion between the bar window and the bar door bare. */
  -webkit-mask-image: var(--cut), var(--cut), var(--cut), var(--cut), var(--cut), var(--cut), var(--cut), var(--cut);
  mask-image:         var(--cut), var(--cut), var(--cut), var(--cut), var(--cut), var(--cut), var(--cut), var(--cut);
  -webkit-mask-size: calc(100% - var(--d) * 2) calc(100% - var(--d) * 2);
  mask-size:         calc(100% - var(--d) * 2) calc(100% - var(--d) * 2);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  mask-mode: alpha;
  -webkit-mask-position:
    0 var(--d), calc(var(--d) * 2) var(--d), var(--d) 0, var(--d) calc(var(--d) * 2),
    0 0, calc(var(--d) * 2) 0, 0 calc(var(--d) * 2), calc(var(--d) * 2) calc(var(--d) * 2);
  mask-position:
    0 var(--d), calc(var(--d) * 2) var(--d), var(--d) 0, var(--d) calc(var(--d) * 2),
    0 0, calc(var(--d) * 2) 0, 0 calc(var(--d) * 2), calc(var(--d) * 2) calc(var(--d) * 2);
}


.opening { position: absolute; inset: 0; }
.opening picture { display: contents; }
/* the whole opening is the target, not just the small underlined name */
.door .opening { cursor: pointer; }

.opening .still,
.opening .loop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  z-index: 3;
}
.opening .still { object-position: 50% 50%; }

.opening .loop {
  object-position: var(--focus, 50% 50%);
  opacity: 0;
  transition: opacity 500ms ease;
  -webkit-mask: var(--cut) 50% 50% / 100% 100% no-repeat;
  mask: var(--cut) 50% 50% / 100% 100% no-repeat;
  mask-mode: alpha;
}
.door.is-playing .loop { opacity: 1; }

/* the opening is a hole in a wall, not a picture stuck on one: the light
   falls off at its edges and the lintel throws a shadow down the inside */
.opening::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background:
    radial-gradient(122% 92% at 50% 34%, transparent 34%, oklch(6% 0.01 45 / 0.5) 100%),
    linear-gradient(180deg, oklch(5% 0.01 40 / 0.62), transparent 20%, transparent 74%, oklch(5% 0.01 40 / 0.42));
  -webkit-mask: var(--cut) 50% 50% / 100% 100% no-repeat;
  mask: var(--cut) 50% 50% / 100% 100% no-repeat;
  mask-mode: alpha;
}

/* the near door's own light. A lantern out on the pavement catches the
   threshold and the jambs; it does not tint the room behind them, so this is a
   rim in the lane's own flame rather than a wash of the door's accent over the
   project's art. */
.opening::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 520ms ease;
  background: radial-gradient(124% 104% at 50% 100%,
    transparent 60%, color-mix(in oklch, var(--flame) 42%, transparent) 100%);
  -webkit-mask: var(--cut) 50% 50% / 100% 100% no-repeat;
  mask: var(--cut) 50% 50% / 100% 100% no-repeat;
  mask-mode: alpha;
}
.door.is-near .opening::before { opacity: 0.72; }
.door.is-lit .opening::before  { opacity: 1; }

/* the chain across a door that is built but has no address yet */
.chain {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  -webkit-mask: var(--cut) 50% 50% / 100% 100% no-repeat;
  mask: var(--cut) 50% 50% / 100% 100% no-repeat;
  mask-mode: alpha;
}
/* One chain of iron links at chest height, hung slightly off level, with a
   padlock where it crosses. Two straight bars of alternating light and dark
   read as hazard tape, which is a sign about the page rather than a thing in
   the street; a row of drawn links reads as iron. Nothing here goes above
   oklch 62%, so the chain stays metal in a dark doorway and never becomes the
   brightest thing in the opening. */
/* Two rows of links on the same step, half a link out of phase: the wide ones
   lying flat and the narrow ones standing on edge between them, which is what
   a chain does. One row of separate rings reads as a line of eyelets. */
.chain::before {
  content: '';
  --link: calc(var(--lane-h) * 0.017);
  position: absolute;
  left: -6%;
  right: -6%;
  top: 54%;
  height: calc(var(--lane-h) * 0.016);
  rotate: -2.1deg;
  background:
    radial-gradient(58% 46% at 50% 50%,
      transparent 56%,
      oklch(56% 0.024 78 / 0.9) 66%,
      oklch(24% 0.014 60 / 0.95) 90%,
      transparent 100%)
      0 50% / var(--link) 100% repeat-x,
    radial-gradient(20% 52% at 50% 50%,
      transparent 44%,
      oklch(46% 0.02 74 / 0.85) 58%,
      oklch(17% 0.012 58 / 0.95) 92%,
      transparent 100%)
      calc(var(--link) / 2) 50% / var(--link) 100% repeat-x;
  filter: drop-shadow(0 calc(var(--lane-h) * 0.0035) calc(var(--lane-h) * 0.005) oklch(3% 0 0 / 0.9));
}
/* the padlock where the chain crosses, off centre so it falls inside the bar
   door rather than on the mullion of door 03 */
.chain::after {
  content: '';
  position: absolute;
  top: 51%;
  left: 74%;
  width: calc(var(--lane-h) * 0.021);
  height: calc(var(--lane-h) * 0.032);
  rotate: -2.1deg;
  background:
    radial-gradient(closest-side,
      transparent 52%,
      oklch(50% 0.022 78 / 0.95) 60%,
      oklch(24% 0.014 60 / 0.95) 84%,
      transparent 88%)
      50% 0 / 74% 58% no-repeat,
    linear-gradient(168deg, oklch(46% 0.024 78), oklch(18% 0.012 58))
      50% 100% / 100% 62% no-repeat;
  filter: drop-shadow(0 calc(var(--lane-h) * 0.003) calc(var(--lane-h) * 0.005) oklch(3% 0 0 / 0.85));
}

/* --------------------------------------------------------------- plates */

/* An enamelled sign screwed to the wall under its opening: flat paint, a thin
   bright rule set in from the edge, one hard shadow line where it meets the
   render. A gradient fill with a soft drop shadow and a rounded corner is a
   dark-theme card, and a card is the one thing that cannot hang on a painting.
   The rule is the lane's own flame on every door, so door 02's sign does not
   go violet while the other three stay brass. */
.plate {
  /* each sign is cut to its own door rather than four identical rectangles */
  --pw: clamp(calc(var(--lane-w) * 0.12), calc(var(--w) * var(--lane-w) * 1.34), calc(var(--lane-w) * 0.165));
  position: absolute;
  z-index: 8;
  top: calc(100% + var(--lane-h) * 0.016);
  left: calc(50% - var(--pw) / 2);
  width: var(--pw);
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  column-gap: 0.6em;
  row-gap: calc(var(--lane-h) * 0.006);
  padding: calc(var(--lane-h) * 0.014) calc(var(--lane-h) * 0.017) calc(var(--lane-h) * 0.017);
  border-radius: 1px;
  color: var(--cream);
  background: oklch(14% 0.016 50 / 0.88);
  border: 1px solid color-mix(in oklch, var(--flame) 34%, transparent);
  outline: 1px solid color-mix(in oklch, var(--flame) 13%, transparent);
  outline-offset: -5px;
  box-shadow: 0 1px 0 oklch(0% 0 0 / 0.55);
  transition: border-color 420ms ease, background 420ms ease;
  /* the note hangs off the whole plate, not off the status word, so it never
     prints over the name it belongs to */
  anchor-name: var(--an);
}
.door.is-near .plate {
  border-color: color-mix(in oklch, var(--flame) 62%, transparent);
  background: oklch(16% 0.02 52 / 0.9);
}

.ord {
  grid-column: 1;
  font-size: clamp(11px, calc(var(--lane-w) * 0.0049), 13px);
  font-variant-numeric: lining-nums tabular-nums;
  letter-spacing: 0.18em;
  color: color-mix(in oklch, var(--cream) 52%, var(--flame));
}
.name {
  grid-column: 1 / -1;
  font-size: clamp(1.05rem, calc(var(--lane-w) * 0.0158), 2.4rem);
  line-height: 1.04;
  letter-spacing: -0.012em;
  color: inherit;
  text-decoration: none;
  text-wrap: balance;
}
.door[data-key='tell'] .name { font-size: clamp(0.95rem, calc(var(--lane-w) * 0.0138), 2.1rem); letter-spacing: 0.03em; }
a.name[href] {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.16em;
  text-decoration-color: color-mix(in oklch, var(--accent) 75%, transparent);
}
a.name[href]:hover, a.name[href]:focus-visible { text-decoration-color: currentColor; }
.line {
  grid-column: 1 / -1;
  font-style: italic;
  font-size: clamp(0.72rem, calc(var(--lane-w) * 0.0069), 1.05rem);
  line-height: 1.34;
  color: var(--dim);
  text-wrap: pretty;
}
/* the note about how door 02's window was built, off the wall and down here
   where it can be read */
.line--small {
  font-style: normal;
  font-size: clamp(10px, calc(var(--lane-w) * 0.0052), 13px);
  letter-spacing: 0.03em;
  opacity: 0.78;
  margin-top: calc(var(--lane-h) * -0.002);
}

/* --------------------------------------------------------------- status */

.status {
  grid-row: 1;
  grid-column: 2;
  justify-self: end;
  display: inline-flex;
  align-items: baseline;
  gap: 0.5em;
  margin: 0;
  padding: 0;
  font: inherit;
  font-size: clamp(11px, calc(var(--lane-w) * 0.0049), 13px);
  letter-spacing: 0.17em;
  text-transform: uppercase;
  line-height: 1.5;
  text-align: right;
  color: var(--dim);
  background: none;
  border: 0;
  border-bottom: 1px solid transparent;
}
button.status { cursor: pointer; border-bottom-color: color-mix(in oklch, var(--cream) 26%, transparent); }
/* a line of capitals is a thin thing to aim at, on a laptop as much as on a
   phone, so the button reaches past its own text */
button.status { position: relative; }
button.status::before { content: ''; position: absolute; inset: -0.6rem -0.65rem; }
button.status:hover { color: var(--cream); border-bottom-color: currentColor; }
.status--live { color: color-mix(in oklch, var(--cream) 62%, var(--accent)); }

.dot {
  align-self: center;
  width: 0.46em; height: 0.46em;
  flex: none;
  border-radius: 50%;
  background: color-mix(in oklch, var(--accent) 85%, var(--cream));
  animation: breathe 2.6s ease-in-out infinite;
}
@keyframes breathe {
  0%, 100% { opacity: 0.5; box-shadow: 0 0 0 0 color-mix(in oklch, var(--accent) 50%, transparent); }
  50%      { opacity: 1;   box-shadow: 0 0 0 4px color-mix(in oklch, var(--accent) 0%, transparent); }
}

/* data-status alone decides which of the two status words is on the page */
.door[data-status='demoted'] .status--live,
.door:not([data-status='demoted']) .status--demoted { display: none; }
.door[data-status='demoted'] .status--demoted { display: inline-flex; }
.door[data-status='demoted'] a.name { text-decoration: none; pointer-events: none; cursor: default; }

/* ---------------------------------------------------------------- notes */

.note {
  width: min(28rem, calc(100vw - 2rem));
  margin: 0;
  padding: 0.95rem 1.1rem;
  font-size: 0.98rem;
  line-height: 1.45;
  text-wrap: pretty;
  /* literals first, for a browser with popover but without oklch tokens */
  color: #3a3128;
  color: oklch(29% 0.028 55);
  background: #efe6d3;
  background-color: oklch(93.5% 0.021 84);
  background-image: url(/paper-900.webp);
  background-size: cover;
  background-blend-mode: multiply;
  border: 1px solid #b8ad98;
  border: 1px solid oklch(62% 0.03 70);
  border-radius: 2px;
  box-shadow: 0 20px 46px -22px rgb(0 0 0 / 0.7);
  opacity: 0;
  translate: 0 -0.4rem;
  transition: opacity 180ms ease, translate 180ms ease, display 180ms allow-discrete, overlay 180ms allow-discrete;
}
.note::backdrop { background: rgb(8 6 4 / 0.36); }
.note:popover-open { opacity: 1; translate: 0 0; }
@starting-style { .note:popover-open { opacity: 0; translate: 0 -0.4rem; } }
@supports not selector(:popover-open) { .note { display: none; } }

.door[data-key='numen']       { --an: --a1; }
.door[data-key='relay']       { --an: --a2; }
.door[data-key='tell']        { --an: --a3; }
.door[data-key='study-rival'] { --an: --a4; }

@supports (anchor-name: --a) {
  .note {
    position-anchor: var(--an);
    position-area: block-end span-inline-start;
    position-try-fallbacks: flip-block, flip-inline;
    margin-block-start: 0.5rem;
  }
}
@supports not (anchor-name: --a) {
  .note { position: fixed; inset: auto; top: 50%; left: 50%; translate: -50% calc(-50% - 0.4rem); }
  .note:popover-open { translate: -50% -50%; }
  @starting-style { .note:popover-open { translate: -50% calc(-50% - 0.4rem); } }
}

/* ----------------------------------------------------------- wall signs */

/* The two group signs hang on the wall just above the lintel of their first
   door, which is the only place a sign can say which doors it means. The
   masthead owns the mouth of the lane, so they come up as it goes and they go
   out again with the wall at the far end. */
.sign {
  position: absolute;
  z-index: 10;
  left: calc(var(--sx) * var(--lane-w) + var(--shift));
  top: calc(var(--sy) * var(--lane-h) + var(--top));
  padding: 0.3em 0.9em 0.34em;
  font-size: clamp(11px, calc(var(--lane-w) * 0.0058), 15px);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: color-mix(in oklch, var(--cream) 84%, var(--flame));
  background: oklch(17% 0.022 55 / 0.9);
  border: 1px solid oklch(72% 0.03 76 / 0.42);
  outline: 1px solid oklch(72% 0.03 76 / 0.18);
  outline-offset: -4px;
  border-radius: 1px;
  box-shadow: 0 1px 0 oklch(0% 0 0 / 0.5);
  opacity: clamp(0, min(calc((var(--walk) - 0.1) * 9), calc(1 - (var(--walk) - 0.88) * 8)), 1);
}

/* 5. the light ------------------------------------------------------------ */

.light {
  z-index: 5;
  translate: var(--shift) var(--top);
  mix-blend-mode: screen;
}
.lantern, .pool, .steam { position: absolute; display: block; }

.lantern {
  left: calc(var(--lx) * 100%);
  top: calc(var(--ly) * 100%);
  width: calc(var(--lane-w) * 0.15);
  height: calc(var(--lane-w) * 0.15);
  translate: -50% -50%;
  background: radial-gradient(closest-side,
    color-mix(in oklch, var(--flame) 62%, transparent),
    color-mix(in oklch, var(--flame) 22%, transparent) 42%,
    transparent 72%);
  /* the flicker owns opacity, so the near-door lift is carried by size and
     brightness instead: a transition on opacity would never be seen */
  filter: brightness(0.92);
  transition: width 700ms ease, height 700ms ease, filter 700ms ease;
  animation: candle 4.3s linear(0, 0.62 11%, 0.4 21%, 0.94 37%, 0.58 51%, 1 67%, 0.72 83%, 0.88) infinite alternate;
}
.lantern--b { animation-duration: 5.2s; animation-delay: -1.7s; }

.pool {
  left: calc(var(--px) * 100%);
  top: 81.6%;
  width: calc(var(--lane-w) * 0.115);
  height: calc(var(--lane-h) * 0.26);
  translate: -50% -14%;
  background: radial-gradient(50% 50% at 50% 42%,
    color-mix(in oklch, var(--flame) 34%, transparent), transparent 70%);
  filter: brightness(0.72);
  transition: width 620ms ease, filter 620ms ease;
  animation: candle 5.1s linear(0, 0.7 14%, 0.48 26%, 0.96 41%, 0.64 57%, 1 72%, 0.8 88%, 0.92) infinite alternate;
}
.pool[data-for='relay']       { animation-delay: -1.1s; }
.pool[data-for='tell']        { animation-delay: -2.4s; }
.pool[data-for='study-rival'] { animation-delay: -3.3s; }

.steam {
  left: calc(var(--gx) * 100%);
  top: 74%;
  width: calc(var(--lane-w) * 0.035);
  height: calc(var(--lane-h) * 0.14);
  translate: -50% 0;
  background: radial-gradient(50% 50% at 50% 70%, oklch(78% 0.02 80 / 0.16), transparent 72%);
  filter: blur(6px);
  animation: steam 9s ease-in-out infinite;
}
@keyframes steam {
  0%   { opacity: 0; translate: -50% 6%; scale: 0.8 0.7; }
  35%  { opacity: 1; }
  100% { opacity: 0; translate: -54% -34%; scale: 1.3 1.5; }
}
@keyframes candle { from { opacity: 0.55; } to { opacity: 1; } }

/* the near door: its lantern warms and its pool widens */
.lane[data-near='numen'] .lantern--a,
.lane[data-near='relay'] .lantern--a,
.lane[data-near='tell'] .lantern--b,
.lane[data-near='study-rival'] .lantern--b {
  width: calc(var(--lane-w) * 0.2);
  height: calc(var(--lane-w) * 0.2);
  filter: brightness(1.3);
}

.lane[data-near='numen']       .pool[data-for='numen'],
.lane[data-near='relay']       .pool[data-for='relay'],
.lane[data-near='tell']        .pool[data-for='tell'],
.lane[data-near='study-rival'] .pool[data-for='study-rival'] {
  width: calc(var(--lane-w) * 0.17);
  filter: brightness(1.4);
}

/* a door that stopped answering goes dark; the opening stays lit so you can
   still look in */
.lane:has(.door[data-key='numen'][data-status='demoted']) .lantern--a { opacity: 0.22; animation: none; }
.lane:has(.door[data-key='numen'][data-status='demoted']) .pool[data-for='numen'] { opacity: 0.16; animation: none; }

/* 6. the foreground lamp post -------------------------------------------- */

.post {
  z-index: 6;
  translate: calc(var(--shift) * 1.25) var(--top);
}
.post img {
  position: absolute;
  left: calc(0.468 * var(--lane-w));
  bottom: calc(-0.055 * var(--lane-h));
  width: calc(0.0739 * var(--lane-w));
  height: auto;
  filter: drop-shadow(0 10px 26px oklch(2% 0 0 / 0.7));
}
.post::before, .post::after {
  content: '';
  position: absolute;
  mix-blend-mode: screen;
  pointer-events: none;
}
.post::before {
  left: calc(0.5049 * var(--lane-w));
  top: calc(0.1866 * var(--lane-h));
  width: calc(var(--lane-w) * 0.2);
  height: calc(var(--lane-w) * 0.2);
  translate: -50% -50%;
  background: radial-gradient(closest-side,
    color-mix(in oklch, var(--flame) 52%, transparent),
    color-mix(in oklch, var(--flame) 18%, transparent) 42%,
    transparent 72%);
  animation: candle 3.7s linear(0, 0.66 13%, 0.42 24%, 0.95 39%, 0.6 55%, 1 70%, 0.75 86%, 0.9) infinite alternate;
}
.post::after {
  left: calc(0.5049 * var(--lane-w));
  top: 86%;
  width: calc(var(--lane-w) * 0.15);
  height: calc(var(--lane-h) * 0.28);
  translate: -50% -30%;
  background: radial-gradient(50% 50% at 50% 40%,
    color-mix(in oklch, var(--flame) 30%, transparent), transparent 68%);
  animation: candle 4.9s linear(0, 0.7 14%, 0.5 27%, 0.96 42%, 0.66 58%, 1 73%, 0.82 89%, 0.94) infinite alternate;
}

/* 7. the rain ------------------------------------------------------------- */

.rain {
  position: absolute;
  inset: 0;
  z-index: 7;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 900ms ease;
}
.rain.on { opacity: 1; }

/* The end of the lane used to be a veil laid over the whole painting from walk
   0.9, which put a third of a stop of black over door 04 during the one clean
   look you ever get at it. The night the directory sits on now comes up from
   below instead, so the far end goes out as the ground arrives and nothing is
   dimmed while it is still the near door. */

/* 8. the masthead --------------------------------------------------------- */

.mast {
  position: fixed;
  z-index: 9;
  left: 0;
  top: 0;
  max-width: min(34rem, 46vw);
  /* Door 01 is the one door that opens, and it is directly under this column.
     The block is cut short so it clears the lit half of the doorway: the
     lantern, the red curtain and the floor of the tent are never behind type. */
  padding: clamp(2.4rem, 8vh, 4.4rem) var(--gut) clamp(1.6rem, 4vw, 3rem);
  pointer-events: none;
  opacity: clamp(0, calc(1 - var(--walk) * 4), 1);
}
/* legibility is carried on the letters themselves; the vignette only has to
   take the wall down a little, not black out a quarter of the painting */
.mast::before {
  content: '';
  position: absolute;
  inset: -18% -24% -10% -22%;
  z-index: -1;
  background:
    radial-gradient(58% 60% at 32% 44%, oklch(5% 0.008 45 / 0.7), oklch(5% 0.008 45 / 0.32) 50%, transparent 76%),
    linear-gradient(100deg, oklch(5% 0.008 45 / 0.5), oklch(5% 0.008 45 / 0.15) 46%, transparent 66%);
}
.wordline, .hero, .subline, .aside {
  text-shadow: 0 1px 2px oklch(3% 0 0 / 0.9), 0 0 18px oklch(3% 0 0 / 0.7);
}

.wordline {
  display: flex;
  align-items: baseline;
  margin: 0 0 0.1em;
  font-size: clamp(2.8rem, 6.4vw, 6.4rem);
  font-weight: 400;
  line-height: 0.9;
  letter-spacing: -0.018em;
}
.word { display: block; }

/* the brush stroke is painted in the page's own ink through a mask, so it
   carries the cream the word beside it carries */
.mark {
  position: relative;
  display: inline-block;
  line-height: 0;
  translate: 0 0.26em;
  margin-inline: -0.25em -0.16em;
}
.mark img { width: auto; height: 0.92em; display: block; visibility: hidden; }
.mark::after {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  -webkit-mask: url(/mark.svg) 50% 50% / contain no-repeat;
  mask: url(/mark.svg) 50% 50% / contain no-repeat;
}
.mark i { display: none; font-style: italic; line-height: 1; }
.mark.no-svg img { display: none; }
.mark.no-svg::after { content: none; }
.mark.no-svg i { display: inline-block; }

.hero, .subline {
  font-style: italic;
  font-size: clamp(1.05rem, 1.1vw + 0.5rem, 1.32rem);
  line-height: 1.36;
  max-width: 28ch;
  text-wrap: balance;
}
.hero { margin-bottom: 0.5rem; }
.subline { color: var(--dim); }
.subline [data-live-word] { text-transform: capitalize; }
.aside {
  margin-top: 0.9rem;
  font-size: 0.88rem;
  letter-spacing: 0.03em;
  color: var(--dim);
}

.corner {
  display: none;
  position: fixed;
  z-index: 10;
  top: 0.8rem;
  right: var(--gut);
  font-size: 0.85rem;
  font-style: italic;
  color: var(--dim);
}

/* --------------------------------------------------------------- beats */

.beats { position: relative; z-index: 0; }
.beat { height: 62vh; }
.beat--half { height: 40vh; }

/* ------------------------------------------------------------ directory */

.directory {
  position: relative;
  z-index: 2;
  padding: clamp(3rem, 9vh, 6rem) var(--gut) clamp(3.5rem, 12vh, 8rem);
  background: linear-gradient(180deg, oklch(9% 0.015 48), oklch(6% 0.01 40) 60%);
  border-top: 1px solid oklch(30% 0.02 60);
}
/* the wall runs on past the last door and goes out, so the ground the
   directory sits on is the same night the lane ends in */
.directory::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 64vh;
  pointer-events: none;
  background: linear-gradient(180deg, transparent, oklch(6% 0.01 40 / 0.72) 58%, oklch(6% 0.01 40));
}
.directory h2 {
  margin: 0 0 clamp(1.4rem, 3vh, 2.4rem);
  font-size: clamp(1.6rem, 3.4vw, 2.6rem);
  font-weight: 400;
  letter-spacing: -0.012em;
}
.directory ol {
  margin: 0;
  padding: 0;
  list-style: none;
  max-width: 44rem;
}
.directory li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.2rem;
  padding: 0.85rem 0;
  border-top: 1px solid oklch(28% 0.02 60);
}
.directory li:last-child { border-bottom: 1px solid oklch(28% 0.02 60); }
.directory .name { font-size: clamp(1.2rem, 3vw, 1.7rem); }
.directory a.name[href] { text-decoration-color: color-mix(in oklch, var(--flame) 70%, transparent); }
.chip {
  flex: none;
  font-size: 0.7rem;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--dim);
}
.chip--demoted { display: none; }
/* the edge stamps data-status on the directory row it closes, and lane.js does
   the same when it closes one after the page has loaded, so the swap is keyed
   to the row itself and stays true whatever else on the street is live */
.directory li[data-status='demoted'] .chip--live { display: none; }
.directory li[data-status='demoted'] .chip--demoted { display: inline; }

/* ------------------------------------------------------- walking in ---- */

.walkin {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  background: var(--ground, #0a0705);
  opacity: 0;
  transition: opacity 460ms ease;
}
body.leaving .walkin { opacity: 1; }
body.leaving .door.is-going .opening {
  scale: 1.45;
  opacity: 0.85;
  transition: scale 520ms cubic-bezier(0.4, 0, 0.2, 1), opacity 520ms ease;
}
body.leaving .mast, body.leaving .plate, body.leaving .sign { opacity: 0; transition: opacity 300ms ease; }

/* ============================================ the street, standing still

   Phones, portrait windows and anyone who asked for less motion get the same
   thing: the four sheets cut from the same painting, one per screen, in
   normal flow. No walk, no parallax, no rain. */

@media (max-width: 900px), (orientation: portrait), (prefers-reduced-motion: reduce) {

  .sky, .wall, .light, .post, .rain, .beats { display: none; }

  .lane {
    position: static;
    overflow: visible;
    background: var(--wall);
  }

  .mast {
    position: static;
    opacity: 1;
    max-width: none;
    /* the first screen ended on bare wall with the doorway below the fold, so
       the word gives back a screen-third and the lit top of door 01 arrives
       with it */
    min-height: 52svh;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: clamp(2rem, 8vh, 5rem) var(--gut) clamp(1.6rem, 5vh, 3rem);
  }
  .mast::before { inset: 0; background: none; }
  .wordline { font-size: clamp(3.4rem, 24vw, 9rem); }
  .hero, .subline { max-width: 26ch; }

  /* one column of sheets, the width of a phone held up, wherever the window
     happens to be: a 3 by 4 sheet stretched across a laptop stops being a
     street and becomes a smear of wall */
  .mast, .doors { max-width: 38rem; margin-inline: auto; }
  .doors { position: static; }

  .door {
    position: relative;
    left: auto;
    top: auto;
    width: auto;
    height: auto;
    min-height: 100svh;
    display: grid;
    grid-template-rows: minmax(0, 1fr) auto;
    background: var(--wall);
  }

  .opening {
    position: relative;
    inset: auto;
    overflow: clip;
    min-height: 46svh;
  }
  .opening .still {
    position: absolute;
    inset: 0;
    object-position: 50% 42%;
  }
  /* No loop on a sheet. The cut-out mask and the loop's box are both measured
     on the lane, where the opening is a small part of a wide painting; on a
     sheet they stretch to the whole 3 by 4 crop, so the tent played across the
     lintel and both open leaves of its own painted doorway. The sheet already
     carries that art, standing still. */
  .opening .loop,
  .door.is-playing .loop { display: none; }
  /* the sheet already carries the wall and the lantern, so the cut-out mask
     and the lintel shadow both stand down here */
  .opening::after {
    -webkit-mask: none;
    mask: none;
    background: linear-gradient(180deg, oklch(5% 0.01 40 / 0.5), transparent 26%, transparent 62%, oklch(6% 0.012 45 / 0.72));
  }
  .opening::before {
    -webkit-mask: none;
    mask: none;
    background: radial-gradient(58% 40% at 50% 22%, color-mix(in oklch, var(--flame) 55%, transparent), transparent 72%);
    opacity: 0;
    transition: opacity 700ms ease;
  }
  .door.is-near .opening::before { opacity: 0.34; }

  .chain, .door::before, .door[data-key='numen']::after { display: none; }

  .plate {
    position: relative;
    top: auto;
    left: auto;
    width: auto;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: none;
    padding: 1.2rem var(--gut) 2rem;
    column-gap: 1rem;
    row-gap: 0.4rem;
  }
  .door.is-near .plate { background: none; }
  .ord, .status { font-size: 0.68rem; }
  .name { font-size: clamp(2rem, 11vw, 3.2rem); }
  .door[data-key='tell'] .name { font-size: clamp(1.7rem, 9vw, 2.7rem); }
  .line { font-size: 1rem; margin-top: 0.2rem; }
  .status { grid-row: auto; grid-column: 1 / -1; justify-self: start; margin-top: 0.5rem; }
  /* a thumb needs more than a line of capitals */
  button.status { position: relative; }
  button.status::before { content: ''; position: absolute; inset: -0.7rem -0.75rem; }

  .sign {
    position: relative;
    left: auto;
    top: auto;
    z-index: 1;
    justify-self: start;
    margin: 0 var(--gut) -0.4rem;
    display: inline-block;
    font-size: 0.72rem;
    /* there is no walk here to fade them in with */
    opacity: 1;
  }

  .corner { position: fixed; }

  /* notes open as bottom sheets */
  .note {
    position: fixed;
    inset: auto 0 0 0;
    width: auto;
    max-height: 62dvh;
    overflow: auto;
    margin: 0;
    border: 0;
    border-top: 2px solid oklch(62% 0.03 70);
    border-radius: 10px 10px 0 0;
    padding: 1.15rem var(--gut) calc(1.15rem + env(safe-area-inset-bottom));
    font-size: 1.02rem;
  }
  @supports (anchor-name: --a) {
    .note { position-area: none; position-try-fallbacks: none; margin-block-start: 0; }
  }
  .note, .note:popover-open { translate: 0 0; }
  @starting-style { .note:popover-open { translate: 0 1rem; } }
}

/* the sheet's lantern warms as the sheet arrives */
@supports (animation-timeline: view()) {
  @media (max-width: 900px) and (prefers-reduced-motion: no-preference),
         (orientation: portrait) and (prefers-reduced-motion: no-preference) {
    .opening {
      view-timeline-name: --sheet;
      view-timeline-axis: block;
    }
    .opening::before {
      animation: warm linear both;
      animation-timeline: --sheet;
      animation-range: entry 20% cover 50%;
    }
    @keyframes warm { from { opacity: 0; } to { opacity: 0.34; } }
  }
}

/* ------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
  .corner { display: block; }
  .dot { animation: none; opacity: 0.9; }
  .lantern, .pool, .steam, .sky::after, .post::before, .post::after { animation: none; }
  .note { transition-duration: 0ms; }
  .opening .loop, .wall .ambient { display: none; }
  .opening::before { transition: none; }
  html { scroll-behavior: auto; }
}
