/* ============================================================================
   PORTFOLIO — 夜灯横丁 / Yato Yokocho
   A six-chapter night walk. Editorial layer over a live WebGL street.
   Palette: near-black, blue-charcoal, warm amber, bone white, vermilion.
   ========================================================================== */

:root{
  /* ---- palette ---- */
  --ink:        #05070a;
  --ink-2:      #080b11;
  --charcoal:   #0d1219;
  --charcoal-2: #141c26;
  --slate:      #2a3644;
  --bone:       #dfe7e0;
  --bone-dim:   #97a3a0;
  --amber:      #f0a54a;
  --amber-deep: #b4762f;
  --vermilion:  #e0231c;
  --moon:       #d9432f;
  --shoji:      #f4e2be;   /* lit paper — the back wall of the house */

  /* ---- type ---- */
  --sans: "Segoe UI Variable Display","Segoe UI",system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif;
  --jp:   "Yu Mincho","YuMincho","Hiragino Mincho ProN","Noto Serif JP","MS PMincho",serif;
  --jps:  "Yu Gothic","Hiragino Kaku Gothic ProN","Noto Sans JP","Meiryo","MS PGothic",sans-serif;
  --mono: ui-monospace,"Cascadia Mono","Segoe UI Mono","SF Mono","Roboto Mono",Menlo,monospace;

  /* ---- rhythm ---- */
  --gut:  clamp(20px, 5vw, 76px);
  --rail: 74px;              /* right chapter rail reserve */
  --navh: 66px;

  /* ---- motion ---- */
  --e-out:  cubic-bezier(.16,.84,.32,1);
  --e-io:   cubic-bezier(.62,.02,.2,1);
  --t-slow: 1.15s;
  --t-mid:  .72s;
  --t-fast: .34s;
}

*,*::before,*::after{ box-sizing:border-box; }

html{
  background:var(--ink);
  /* No page scrollbar. The chapter rail on the right is already the position
     indicator, and Lenis means the native bar is a readout rather than a
     control anyway. Scrolling itself is untouched — this hides the bar, it does
     not clip the overflow. The three inner panels hide their own further down. */
  scrollbar-width:none;
  -ms-overflow-style:none;
  /* No `scroll-behavior: smooth` here. Lenis animates the scroll position
     itself, and with the browser animating the same anchor jump the two fight
     over it. See assets/js/smooth.js. */
  -webkit-text-size-adjust:100%;
}

html::-webkit-scrollbar{ width:0; height:0; display:none; }

/* ---- Lenis (darkroomengineering/lenis, MIT) ---------------------------- */
html.lenis, html.lenis body{ height:auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped{ overflow:clip; }
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch],
.lenis [data-lenis-prevent-vertical],
.lenis [data-lenis-prevent-horizontal]{ overscroll-behavior:contain; }
.lenis.lenis-smooth iframe{ pointer-events:none; }
.lenis.lenis-autoToggle{
  transition-property:overflow; transition-duration:1ms;
  transition-behavior:allow-discrete;
}

body{
  margin:0;
  min-height:100%;
  font-family:var(--sans);
  font-weight:300;
  color:var(--bone);
  background:var(--ink);
  overflow-x:hidden;
  line-height:1.6;
  letter-spacing:.004em;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
body.menu-open{ overflow:hidden; }

::selection{ background:rgba(224,35,28,.34); color:#fff; }

/* ---------------------------------------------------------- the canvas */
#gl{
  position:fixed;
  inset:0;
  width:100%;
  height:100%;
  z-index:0;
  display:block;
  background:var(--ink);
}
/* a fixed grade sits over the canvas: deepens the corners, lifts the street */
.grade{
  position:fixed; inset:0; z-index:1; pointer-events:none;
  background:
    radial-gradient(132% 90% at 50% 46%, rgba(5,7,10,0) 44%, rgba(5,7,10,.4) 100%),
    linear-gradient(to bottom, rgba(5,7,10,.62) 0%, rgba(5,7,10,0) 20%, rgba(5,7,10,0) 76%, rgba(5,7,10,.5) 100%);
}
.no-webgl #gl{ display:none; }
.no-webgl .grade{
  background:
    radial-gradient(120% 80% at 50% 30%, rgba(217,67,47,.18) 0%, rgba(5,7,10,0) 62%),
    linear-gradient(to bottom, #0d1219 0%, #05070a 60%);
}

/* ------------------------------------------------------------- skip link */
.skip{
  position:fixed; top:10px; left:10px; z-index:120;
  padding:11px 16px;
  background:var(--bone); color:var(--ink);
  font:500 12px/1 var(--sans); letter-spacing:.18em; text-transform:uppercase;
  text-decoration:none; border-radius:2px;
  transform:translateY(-160%);
  transition:transform var(--t-fast) var(--e-out);
}
.skip:focus{ transform:translateY(0); }

/* readable to a screen reader, invisible to everyone else */
.sr-only{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;
}

/* ============================================================ typography */
.display{
  font-weight:500;
  line-height:.96;
  letter-spacing:-.022em;
  text-transform:uppercase;
  margin:0;
  text-wrap:balance;
}
.h-hero{ font-size:clamp(2.5rem, 6.6vw, 5.6rem); }
.h-sec { font-size:clamp(2.05rem, 5vw, 4.15rem); }
/* Same size as ABOUT, by construction rather than by coincidence: both read
   --word, so changing it moves the pair together. */
.h-word{ font-size:var(--word); line-height:.82; letter-spacing:-.045em; }

/**
 * CONTACT and SETTINGS, the third and fourth of the big words.
 *
 * Same corner and same ramp as ABOUT, and bone rather than paper-cream: ABOUT
 * takes its colour from the shoji it is written across, and there is no shoji
 * down here — this one lies over the street.
 */
/* The word lands exactly where `.foot` does — `margin-top:auto` puts that
   label on the bottom of the layer, which is the corner CONTACT now occupies.
   The layer gives up the band so the label sits above it rather than under it. */
#contact .layer{
  padding-bottom:calc(var(--word) * .82 + clamp(18px,4vh,58px) + 20px);
}

.ct-word,.st-word{
  position:absolute; z-index:9;
  left:clamp(14px,2.6vw,54px); bottom:clamp(18px,4vh,58px);
  margin:0; pointer-events:none;
  font-family:var(--sans);
  font-size:var(--word);
  font-weight:500;
  line-height:.82;
  letter-spacing:-.045em; text-transform:uppercase;
  color:var(--bone);
}

.jp-inline{
  font-family:var(--jp);
  font-size:.36em;
  font-weight:400;
  letter-spacing:.16em;
  color:var(--bone-dim);
  text-transform:none;
  margin-left:.7em;
  vertical-align:.34em;
  white-space:nowrap;
}

.label{
  font:500 clamp(10px,1.05vw,11.5px)/1.1 var(--sans);
  letter-spacing:.28em;
  text-transform:uppercase;
  color:var(--bone-dim);
  margin:0;
}
.label .dot{
  display:inline-block; width:5px; height:5px; border-radius:50%;
  background:var(--vermilion); margin-right:.9em; vertical-align:.14em;
}

.body{
  font-size:clamp(.98rem,1.12vw,1.1rem);
  font-weight:300;
  line-height:1.68;
  color:rgba(223,231,224,.74);
  max-width:34em;
  margin:0;
}
.lead{
  font-size:clamp(1.05rem,1.42vw,1.36rem);
  font-weight:300;
  line-height:1.58;
  color:rgba(223,231,224,.86);
  max-width:30em;
  margin:0;
}

.rule{
  height:1px; width:100%; border:0; margin:0;
  background:linear-gradient(to right, rgba(223,231,224,.26), rgba(223,231,224,.05) 62%, rgba(223,231,224,0));
}
.rule--short{ max-width:120px; background:linear-gradient(to right, rgba(224,35,28,.85), rgba(224,35,28,0)); }

.num{
  font:200 clamp(2.1rem,3.6vw,3.1rem)/1 var(--sans);
  letter-spacing:.03em;
  color:rgba(223,231,224,.3);
  font-variant-numeric:tabular-nums;
}

/* vertical Japanese display rail inside a chapter */
.jp-vert{
  font-family:var(--jp);
  font-weight:400;
  font-size:clamp(1.5rem,2.6vw,2.3rem);
  /* In vertical-rl with upright orientation, `letter-spacing` is the gap
     *between stacked glyphs* — so this is the rail's height knob. Each glyph
     advances 1em + this, which is why the long words (Welcome, Aerial) were
     running down into the chapter numbers. */
  letter-spacing:.12em;
  line-height:1;
  writing-mode:vertical-rl;
  text-orientation:upright;
  color:rgba(223,231,224,.5);
  white-space:nowrap;
  -webkit-user-select:none; user-select:none;
}

/* =============================================================== header */
.hdr{
  position:fixed; inset:0 0 auto 0; z-index:60;
  height:var(--navh);
  display:flex; align-items:center; gap:20px;
  padding:0 var(--gut);
  border-bottom:1px solid transparent;
  transition:background var(--t-mid) var(--e-out), border-color var(--t-mid) var(--e-out);
}
.hdr.solid{
  background:linear-gradient(to bottom, rgba(5,7,10,.94), rgba(5,7,10,.66) 70%, rgba(5,7,10,0));
  border-bottom-color:rgba(223,231,224,.08);
}

.brand{
  display:flex; align-items:center; gap:13px;
  text-decoration:none; color:inherit; flex:0 0 auto;
}
.brand svg{ width:26px; height:26px; display:block; flex:0 0 auto; }
.brand-tx{ display:flex; flex-direction:column; gap:3px; }
.brand-tx b{ font:500 12px/1 var(--sans); letter-spacing:.3em; color:var(--bone); }
.brand-tx i{
  font:400 9.5px/1 var(--sans); letter-spacing:.24em; font-style:normal;
  color:rgba(223,231,224,.42); text-transform:uppercase;
}

.nav{ margin-left:auto; display:flex; align-items:center; gap:clamp(14px,2vw,30px); }
.nav a{
  position:relative;
  display:flex; align-items:baseline; gap:.5em;
  font:500 11px/1 var(--sans); letter-spacing:.22em; text-transform:uppercase;
  color:rgba(223,231,224,.5); text-decoration:none;
  padding:8px 0;
  transition:color var(--t-fast) var(--e-out);
}
.nav a .jp{ font-family:var(--jps); font-size:10.5px; letter-spacing:.1em; opacity:.62; }
.nav a::after{
  content:""; position:absolute; left:0; right:0; bottom:0; height:1px;
  background:var(--vermilion); transform:scaleX(0); transform-origin:0 50%;
  transition:transform var(--t-mid) var(--e-out);
}
.nav a:hover, .nav a:focus-visible{ color:var(--bone); }
.nav a[aria-current="true"]{ color:var(--bone); }
.nav a[aria-current="true"]::after{ transform:scaleX(1); }

.burger{
  margin-left:auto; display:none;
  width:42px; height:42px; padding:0;
  background:none; border:0; cursor:pointer; color:var(--bone);
  align-items:center; justify-content:center;
}
.burger span{ display:block; width:24px; height:1px; background:currentColor; position:relative; transition:background var(--t-fast) linear; }
.burger span::before,.burger span::after{
  content:""; position:absolute; left:0; width:24px; height:1px; background:currentColor;
  transition:transform var(--t-mid) var(--e-out);
}
.burger span::before{ top:-7px; }
.burger span::after{ top:7px; }
body.menu-open .burger span{ background:transparent; }
body.menu-open .burger span::before{ transform:translateY(7px) rotate(45deg); }
body.menu-open .burger span::after{ transform:translateY(-7px) rotate(-45deg); }

/* --------------------------------------------------------- mobile menu */
.mnav{
  position:fixed; inset:0; z-index:55;
  background:var(--ink);
  display:flex; flex-direction:column; justify-content:center;
  padding:calc(var(--navh) + 24px) var(--gut) 40px;
  opacity:0; visibility:hidden;
  transition:opacity var(--t-mid) var(--e-out), visibility 0s linear var(--t-mid);
}
body.menu-open .mnav{ opacity:1; visibility:visible; transition-delay:0s; }
.mnav ol{ list-style:none; margin:0; padding:0; }
.mnav li{ border-top:1px solid rgba(223,231,224,.1); }
.mnav li:last-child{ border-bottom:1px solid rgba(223,231,224,.1); }
.mnav a{
  display:flex; align-items:center; gap:18px;
  padding:20px 0; text-decoration:none; color:var(--bone);
  opacity:0; transform:translateY(14px);
  transition:opacity var(--t-mid) var(--e-out), transform var(--t-mid) var(--e-out);
}
body.menu-open .mnav a{ opacity:1; transform:none; transition-delay:calc(var(--i,0) * 60ms + 90ms); }
.mnav .mn{ font:200 1.5rem/1 var(--sans); color:rgba(223,231,224,.32); font-variant-numeric:tabular-nums; }
.mnav .mt{ font:500 1.35rem/1.1 var(--sans); letter-spacing:.02em; text-transform:uppercase; }
.mnav .mj{ margin-left:auto; font-family:var(--jp); font-size:1.15rem; color:rgba(223,231,224,.66); letter-spacing:.1em; }

/* ========================================================= chapter rail */
.rail{
  position:fixed; z-index:50;
  right:calc(var(--gut) * .5); top:50%; transform:translateY(-50%);
  display:flex; flex-direction:column; gap:2px;
  padding:0; margin:0; list-style:none;
}
.rail li{ display:block; }
.rail a{
  display:flex; align-items:center; justify-content:flex-end; gap:10px;
  padding:7px 0; text-decoration:none;
  font:500 10px/1 var(--mono); letter-spacing:.14em;
  color:rgba(223,231,224,.3);
  transition:color var(--t-mid) var(--e-out);
}
.rail a .tick{
  display:block; width:16px; height:1px; background:currentColor;
  transition:width var(--t-mid) var(--e-out), background var(--t-mid) var(--e-out);
}
.rail a:hover,.rail a:focus-visible{ color:rgba(223,231,224,.72); }
.rail a[aria-current="true"]{ color:var(--bone); }
.rail a[aria-current="true"] .tick{ width:30px; background:var(--vermilion); }

/* ================================================================ main */
main{ position:relative; z-index:2; }

.chapter{ position:relative; min-height:100svh; }

/* the pinned foreground layer: arrives opaque, holds, then blurs away */
.layer{
  position:sticky; top:0;
  min-height:100svh;
  display:flex; flex-direction:column;
  padding:calc(var(--navh) + clamp(28px,5vh,60px)) var(--rail) clamp(34px,7vh,72px) var(--gut);
  opacity:var(--vis,1);
  filter:blur(calc(var(--blr,0) * 1px));
  transform:translate3d(0, calc(var(--sft,0) * 1px), 0);
  will-change:opacity, filter, transform;
}
.chapter[data-len="tall"]{ height:260svh; }
.chapter[data-len="std"] { height:220svh; }
.chapter[data-len="short"]{ height:180svh; }
/* the stretches of alley between the bars — long enough to be a walk, short
   enough that nobody wonders whether the page has stopped */
.chapter[data-len="walk"]{ height:140svh; }
/* The run-out under the footer.

   `min-height` has to be cleared, not just overridden: every .chapter carries a
   floor of 100svh, so any height below a screen is silently ignored and the
   section is a full viewport whatever this says. That floor is why an earlier
   attempt at this measured 944px on a 944px screen and looked like the rule had
   not been written at all.

   The number decides when the footer meets the camera. The move from the
   roofline to the close runs 135svh — from the aerial's release at 78% of its
   span to the moment this section's top arrives — and the footer's first pixel
   shows when this section's bottom reaches the fold, at `height - 100svh` past
   its top. At 55svh that lands about two thirds of the way through the move, so
   the footer is rising while the camera is still settling into the shot that
   was composed to be cropped by it. Bigger delays the footer; smaller brings it
   up under a frame that is still the roofline's. */
.chapter[data-len="out"]{ height:55svh; min-height:0; }

/* ---- HOW MUCH SCROLLING ONE TURN OF THE WALL TAKES ----------------------
   This number, and only this number.

   It moves the carousel and nothing else. Walking down the room and placing the
   heading cost a fixed 1.1 screens whatever this is set to — the same as any
   other chapter — so raising it does not stretch the walk in, it only slows the
   wall down.

     400svh  ->  0.19 screens of scroll per figure  — twitchy
     500svh  ->  0.32
     620svh  ->  0.49                               — current
     700svh  ->  0.60                               — very deliberate

   (A chapter holds between 10% and 78% of its own height; the walk takes the
   first 1.1 screens of that, and the wall's six turns split whatever is left —
   `((height/100 - 1) * 0.68 - 1.1) / 5` screens each. The walk itself is
   `WALK_SCREENS` in main.js.) */
#about{ height:620svh; }
/* On a phone every other chapter becomes ordinary flow content. Chapter 01
   cannot: its content *is* the wall turning, and a wall with no scroll to turn
   in is six of the seven biographies the reader never reaches. So this one
   keeps its pin, at a length that suits a thumb rather than a wheel. */
@media (max-width: 860px){
  #about{ height:560svh; }
  #about .layer{
    position:sticky; top:0; min-height:100svh;
    /* Chapter 01 keeps its pin here, and it has to keep its fade with it. The
       rule above sets `opacity:1` flat, because every other chapter at this
       width is ordinary flow content that should simply be read — but this one
       still walks a camera into a room, and the arrival gate that holds the
       biography back until it gets there writes to `--vis`. Flattened, the gate
       still computes and nothing consumes it: the words sit on the street
       outside the bar, which is exactly what they were built not to do. */
    opacity:var(--vis,1);
  }
  /* The chapter scrim runs left to right, which works when the reading column
     is a column. At this width it is the full page, so the far end of every
     line lands on lit shoji and the biography stops being readable. Turn the
     grade through ninety degrees and it sits behind the text instead. */
  #about .scrim{
    background:linear-gradient(to bottom,
      rgba(5,7,10,.95) 0%, rgba(5,7,10,.92) 46%,
      rgba(5,7,10,.72) 62%, rgba(5,7,10,.24) 78%, rgba(5,7,10,0) 92%);
  }
}

/* a soft grade behind reading columns — grading, not a glass panel */
.scrim{
  position:absolute; inset:0; z-index:-1; pointer-events:none;
  background:linear-gradient(100deg, rgba(5,7,10,.93) 0%, rgba(5,7,10,.8) 30%, rgba(5,7,10,.5) 48%, rgba(5,7,10,.1) 70%, rgba(5,7,10,0) 88%);
}

.head{ display:flex; flex-direction:column; gap:clamp(14px,2.2vh,26px); max-width:min(100%, 46rem); }
.foot{ margin-top:auto; }

/* the vertical Japanese rail pinned to the right of a chapter */
.side{
  position:absolute; z-index:2;
  top:calc(var(--navh) + 40px);
  right:clamp(14px,2.4vw,34px);
  pointer-events:none;
}

/* ============================================================== walks */
.layer--walk{
  justify-content:flex-end; gap:14px;
  /* held off the very bottom edge so the line has air under it */
  padding-bottom:clamp(52px, 12vh, 120px);
}
.walk-nx{
  margin:10px 0 0;
  display:flex; align-items:baseline; gap:.7em; flex-wrap:wrap;
  font:300 clamp(1.1rem,2.2vw,1.7rem)/1.2 var(--sans);
  color:rgba(223,231,224,.6);
}
.walk-nx b{ font-weight:500; color:var(--bone); letter-spacing:.01em; }
.walk-nx .jp{ font-family:var(--jp); font-size:.86em; color:rgba(223,231,224,.72); letter-spacing:.14em; }
.walk-nx .sd{
  font:500 10px/1 var(--sans); letter-spacing:.24em; text-transform:uppercase;
  color:rgba(223,231,224,.4); padding-left:.4em;
}

/* =========================================================== entrance */
.entrance .layer{ justify-content:space-between; }
.cue{
  display:inline-flex; align-items:center; gap:14px;
  font:500 10.5px/1 var(--sans); letter-spacing:.26em; text-transform:uppercase;
  color:rgba(223,231,224,.5);
}
.cue .track{ display:block; width:64px; height:1px; background:rgba(223,231,224,.2); position:relative; overflow:hidden; }
.cue .track i{ position:absolute; inset:0; background:var(--bone); transform-origin:0 50%; animation:cue 2.6s var(--e-io) infinite; }
@keyframes cue{
  0%  { transform:scaleX(0); }
  45% { transform:scaleX(1); }
  55% { transform:scaleX(1); transform-origin:100% 50%; }
  100%{ transform:scaleX(0); transform-origin:100% 50%; }
}

/* the four entry chips */
.chips{
  display:grid; gap:clamp(14px,2vw,26px) clamp(18px,3vw,44px);
  grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));
  max-width:56rem; margin-top:clamp(22px,3.4vh,38px);
}
.chip{ display:flex; gap:16px; align-items:flex-start; text-decoration:none; color:inherit; }
.chip .num{ font-size:clamp(1.7rem,2.6vw,2.3rem); flex:0 0 auto; transition:color var(--t-fast) var(--e-out); }
.chip b{
  display:block; font:500 12px/1.2 var(--sans); letter-spacing:.2em; text-transform:uppercase;
  color:var(--bone); margin-bottom:7px; transition:color var(--t-fast) var(--e-out);
}
.chip p{ margin:0; font-size:.86rem; line-height:1.5; color:rgba(223,231,224,.52); max-width:24em; }
.chip:hover b,.chip:focus-visible b{ color:var(--amber); }
.chip:hover .num{ color:rgba(223,231,224,.52); }

/* ============================================================== peek */
/* A live cut of the world. The frame is a hole in the page; the renderer
   paints the street into it. The play mark is centred on the FRAME only —
   the caption below sits outside its box. */
.peek{
  position:relative; display:block; text-decoration:none; color:inherit;
  width:min(38vw, 420px); min-width:236px;
}
.peek-fr{
  position:relative; display:block;
  aspect-ratio:16/10;
  border:1px solid rgba(223,231,224,.16);
  background:linear-gradient(180deg, rgba(13,18,25,.5), rgba(5,7,10,.5));
  overflow:hidden;
  transition:border-color var(--t-mid) var(--e-out);
}
.peek-fr::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(120% 100% at 50% 50%, rgba(5,7,10,0) 44%, rgba(5,7,10,.5) 100%);
}
.peek-play{
  position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
  width:52px; height:52px; border-radius:50%;
  border:1px solid rgba(223,231,224,.5);
  display:grid; place-items:center;
  background:rgba(5,7,10,.42);
  z-index:2;
  transition:transform var(--t-mid) var(--e-out), border-color var(--t-mid) var(--e-out), background var(--t-mid) var(--e-out);
}
.peek-play svg{ width:19px; height:19px; margin-left:2px; display:block; }
.peek:hover .peek-play,.peek:focus-visible .peek-play{ transform:translate(-50%,-50%) scale(1.09); border-color:var(--bone); background:rgba(5,7,10,.66); }
.peek:hover .peek-fr,.peek:focus-visible .peek-fr{ border-color:rgba(223,231,224,.42); }
.peek-cap{
  display:flex; align-items:baseline; gap:12px; padding-top:12px;
  font:500 10.5px/1.2 var(--sans); letter-spacing:.2em; text-transform:uppercase;
  color:rgba(223,231,224,.5);
}
.peek-cap .jp{ font-family:var(--jp); font-size:13px; letter-spacing:.14em; text-transform:none; color:rgba(223,231,224,.66); }
.peek.is-static .peek-fr{
  background:
    radial-gradient(80% 60% at 62% 30%, rgba(217,67,47,.26), rgba(5,7,10,0) 70%),
    linear-gradient(180deg, #101820, #05070a);
}

/* ============================================================== cards */
/**
 * PROJECTS sits in the bottom-left corner of the screen, the way ABOUT does,
 * and the list starts at the top. The word is allowed to cross the last cards.
 *
 * It is `sticky`, not `fixed`, and not `absolute`. Absolute would pin it to the
 * bottom of the layer — and this layer is taller than the screen, because the
 * card list is, so the word would sit under the last card instead of on the
 * screen. Fixed cannot help either: `.layer` carries a `filter` and a
 * `transform` for the chapter fades, and either of those makes it the
 * containing block for fixed descendants, so `position:fixed` here behaves
 * exactly like absolute. Sticky is measured against the scrollport rather than
 * the containing block, so it is the one that does what is wanted.
 *
 * The heading's wrapper does the sticking at `height:0`, so it takes no room in
 * the column and the list rises to the top; the heading itself hangs upward off
 * that zero-height line. `order` puts the wrapper last so there is something
 * for it to stick against, and `pointer-events:none` keeps the cards it crosses
 * clickable.
 */
#projects .layer{ padding-top:calc(var(--navh) + clamp(14px,2.4vh,30px)); }

/**
 * PROJECTS sits in the corner exactly as ABOUT does — same offsets, same
 * absolute positioning against the layer.
 *
 * This only works because the list above is capped. Uncapped, the layer grew
 * past the height of the screen and the bottom of the layer was somewhere below
 * the fold, which is where absolute positioning would have put the word. Sticky
 * was the workaround for that, and it had its own failure: sticky pulls an
 * element up when it would otherwise sit too low, and never pushes one down, so
 * with a short column above it the word simply stayed where the flow left it —
 * against the bottom of the list rather than the bottom of the screen. With the
 * list capped the layer is one screen tall again and the simple thing is right.
 */
#projects .head{
  position:absolute; z-index:9;
  left:clamp(14px,2.6vw,54px); bottom:clamp(18px,4vh,58px);
  height:0; pointer-events:none;
}
#h-projects{ position:absolute; left:0; bottom:0; margin:0; white-space:nowrap; }

/**
 * The list runs behind a fixed opening rather than down the whole page, so it
 * can never reach the word. The opening stops a little above PROJECTS; the list
 * slides up past it as the chapter scrolls, and is clipped at both ends.
 *
 * The card itself is untouched — same rules, same hairlines, same widths. All
 * that is new is the box it is seen through.
 */
#projects .cards-win{
  max-width:30rem;
  /* A starting value only. fitProjects() in main.js measures the word and
     writes the real one — see the note there for why this is not a calc(). */
  max-height:56svh;
  overflow:hidden;
}
#projects .cards{
  margin-top:0; max-width:none;
  /* driven from the scroll handler; see slideProjects in main.js */
  transform:translate3d(0, calc(var(--slide, 0) * -1px), 0);
  will-change:transform;
}

/* ---- the panel's own type and plates -------------------------------
   Width-agnostic, so they sit outside the breakpoint: the same panel is a
   column beside the list on a desktop and a full-screen sheet on a phone,
   and only its frame changes between the two. */
.pj-kicker{
  margin:0 0 clamp(10px,1.4vh,16px);
  font-family:var(--mono);
  font-size:10.5px; font-weight:500; line-height:1.4;
  letter-spacing:.16em; text-transform:uppercase;
  color:var(--bone-dim);
}
.pj-title{
  margin:0;
  font-family:var(--sans);
  font-size:clamp(1.85rem,3vw,3rem);
  font-weight:700; line-height:.95; letter-spacing:-.032em;
  color:var(--bone);
  text-wrap:balance;
}
.pj-spec{
  display:flex; flex-wrap:wrap; gap:0 clamp(22px,3vw,46px);
  margin:clamp(16px,2.2vh,26px) 0 0; padding:clamp(12px,1.6vh,18px) 0 0;
  border-top:1px solid rgba(223,231,224,.14);
}
.pj-spec div{ display:flex; flex-direction:column; gap:5px; }
.pj-spec dt{
  font-family:var(--mono);
  font-size:9.5px; font-weight:500; line-height:1;
  letter-spacing:.18em; text-transform:uppercase;
  color:var(--bone-dim);
}
.pj-spec dd{
  margin:0;
  font-family:var(--sans);
  font-size:clamp(.86rem,1.05vw,.98rem); font-weight:500; line-height:1.2;
  color:var(--bone);
}
.pj-para{
  margin:0 0 clamp(16px,2.2vh,26px);
  font-family:var(--jp);
  font-size:clamp(.95rem,1.12vw,1.1rem);
  line-height:1.72;
  color:rgba(223,231,224,.78);
  max-width:62ch;
}
.pj-src{
  color:var(--bone); text-decoration:none;
  border-bottom:1px solid rgba(223,231,224,.34);
  padding-bottom:1px;
  transition:border-color var(--t-mid) var(--e-out), color var(--t-mid) var(--e-out);
}
.pj-src:hover,.pj-src:focus-visible{ color:var(--amber-deep); border-color:var(--amber-deep); }
.pj-panel.is-out{ opacity:0; }
.pj-lede{
  margin:0 0 clamp(20px,2.8vh,32px);
  /* The reference sets its body in a serif against a sans headline, and this
     palette already owns one — the mincho used on the menus and sake labels
     in the street. Same contrast, no new typeface. */
  font-family:var(--jp);
  font-size:clamp(.95rem,1.12vw,1.1rem);
  line-height:1.72;
  color:rgba(223,231,224,.78);
  max-width:62ch;
}
.pj-lede::first-letter{
  float:left;
  font-size:3.2em; line-height:.8;
  margin:.06em .14em 0 0;
  color:var(--bone); font-weight:400;
}
.pj-plates{
  display:grid; grid-template-columns:1fr 1fr;
  gap:clamp(10px,1.1vw,16px);
  margin:0 0 clamp(10px,1.1vw,16px);
}
.pj-flow.is-portrait .pj-plates{ grid-template-columns:repeat(3, 1fr); }
.pj-plate{ margin:0 0 clamp(10px,1.1vw,16px); }
.pj-plates .pj-plate{ margin-bottom:0; }
.pj-plate img{
  display:block; width:100%; height:auto;
  /* One ratio per project, set from its own plates by renderPanel — see the
     note there. Held for the whole grid rather than per image, so the shots
     inside a set (1.55 to 1.79 in the landscape ones) do not stagger their
     captions. The fallback is the landscape frame it used to be fixed at. */
  aspect-ratio:var(--plate-ar, 16 / 9); object-fit:cover;
  background:var(--ink-2);
  border:1px solid rgba(223,231,224,.14);
}
.pj-plate figcaption{
  margin-top:7px;
  font-family:var(--mono);
  font-size:9px; font-weight:500; line-height:1.4;
  letter-spacing:.12em; text-transform:uppercase;
  color:var(--bone-dim);
}
.pj-panel{ transition:opacity 200ms var(--e-out); }

/* ==========================================================================
   THE PROJECT DETAIL PANEL

   Tablet and desktop only. Built from the reference screenshots, translated
   into this site's palette rather than copied out of it: the reference is a
   white editorial page, and a white panel beside a night street would fight
   everything around it. What carries over is the structure — corner rivets, a
   mono kicker, a heavy tight title, a serif body opening on a drop cap, and a
   figure grid of two-up then wide, every plate captioned in mono.

   The head holds still and only the body travels, so the name of the project
   being read never scrolls away from the reading.
   ========================================================================== */
.pj-panel{ display:none; }

@media (min-width: 861px){
  /* the chapter becomes two columns: the list, then the panel */
  #projects .layer{
    flex-direction:row; align-items:stretch;
    gap:clamp(28px,3.4vw,64px);
    /* A definite height, not just a minimum. The panel's body is a column of
       plates far taller than the screen, and against `min-height` alone it
       simply grew the layer to fit — no overflow, so nothing to clip and
       nothing to travel. One screen exactly gives the body something to be
       clipped against. */
    height:100svh;
  }
  #projects .cards-win{ flex:0 0 auto; }

  .pj-panel{
    position:relative;
    display:flex; flex-direction:column; min-width:0;
    flex:1 1 auto;
    padding:2px clamp(10px,1vw,18px) 0 0;
  }

  /* ---- the head, which does not move ---- */
  .pj-head{ flex:0 0 auto; padding:0 0 clamp(14px,1.8vh,22px); }

  /* Team and date, the two the brief asked for, plus what they were built in.
     A row of label-over-value, hairline above, which is the same reading
     pattern the settings rows use elsewhere on the page. */

  /* ---- the body, which does ---- */
  .pj-body{
    flex:1 1 auto; min-height:0;
    /* Its own scroller, so the wheel over the panel moves the panel and
       nothing else — until it runs out, at which point chainScroll() in main.js
       hands the wheel to Lenis and the page carries on.

       `auto`, not `contain`. Contain is what a scroller normally wants next to
       a smooth-scroll library, and it is what the vendor stylesheet sets for
       anything carrying a lenis-prevent attribute — but it also blocks the
       touch chain, and on a tablet that is the only chain there is. The wheel
       path does not need it: chainScroll cancels the native chain itself on the
       one tick where both could fire. */
    overflow-y:auto;
    scrollbar-width:none;
    /* A hard cut through a line of prose reads as a fault; the card list gets
       away with it because a card is a block and the cut lands between rules.
       Fading the two edges says "window" instead. The mask is static — it is
       the child that moves — so this is not the per-frame mask rasterisation
       that the About reveal had to get rid of. */
    /* The top fade means "there is more above". At rest there is not, and
       fading anyway put the first line of every project under it — the opening
       sentence arrived already half gone. It is zero until the body has
       actually been scrolled; `is-scrolled` is set from the scroll handler. */
    --fade-top:0px;
    -webkit-mask-image:linear-gradient(to bottom,
      transparent 0, #000 var(--fade-top), #000 calc(100% - 34px), transparent 100%);
    mask-image:linear-gradient(to bottom,
      transparent 0, #000 var(--fade-top), #000 calc(100% - 34px), transparent 100%);
  }
  .pj-body.is-scrolled{ --fade-top:26px; }
  .pj-body::-webkit-scrollbar{ display:none; }
  /* the chapter's scroll no longer drives either column at this width */
  .pj-flow{ transform:none; }

  #projects .cards-win{
    overflow-y:auto;
    scrollbar-width:none;
  }

  /* Out-specify the vendor rule, which would put `contain` back on both of
     these for carrying a lenis-prevent attribute. */
  .lenis #projects .cards-win,
  .lenis #projects .pj-body{ overscroll-behavior:auto; }
  #projects .cards-win::-webkit-scrollbar{ display:none; }
  #projects .cards{ transform:none; }

  /* The swap: out, exchange, in. Short enough that the panel does not feel
     like it reloads, long enough that the eye registers a change rather than
     a flicker of different words in the same place. */

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

  /* Three across for a portrait set. Two would run about 290 wide and 395 tall
     each, which is a full screen of panel for two images; three lands near 187
     by 254 and reads as the set of screens it is. */

  /* The chapter grade runs left to right, for a page that was a reading column
     on the left and street on the right. With the panel there, the right half
     needs ground of its own or the serif sits on lit signage. */
  #projects .scrim{
    background:
      linear-gradient(100deg,
        rgba(5,7,10,.96) 0%, rgba(5,7,10,.94) 34%,
        rgba(5,7,10,.9) 62%, rgba(5,7,10,.86) 88%, rgba(5,7,10,.83) 100%);
  }
}

/* ==========================================================================
   THE WALKS SPEAK

   The stretches of alley between the bars used to carry wayfinding — "Out into
   the alley", "Next: Kobo, on the left". They carry a thought now, typed out as
   the reader walks, the way a line lands in a visual novel: centred, one
   character at a time, and untyped again if they scroll back up.
   ========================================================================== */
.layer--walk{
  justify-content:center; align-items:center;
  /* --rail is a right-hand reserve for the vertical kanji, and a centred line
     has to have the same room either side or it sits off-centre. */
  padding-left:var(--rail); padding-right:var(--rail);
}

.say{
  margin:0; max-width:min(24ch, 88vw);
  text-align:center; text-wrap:balance;
  font-family:var(--sans);
  font-size:clamp(1.35rem,3.2vw,2.6rem);
  font-weight:300; line-height:1.34; letter-spacing:-.012em;
  color:var(--bone);
  /* it is a thought over a street, so it needs its own ground */
  text-shadow:0 2px 22px rgba(5,7,10,.9), 0 0 60px rgba(5,7,10,.75);
}

/* The caret sits on the baseline and blinks only while the line is unfinished —
   a cursor still winking after the sentence has landed reads as a hang. */
.say-cx{
  display:inline-block; width:.06em; height:1em;
  margin-left:.08em; vertical-align:-.12em;
  background:var(--bone); opacity:0;
}
.say.is-typing .say-cx{ opacity:1; animation:say-blink 1s steps(1,end) infinite; }
@keyframes say-blink{ 0%,50%{ opacity:1 } 50.01%,100%{ opacity:0 } }

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

/* ==========================================================================
   CONTACT, CENTRED  (tablet and desktop)

   Every other chapter reads as a column down the left with the street to the
   right of it. This one is the end of the walk and has nothing to sit beside,
   so it takes the middle of the frame instead and the type goes up with it.

   The rows stay rows — label, value, action — because centring their columns
   turns a table into three stacked fragments and the eye loses which value
   belongs to which label. What moves is the block, not the row.
   ========================================================================== */
@media (min-width: 861px){
  #contact .layer{
    align-items:center; justify-content:center;
    /* the rail is a right-hand reserve for the vertical kanji; a centred column
       has to have the same room on both sides or it sits off-centre */
    padding-left:var(--rail); padding-right:var(--rail);
  }
  #contact .head{ align-items:center; text-align:center; max-width:46rem; }
  #contact .h-sec{ font-size:clamp(2.9rem,6.2vw,5.4rem); }
  #contact .head .body{
    font-size:clamp(1.02rem,1.35vw,1.24rem);
    max-width:34rem; margin-inline:auto;
  }

  #contact .contacts{ width:100%; max-width:38rem; }
  #contact .crow{
    grid-template-columns:minmax(120px,.32fr) 1fr auto;
    padding:clamp(20px,2.6vh,28px) clamp(14px,2vw,26px);
  }
  #contact .crow .k{ font-size:11px; }
  #contact .crow .v{ font-size:clamp(1.02rem,1.3vw,1.2rem); }

  #contact .foot{ margin-top:clamp(22px,3vh,38px); text-align:center; }

  /* The chapter grade runs left to right, for a column on the left. With the
     block in the middle it left the copy sitting on the lit end of the counter.
     A centred wash puts the ground under the reading instead, and keeps the
     room visible around it. */
  #contact .scrim{
    background:
      radial-gradient(86% 62% at 50% 44%,
        rgba(5,7,10,.90) 0%, rgba(5,7,10,.80) 44%,
        rgba(5,7,10,.42) 74%, rgba(5,7,10,.06) 100%),
      linear-gradient(180deg, rgba(5,7,10,.58) 0%, rgba(5,7,10,.10) 26%,
        rgba(5,7,10,.10) 74%, rgba(5,7,10,.58) 100%);
  }
}

/* ==========================================================================
   THE PANEL AS A SHEET  (phones)

   There is no room for two columns here, so the panel is not one. Press a card
   and the project drops down over the page, stopping under the header; the
   header is then the way back, because with the sheet covering everything it is
   the only thing left to press.
   ========================================================================== */
.pj-back{ display:none; }

@media (max-width: 860px){
  .pj-panel{
    /* laid out, but held off the top of the screen until it is asked for */
    display:flex; flex-direction:column;
    position:fixed; inset:var(--navh) 0 0 0; z-index:55;
    background:var(--ink);
    padding:clamp(18px,3vh,30px) var(--gut) 48px;
    overflow-y:auto; overscroll-behavior:contain;
    transform:translateY(-100%);
    visibility:hidden;
    transition:transform 320ms var(--e-out), visibility 0s linear 320ms;
  }
  body.pj-open .pj-panel{
    transform:none; visibility:visible;
    /* enters on ease-out and leaves faster, per the usual asymmetry */
    transition:transform 320ms var(--e-out), visibility 0s;
  }

  /* the sheet is the scroller here, so the body inside it is not */
  .pj-body{ overflow:visible; -webkit-mask-image:none; mask-image:none; }
  .pj-flow{ transform:none; }
  .pj-head{ padding-bottom:clamp(14px,2.2vh,22px); }
  /* two-up plates on a phone are thumbnails; one at a time reads */
  .pj-plates{ grid-template-columns:1fr; }
  .pj-flow.is-portrait .pj-plates{ grid-template-columns:1fr 1fr; }

  /* the bar swaps identity while the sheet is up */
  body.pj-open .hdr{
    /* It is transparent until the page is scrolled under it, which is fine over
       a street and wrong over a sheet — the list showed straight through the
       back control. */
    background:var(--ink);
    border-bottom-color:rgba(223,231,224,.12);
  }
  body.pj-open .brand{ display:none; }
  body.pj-open .pj-back{ display:inline-flex; }
  .pj-back{
    align-items:center; gap:9px;
    background:none; border:0; padding:0; margin:0;
    font-family:var(--mono);
    font-size:11px; font-weight:500; letter-spacing:.16em; text-transform:uppercase;
    color:var(--bone); cursor:pointer;
  }
  .pj-back .arw{ font-size:14px; transition:transform var(--t-mid) var(--e-out); }
  .pj-back:hover .arw,.pj-back:focus-visible .arw{ transform:translateX(-4px); }
}

@media (max-width: 860px) and (prefers-reduced-motion: reduce){
  .pj-panel,body.pj-open .pj-panel{ transition:visibility 0s; transform:none; }
}

/* Between 861 and about 1080 both columns are tight. The list gives up width
   first: it is a row of short lines and loses little, where the panel holds a
   paragraph and a two-up grid and turns unreadable much sooner. */
@media (min-width: 861px) and (max-width: 1080px){
  #projects .cards-win{ max-width:19rem; }
  .pj-plates{ grid-template-columns:1fr; }
  /* A portrait plate alone at this width is 504 by 686 — taller than the panel
     it sits in. Two across stays readable. */
  .pj-flow.is-portrait .pj-plates{ grid-template-columns:1fr 1fr; }
  /* Through this range the list is narrow enough that the panel begins before
     PROJECTS has finished — the word is sized against the screen, not against
     the column it sits under, and at 960px it reached about 90px into the
     panel. The panel gives up the band the word occupies. Padding on the body
     would not do it: overflow clips at the padding edge, so the plates would
     still paint across the word. Taking it off the panel shortens the body
     itself, which is what actually holds the content clear. */
  .pj-panel{
    padding-bottom:calc(var(--word) * .82 + clamp(18px,4vh,58px) + 18px);
  }
}

.cards{
  display:grid; gap:1px;
  background:rgba(223,231,224,.11);
  border-top:1px solid rgba(223,231,224,.11);
  border-bottom:1px solid rgba(223,231,224,.11);
  margin-top:clamp(20px,3vh,34px);
  max-width:30rem;
}
.card{
  display:grid; align-items:start; gap:6px 26px;
  grid-template-columns:auto 1fr auto;
  padding:clamp(18px,2.4vh,26px) clamp(4px,1.4vw,18px);
  background:var(--ink);
  text-decoration:none; color:inherit;
  transition:background var(--t-mid) var(--e-out);
}
.card:hover,.card:focus-visible{ background:var(--charcoal); }

/**
 * The card selects a project rather than leaving for its source.
 *
 * The control is the title, and its ::after covers the whole card — a heading
 * cannot legally live inside a button, and losing the heading would take the
 * card list out of the document outline for the sake of a bigger hit area.
 * This way the outline survives, there is one tab stop per card, and the whole
 * card is still clickable.
 */
.card{ position:relative; }
.card-hit{
  appearance:none; -webkit-appearance:none;
  background:none; border:0; margin:0; padding:0;
  font:inherit; color:inherit; letter-spacing:inherit; text-align:left;
  cursor:pointer;
}
.card-hit::after{ content:''; position:absolute; inset:0; }
.card-hit:focus{ outline:none; }
.card:has(.card-hit:focus-visible){
  background:var(--charcoal);
  outline:1px solid rgba(223,231,224,.5); outline-offset:-1px;
}

/* which project the panel is showing */
.card.is-on{ background:var(--charcoal); }
.card.is-on .num{ color:var(--amber-deep); }

.card .num{ font-size:clamp(1.2rem,1.9vw,1.55rem); grid-row:span 2; padding-top:.1em; }
.card h3{
  margin:0; font:500 clamp(1.02rem,1.7vw,1.3rem)/1.2 var(--sans);
  letter-spacing:.005em; color:var(--bone);
  display:flex; align-items:baseline; gap:.7em; flex-wrap:wrap;
}
.card h3 .jp{ font-family:var(--jp); font-size:.76em; font-weight:400; color:rgba(223,231,224,.5); letter-spacing:.12em; }
.card p{ grid-column:2; margin:0; font-size:.9rem; line-height:1.6; color:rgba(223,231,224,.56); max-width:38em; }
.card .meta{
  grid-column:3; grid-row:1/span 2; align-self:center;
  display:flex; flex-direction:column; align-items:flex-end; gap:7px;
  font:500 10px/1 var(--mono); letter-spacing:.14em; text-transform:uppercase;
  color:rgba(223,231,224,.4); text-align:right; white-space:nowrap;
}
.card .meta em{ font-style:normal; color:var(--amber-deep); }
.card .arw{ display:inline-block; transition:transform var(--t-mid) var(--e-out); }
.card:hover .arw,.card:focus-visible .arw{ transform:translateX(5px); }
.card .tags{ grid-column:2; display:flex; flex-wrap:wrap; gap:8px 14px; margin-top:4px; }
.card .tags span{
  font:500 9.5px/1 var(--mono); letter-spacing:.16em; text-transform:uppercase;
  color:rgba(223,231,224,.38); padding:5px 9px; border:1px solid rgba(223,231,224,.13);
}

/* ------------------------------------------------------- contact rows */
/* The reading column for the rows. One number: narrow enough that the four
   read as a stack of labels rather than as a table, wide enough that an email
   address does not wrap. */
.contacts{
  display:grid; gap:1px; background:rgba(223,231,224,.11);
  border-top:1px solid rgba(223,231,224,.11); border-bottom:1px solid rgba(223,231,224,.11);
  margin-top:clamp(18px,3vh,32px);
  max-width:24rem;
}

.crow{
  display:grid; grid-template-columns:minmax(96px,.34fr) 1fr auto;
  gap:12px 26px; align-items:baseline;
  padding:clamp(16px,2.2vh,22px) clamp(4px,1.4vw,16px);
  background:var(--ink); text-decoration:none; color:inherit;
  transition:background var(--t-mid) var(--e-out);
}
a.crow:hover,a.crow:focus-visible{ background:var(--charcoal); }
.crow .k{ font:500 10px/1.2 var(--sans); letter-spacing:.24em; text-transform:uppercase; color:rgba(223,231,224,.42); margin:0; }
.crow .v{ font:400 clamp(.98rem,1.5vw,1.15rem)/1.35 var(--sans); color:var(--bone); word-break:break-word; margin:0; }
.crow .s{ font:500 9.5px/1 var(--mono); letter-spacing:.16em; text-transform:uppercase; color:rgba(223,231,224,.34); white-space:nowrap; }
.crow[data-state="pending"] .v{ color:rgba(223,231,224,.42); }
.crow[data-state="pending"] .s{ color:var(--amber-deep); }

/* ---------------------------------------------------------- settings */
/**
 * The settings run behind a fixed opening, exactly as the project list does.
 *
 * Seven rows is more than a screen once SETTINGS has taken its band along the
 * bottom, and letting them push the chapter taller would put the last of them
 * under the word. The height is measured rather than guessed — see fitWindow in
 * main.js for why a calc() will not do it — and the wheel is scoped to the box
 * while it has somewhere to go, then handed back to the page.
 */
/* A definite height, not a minimum. The window is measured from where the word
   sits, the word sits at the bottom of the layer, and with `min-height` the
   layer's own height then depends on the window — a loop that settled tall
   enough to push the heading off the top of the screen. One screen exactly
   breaks it: the word has a fixed place to be measured against. */
#settings .layer{
  height:100svh;
  /* With no heading above the rows, the chapter's usual top padding is a lot of
     empty ground. Pulling it in hands the space to the list. */
  padding-top:calc(var(--navh) + clamp(12px,2vh,26px));
}
/* Not `margin-top:auto`. Pushed to the bottom of the layer the button left a
   band of nothing between itself and the last row, which is space the list
   could have had — and fitWindow cannot claim it, because the button is not
   where the arithmetic thinks it is. */
#settings .foot{ margin-top:clamp(12px,1.6vh,20px); }

/* The second knob, and the one with real headroom. SET_TAIL trims the space
   below the list; this trims every row, so it buys about fourteen pixels a row
   rather than fourteen once. Eleven rows in, that is the difference between
   cutting at Leaves and cutting well past Film grain. */
#settings .srow{ padding-top:clamp(11px,1.5vh,15px); padding-bottom:clamp(11px,1.5vh,15px); }
#settings .srow .st span{ line-height:1.45; }

#settings .set-win{ max-width:52rem; }
#settings .set-win .settings{
  margin-top:0; max-width:none;
  /* driven from the scroll handler below 861px, where the wheel belongs to the
     page rather than to the box — the same handoff the project list makes */
  transform:translate3d(0, calc(var(--sslide, 0) * -1px), 0);
  will-change:transform;
}

/* Only where the chapter is a pinned frame with a word along the bottom. Below
   that the chapter is ordinary flow content and the rows simply run down the
   page, which is what a phone wants anyway. */
@media (min-width: 861px){
  #settings .set-win{
    max-height:52svh;        /* a starting value; fitWindow writes the real one */
    overflow-y:auto;
    scrollbar-width:none;
  }
  #settings .set-win::-webkit-scrollbar{ display:none; }
  #settings .set-win .settings{ transform:none; }
}

.settings{
  display:grid; gap:1px; background:rgba(223,231,224,.11);
  border-top:1px solid rgba(223,231,224,.11); border-bottom:1px solid rgba(223,231,224,.11);
  margin-top:clamp(18px,3vh,32px); max-width:52rem;
}
.srow{
  display:grid; grid-template-columns:1fr auto; gap:8px 24px; align-items:center;
  padding:clamp(14px,2vh,19px) clamp(4px,1.4vw,16px); background:var(--ink);
}
.srow .st b{ display:block; font:500 11px/1.2 var(--sans); letter-spacing:.22em; text-transform:uppercase; color:var(--bone); margin-bottom:5px; }
.srow .st span{ display:block; font-size:.82rem; line-height:1.5; color:rgba(223,231,224,.48); max-width:36em; }
.seg{ display:flex; border:1px solid rgba(223,231,224,.16); }
.seg button{
  appearance:none; background:none; border:0; cursor:pointer;
  padding:9px 15px; min-width:66px;
  font:500 10px/1 var(--sans); letter-spacing:.18em; text-transform:uppercase;
  color:rgba(223,231,224,.46);
  transition:color var(--t-fast) var(--e-out), background var(--t-fast) var(--e-out);
}
.seg button + button{ border-left:1px solid rgba(223,231,224,.16); }
.seg button:hover{ color:var(--bone); }
.seg button[aria-pressed="true"]{ background:var(--bone); color:var(--ink); }
/* The settings confirmation: in quickly, hold, then out on the slower side —
   an exit that snaps reads as the line being cut off rather than finished. */
#s-stat{
  /* Out of the flow. It is a line that shows for three quarters of a second and
     then goes, and while it reserved height the settings list above it lost a
     row to space that is empty almost all the time. */
  position:absolute; margin:0; left:0; top:calc(100% + 12px);
  opacity:0;
  transition:opacity 300ms var(--e-out);
  pointer-events:none;
}
#settings .foot{ position:relative; }
#s-stat.is-said{ opacity:1; transition-duration:130ms; }

.sreset{
  margin-top:16px; appearance:none; background:none; cursor:pointer;
  border:1px solid rgba(223,231,224,.18); color:rgba(223,231,224,.6);
  padding:11px 20px; font:500 10px/1 var(--sans); letter-spacing:.2em; text-transform:uppercase;
  align-self:flex-start;
  transition:color var(--t-fast) var(--e-out), border-color var(--t-fast) var(--e-out);
}
.sreset:hover{ color:var(--bone); border-color:rgba(223,231,224,.45); }

/* -------------------------------------------------------- bird's eye */
/* No scrim up here. Every other chapter grades the street down so a reading
   column can sit on it — but the roofline shot has nothing to read: one button
   with a ground of its own, and the rail, which lives past the far end of the
   gradient anyway. The wash was covering the only thing this chapter is for. */
.aerial .scrim{ display:none; }
/* ...which the cue was quietly standing on. It ships with `background:none`
   because every other place it appears has a graded column under it. Here it is
   on bare roofline, so it carries its own — a pill, not a page. */
.aerial .roam-cue{ background:rgba(5,7,10,.72); backdrop-filter:blur(3px); }
.aerial .roam-cue:hover,
.aerial .roam-cue:focus-visible{ background:rgba(5,7,10,.82); }
.aerial .layer{ justify-content:center; align-items:flex-start; }
.plan{
  display:grid; gap:1px; background:rgba(223,231,224,.11);
  max-width:60rem; margin-top:clamp(20px,3vh,32px);
  grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
}
.plan div{ background:var(--ink); padding:clamp(15px,2vh,20px) clamp(6px,1.2vw,16px); }
.plan dt{ font:500 9.5px/1 var(--mono); letter-spacing:.18em; text-transform:uppercase; color:rgba(223,231,224,.38); margin:0 0 9px; }
.plan dd{ margin:0; font:300 clamp(1.05rem,1.7vw,1.3rem)/1.2 var(--sans); color:var(--bone); }
.plan dd .jp{ font-family:var(--jp); font-size:.72em; color:rgba(223,231,224,.5); margin-left:.5em; }

/* ============================================================== footer */
.ftr{
  position:relative; z-index:2;
  background:var(--ink);
  border-top:1px solid rgba(223,231,224,.11);
  padding:clamp(40px,7vh,84px) var(--gut) clamp(28px,4vh,44px);
  display:grid; gap:clamp(24px,4vh,44px);
}
.ftr-top{ display:flex; flex-wrap:wrap; gap:clamp(20px,4vw,64px); align-items:flex-start; justify-content:space-between; }
.ftr-lg{ font:500 clamp(1.6rem,4.4vw,3rem)/1 var(--sans); letter-spacing:.12em; text-transform:uppercase; color:rgba(223,231,224,.9); margin:0; }
.ftr-jp{ font-family:var(--jp); font-size:clamp(.95rem,1.6vw,1.2rem); letter-spacing:.3em; color:rgba(223,231,224,.44); margin:12px 0 0; }
.ftr-links{ display:flex; flex-wrap:wrap; gap:10px 30px; margin:0; padding:0; list-style:none; }
.ftr-links a{
  font:500 10.5px/1 var(--sans); letter-spacing:.22em; text-transform:uppercase;
  color:rgba(223,231,224,.52); text-decoration:none;
  transition:color var(--t-fast) var(--e-out);
}
.ftr-links a:hover,.ftr-links a:focus-visible{ color:var(--bone); }
.ftr-btm{
  display:flex; flex-wrap:wrap; gap:10px 26px; justify-content:space-between; align-items:center;
  padding-top:clamp(18px,3vh,28px); border-top:1px solid rgba(223,231,224,.09);
}
.ftr-btm small{ font:400 10.5px/1.5 var(--mono); letter-spacing:.12em; color:rgba(223,231,224,.34); text-transform:uppercase; }

/* ======================================================= word reveal */
.mask-line{ display:block; overflow:hidden; }
/* The display faces are set at a line-height below 1, so the inline box a mask
   inherits is shorter than the glyphs in it and overflow:hidden slices the
   ascenders and descenders off. The padding gives the mask the room the type
   actually needs; the equal negative margin keeps the line rhythm unchanged. */
.word-mask{
  display:inline-block; overflow:hidden; vertical-align:top;
  padding:.16em 0 .26em; margin:-.16em 0 -.26em;
}
.word{
  display:inline-block;
  transform:translateY(120%);
  transition:transform 1.02s var(--e-out);
  transition-delay:var(--word-delay,0ms);
}
.word-reveal.is-in .word{ transform:translateY(0); }

/* generic staged reveal */
[data-rv]{
  opacity:0;
  transition:opacity var(--t-slow) var(--e-out), transform var(--t-slow) var(--e-out), filter var(--t-slow) var(--e-out);
  transition-delay:calc(var(--rvd,0) * 1ms);
}
[data-rv="up"]  { transform:translateY(22px); }
[data-rv="left"]{ transform:translateX(-18px); }
[data-rv="blur"]{ filter:blur(9px); }
[data-rv].is-in{ opacity:1; transform:none; filter:none; }

/* =========================================================== cursor */
.cur, .cur-dot{
  position:fixed; top:0; left:0; z-index:200; pointer-events:none;
  border-radius:50%; opacity:0;
  will-change:transform;
  transition:opacity .3s linear, width .3s var(--e-out), height .3s var(--e-out),
             background .3s var(--e-out), border-color .3s var(--e-out), margin .3s var(--e-out);
}
.cur{ width:34px; height:34px; border:1px solid rgba(223,231,224,.5); margin:-17px 0 0 -17px; }
.cur-dot{ width:4px; height:4px; background:var(--bone); margin:-2px 0 0 -2px; }
body.cursor-on .cur, body.cursor-on .cur-dot{ opacity:1; }
body.cursor-on.cur-hot .cur{ width:56px; height:56px; margin:-28px 0 0 -28px; border-color:var(--vermilion); background:rgba(224,35,28,.08); }
body.cursor-on.cur-hot .cur-dot{ opacity:0; }
@media (hover:none), (pointer:coarse){ .cur,.cur-dot{ display:none; } }

/* The roofline shot turns under the pointer, and the cursor is the only thing
   that says so — the same grab/grabbing every 3D viewer on the web uses, showing
   up exactly when the gesture starts working. */
body.can-orbit .cur{ border-color:rgba(223,231,224,.62); }
body.can-orbit:not(.orbiting){ cursor:grab; }
body.orbiting{ cursor:grabbing; }
/* the drag runs over the chapter's own text, so nothing may select under it */
body.orbiting, body.orbiting *{ -webkit-user-select:none; user-select:none; }
body.orbiting .cur, body.orbiting .cur-dot{ opacity:0; }

/* ========================================================= preloader */
.pre{
  position:fixed; inset:0; z-index:150;
  background:var(--ink);
  display:grid; place-content:center; justify-items:center; gap:26px;
  padding:0 var(--gut);
  transition:opacity .9s var(--e-out), visibility 0s linear .9s;
}
.pre.gone{ opacity:0; visibility:hidden; pointer-events:none; }

/* -------------------------------------------------- the gate goes up ---
   Nothing here but the gate and the bar. The wordmark and the 44px icon that
   used to sit above it are gone: a loading screen that is already spelling out
   what it is doing does not also need to introduce itself, and with the gate
   being built there is something to watch instead of something to read. */
.pre-gate{
  position:relative;
  width:min(58vw, 236px);
  aspect-ratio:200 / 190;
  /* the pieces fall in from above and this is what they fall out of — a member
     translated up by more than its own height is simply not on screen yet */
  overflow:hidden;
}
.t-ground{
  position:absolute; left:4%; right:4%; top:92.9%; height:1px;
  background:rgba(223,231,224,.16);
}
.t-part{
  position:absolute; inset:0;
  /* off the top, until told otherwise. Set here as well as from JS so the
     first painted frame is already an empty site rather than a whole gate. */
  transform:translateY(-118%);
  /* A composited layer, which is the entire point of these being divs: the main
     thread is frozen for about two and a half seconds inside buildWorld and
     again inside the first shader compile, and a piece that animates on it
     stops dead in mid-air for the duration. */
  will-change:transform;
  /* Duration and delay are handed in per piece, per move — the bar arrives in a
     handful of large steps and a piece has to know how much of a step is its
     own. See paintTorii(). */
  transition:transform var(--td,.6s) cubic-bezier(.2,.62,.3,1) var(--tdl,0s);
}
.t-part svg{ position:absolute; inset:0; width:100%; height:100%; display:block; }
.t-part path{ fill:var(--vermilion); }

/* The kasagi catches the light and the shimagi sits in its shadow — which is
   both what happens under a real overhang and the only thing that stops the two
   from reading as one member. They go up as separate steps, so they have to
   look like separate timbers once they are up. */
#t-kasagi  path{ fill:#ec3a24; }
#t-shimagi path{ fill:#a81f14; }

@media (prefers-reduced-motion: reduce){
  .t-part{ transition:none; }
}
.pre-bar{ width:min(72vw,510px); height:1px; background:rgba(223,231,224,.16); position:relative; overflow:hidden; }
.pre-bar i{ position:absolute; inset:0; background:var(--bone); transform:scaleX(0); transform-origin:0 50%; transition:transform .4s var(--e-out); }
.pre-ft{
  width:min(72vw,510px); display:flex; justify-content:space-between; gap:20px;
  font:500 10px/1 var(--sans); letter-spacing:.22em; text-transform:uppercase; color:rgba(223,231,224,.4);
}


/* ============================================== the card on the counter */
/* the card is a 3D object, so the only thing the page can do about hovering
   it is change the cursor */
body.menu-hot{ cursor:pointer; }
body.cursor-on.menu-hot .cur{
  width:64px; height:64px; margin:-32px 0 0 -32px;
  border-color:var(--amber); background:rgba(240,165,74,.10);
}
body.cursor-on.menu-hot .cur-dot{ opacity:0; }

.menu-cue, .roam-cue{
  display:inline-flex; align-items:center; gap:16px;
  background:none; border:1px solid rgba(223,231,224,.18);
  padding:16px 22px; cursor:pointer; color:inherit; text-align:left;
  align-self:flex-start;
  transition:border-color var(--t-mid) var(--e-out), background var(--t-mid) var(--e-out);
}
.menu-cue:hover, .roam-cue:hover,
.menu-cue:focus-visible, .roam-cue:focus-visible{
  border-color:rgba(223,231,224,.5); background:rgba(223,231,224,.04);
}
.mc-mark, .rc-mark{ font-size:1.1rem; color:var(--vermilion); }
.mc-tx, .rc-tx{ display:flex; flex-direction:column; gap:6px; }
.mc-tx b, .rc-tx b{
  font:500 12px/1 var(--sans); letter-spacing:.22em; text-transform:uppercase; color:var(--bone);
}
.mc-tx i, .rc-tx i{
  font:300 .84rem/1.3 var(--sans); font-style:normal; color:rgba(223,231,224,.5);
}
.roam-cue{ margin-bottom:clamp(20px,3vh,34px); }

/* ================================================ chapter 01 · about === */
/**
 * The About content is no longer an overlay you open — it is the back wall of
 * the room, and the camera walks to it. What is left in the DOM is the reading
 * matter, plus the word itself.
 */
/* ==========================================================================
   THE ABOUT / PROJECTS TYPE KNOBS — everything on this screen, in one place.

   --word        the size of the giant words, ABOUT and PROJECTS. One property
                 drives both, so they cannot drift apart. Overridden for phones
                 in the max-width:860px block near the end of this section.
   .ab-intro     how wide the reading column is allowed to get. Two numbers:
                 an absolute cap in rem, and a share of the viewport. Whichever
                 is smaller wins, so the rem caps it on a big screen and the vw
                 keeps it off the edges on a small one.
   .ab-title     the character's name.
   .ab-bio       the paragraph under it.
   .ab-card      min-height reserves room for the tallest bio, so the wall does
                 not jump as the carousel turns. Raise it if a longer bio makes
                 the block twitch when it changes.
   ========================================================================== */
:root{ --word: clamp(2.4rem, 15vw, 6.5rem); }

.ab-intro{ max-width:min(23rem, 40vw); }

.ab-title{
  margin:0 0 clamp(10px,1.6vh,18px);
  font-family:var(--sans);
  font-size:clamp(2.2rem,4.4vw,3.7rem);
  font-weight:400;
  line-height:1.02;
  letter-spacing:-.025em;
  color:var(--bone);
}
/* the block that changes as the wall turns */
.ab-card{
  margin:0 0 clamp(14px,2vh,22px);
  min-height:12.5rem;
  transition:opacity var(--t-mid) var(--e-out), transform var(--t-mid) var(--e-out);
}
.ab-card.is-out{ opacity:0; transform:translateY(6px); }

.ab-bio{
  margin:0;
  font-family:var(--sans);
  font-size:clamp(1.02rem,1.35vw,1.22rem);
  font-weight:600;
  line-height:1.55;
  /* semi-bold carries more ink, so the dimmer grey it used at weight 300 now
     reads as muddy rather than quiet — brought up to match */
  color:rgba(223,231,224,.86);
}
.ab-hint{
  margin:0;
  font:400 10px/1.5 var(--mono); letter-spacing:.14em; text-transform:uppercase;
  color:rgba(223,231,224,.38);
}

/**
 * ABOUT, thrown across the screens at the end of the room.
 *
 * In front of everything — including the chapter furniture — and the silhouette
 * is done with colour alone: near-black at a little under full opacity, so the
 * paper it lies on comes through it. It is ordinary text, so the font, the
 * tracking and the weight are all yours to change. Deliberately still
 * selectable; only the blur and the stacking are doing any work.
 */
.ab-word{
  position:absolute; z-index:9;
  left:clamp(14px,2.6vw,54px); bottom:clamp(18px,4vh,58px);
  margin:0; pointer-events:none;
  font-family:var(--sans);
  font-size:var(--word);
  font-weight:500;
  line-height:.82;
  letter-spacing:-.045em; text-transform:uppercase;
  /* The colour of lit shoji paper, not ink.
     Set near-black it read as a silhouette only where it happened to cross a
     bright bay, and vanished everywhere else — over the dark timber and the
     tatami there was nothing to see it against. Taking the paper's own warm
     cream instead inverts that: it is legible across the whole frame, and where
     it does cross a lit bay it drops back into the screen it is written on. */
  color:var(--shoji);
}

@media (max-width: 860px){
  /* ABOUT hangs off the bottom of the screen here too.
     The pinned layer becomes ordinary flow content at this width, so there was
     no full-height box left to hang it from and it landed halfway up the page
     on top of the copy. Letting it flow instead put it wherever the copy
     happened to end. Giving chapter 01's layer a screen of height — exactly
     what `.entrance .layer` below already does, for the same reason — gives the
     word a bottom to sit on again, and nothing else has to change. */
  #about .layer{ min-height:100svh; }
  /* The list is capped now, so the layer fits a screen and can be pinned the
     way #about's is — which is what holds PROJECTS on the bottom edge for the
     whole chapter here as well. Pinning was the thing that used to put the last
     cards out of reach; it cannot any more, because they are reached by sliding
     the list behind its opening rather than by scrolling past it. */
  #projects .layer{ position:sticky; top:0; min-height:100svh; }
  /* Chapters collapse to content height here, which leaves this one with about
     ten pixels of scroll — nowhere near enough to move a list that is twice the
     height of its opening. It needs a length of its own, the same way chapter 01
     does, and for the same reason: the scrolling *is* the content. */
  #projects{ height:240svh; }

  /* Chapter 04 reads the same way: a pinned frame, the rows behind an opening,
     and enough chapter to travel in. */
  #settings{ height:210svh; }
  #settings .layer{ position:sticky; top:0; }
  #settings .set-win{ overflow:hidden; }

  /* CONTACT sits on the bottom edge the way the other two do, which means its
     layer has to be a pinned screen for the word to hang off — otherwise it
     lands at the end of the rows and travels away with them. The chapter then
     needs a length of its own for the pin to have anywhere to go. */
  #contact{ height:200svh; }
  #contact .layer{ position:sticky; top:0; min-height:100svh; }
  #contact .ct-word,#settings .st-word{ bottom:clamp(10px,2.4vh,26px); }
  /* Line the two words up. ABOUT lifts off the bottom edge less at this width
     than the shared value does, and PROJECTS was sitting a clear step above it;
     they share a corner across chapters, so they should share the offset. */
  #projects .head{ bottom:clamp(10px,2.4vh,26px); }
  /* one knob, both words */
  :root{ --word: clamp(2rem,12.5vw,4rem); }
  .ab-word,.ct-word,.st-word{ bottom:clamp(10px,2.4vh,26px); }
  /* nothing to sit beside at this width, but it still should not run the full
     width of a phone — a line that long is tiring to read */
  .ab-intro{ max-width:min(24rem, 84vw); }
}

/* ==================================================== free roam ======== */
body.roaming main, body.roaming .hdr, body.roaming .rail,
body.roaming .ftr, body.roaming .grade{ opacity:0; pointer-events:none; }
body.roaming{ cursor:none; overflow:hidden; }
/* without pointer lock the look is a drag, so say so with the cursor */
body.roaming:not(.roam-drag){ cursor:grab; }
body.roaming.roam-drag{ cursor:grabbing; }

/* display:flex beats the hidden attribute's UA display:none, so say it again */
.roam-hud[hidden]{ display:none; }
.roam-hud{
  position:fixed; inset:auto 0 0 0; z-index:110;
  display:flex; align-items:center; justify-content:space-between;
  gap:20px; flex-wrap:wrap;
  padding:16px var(--gut);
  background:linear-gradient(to top, rgba(5,7,10,.92), rgba(5,7,10,0));
  pointer-events:none;
}
.rh-keys{ display:flex; gap:clamp(10px,2vw,26px); flex-wrap:wrap; }
.rh-keys span{
  font:400 10px/1.4 var(--mono); letter-spacing:.1em; text-transform:uppercase;
  color:rgba(223,231,224,.46);
}
.rh-keys b{ color:var(--bone); font-weight:500; }
.rh-pos{ margin:0; font:400 10px/1 var(--mono); letter-spacing:.14em; color:rgba(223,231,224,.34); }
.rh-exit{
  pointer-events:auto; cursor:pointer;
  background:none; border:1px solid rgba(223,231,224,.24); color:rgba(223,231,224,.72);
  padding:9px 15px; font:500 10px/1 var(--sans); letter-spacing:.2em; text-transform:uppercase;
}
.rh-exit:hover{ color:var(--bone); border-color:rgba(223,231,224,.55); }

/* ============================================================ focus */
:focus-visible{ outline:2px solid var(--amber); outline-offset:3px; border-radius:1px; }

/* ======================================================== responsive */
@media (max-width: 1080px){
  :root{ --rail:56px; }
  .rail a{ font-size:9px; }
}
@media (max-width: 860px){
  :root{ --rail:0px; --navh:60px; }
  .nav{ display:none; }
  .burger{ display:flex; }
  .rail{ display:none; }
  .side{ display:none; }
  .peek{ width:min(72vw, 380px); }
  .card{ grid-template-columns:auto 1fr; }
  .card .meta{
    grid-column:2; grid-row:auto; align-items:flex-start; text-align:left;
    flex-direction:row; flex-wrap:wrap; gap:12px; margin-top:8px;
  }
  .crow{ grid-template-columns:1fr; gap:6px; }
  .crow .s{ margin-top:2px; }
  .srow{ grid-template-columns:1fr; }
  /* Five options do not fit a phone at the desktop sizing: min-width 66px plus
     padding is about 480px of buttons against roughly 320px of row. Let them
     share the row equally instead of each demanding a width, and take the
     tracking and padding down to match. */
  /* Five options do not fit a phone at the desktop sizing: min-width 66px plus
     padding is about 480px of buttons against roughly 320px of row. Drop the
     minimum and tighten the tracking so each button takes only the width of its
     own label — the two-option rows keep their natural size rather than being
     stretched across the row, and the five-option one lands around 250px. */
  /* Free roam is WASD and a mouse to look. There is neither here, so the
     invitation is only an invitation to be disappointed. */
  .roam-cue{ display:none; }

  .seg{ justify-self:start; max-width:100%; }
  .seg button{
    min-width:0;
    padding:10px 8px;
    font-size:9px; letter-spacing:.08em;
    white-space:nowrap;
  }
  .chapter[data-len="tall"],.chapter[data-len="std"],.chapter[data-len="short"]{ height:auto; min-height:100svh; }
  /* a walk is scenery, not reading — it does not need a whole screen here */
  .chapter[data-len="walk"]{ height:auto; min-height:62svh; }
  .layer--walk{ padding-top:calc(var(--navh) + 20px); padding-bottom:34px; }
  .layer{
    position:relative; min-height:auto;
    /* --rail is zero here, so the right gutter has to come back explicitly */
    padding:calc(var(--navh) + 40px) var(--gut) 52px var(--gut);
    opacity:1; filter:none; transform:none;
    /* Not `transform` in the list. A promised transform makes this a containing
       block for anything `position:fixed` inside it, whether or not a transform
       is ever set — and the project sheet is fixed. Left in, the sheet hangs off
       a scrolled chapter instead of off the screen. Chapter 01 still fades here,
       so opacity stays; opacity alone does not do that. */
    will-change:opacity;
  }
  .chapter{ min-height:0; padding-bottom:6svh; }
  .entrance .layer{ min-height:100svh; }
}
@media (max-width: 460px){
  .chips{ grid-template-columns:1fr; }
  .plan{ grid-template-columns:1fr 1fr; }
  .h-hero{ font-size:clamp(2.1rem,10.4vw,3rem); }
  .h-sec{ font-size:clamp(1.85rem,9vw,2.6rem); }
  /* kept inline: as its own masked word it wraps to the next line on its own,
     and a block here would put a block box inside an inline-block mask */
  .jp-inline{ font-size:.42em; margin-left:.5em; vertical-align:.28em; }
}

/* ==================================================== reduced motion */
@media (prefers-reduced-motion: reduce){
  html{ scroll-behavior:auto; }
  *,*::before,*::after{
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important; scroll-behavior:auto !important;
  }
  .word{ transform:none; }
  [data-rv]{ opacity:1 !important; transform:none !important; filter:none !important; }
  .layer{ opacity:1 !important; filter:none !important; transform:none !important; }
  .cue .track i{ animation:none; transform:scaleX(1); }
}
/* the in-page Motion setting mirrors the OS preference */
body.reduced .word{ transform:none !important; }
body.reduced [data-rv]{ opacity:1 !important; transform:none !important; filter:none !important; }
body.reduced .layer{ opacity:1 !important; filter:none !important; transform:none !important; }
body.reduced .cue .track i{ animation:none; transform:scaleX(1); }

/* the peek frame becomes a hole in the page while its chapter holds the frame */
.peek-fr.is-live{ background:transparent; }

/* ==========================================================================
   THE BROWSER'S OWN BOTTOM BAR  (phones and small tablets)

   Safari and Chrome on a phone hide their toolbar as you scroll down and bring
   it back as you scroll up. The viewport grows and shrinks by its height —
   roughly 80 to 110px — while the page is being read.

   Every height on this page was written in `svh`, the *small* viewport: the
   screen with the bar showing. That is the right unit for the chapter lengths,
   because they set the scroll distance the camera is driven by and a document
   that changes height mid-scroll drags the camera with it. It is the wrong unit
   for anything that has to cover the screen or hang off its bottom edge. The
   moment the bar goes, a layer sized `100svh` is a hundred pixels shorter than
   what you can see, so:

     - PROJECTS and SETTINGS, which are absolutely positioned against the
       bottom of their layer, sit a hundred pixels too high;
     - the dark ground under the reading stops early, and the raw lit street
       shows underneath it as a bright band with a hard edge along the top —
       the gold line under the entrance was this, the chapter's own 6svh of
       bottom padding lying outside the layer that paints the ground.

   So: `dvh` for the boxes that must match the screen — it tracks the bar, and
   because only the pinned layer changes height and never the chapter, the
   document stays exactly as tall as it was and the camera never feels it. And a
   bleed on the ground itself, past the bottom of its layer by the chapter's
   padding plus the full height of the bar, so that even mid-animation — when
   `dvh` has not caught up yet — there is no frame where the street shows
   through the floor.

   `100lvh - 100svh` is the bar. Nothing else needs to know how tall it is.
   ========================================================================== */
@media (max-width: 860px){
  :root{ --bar: 0px; --bar: calc(100lvh - 100svh); }

  /* the ground reaches past its layer, and keeps reaching while the bar moves */
  .scrim{ bottom:calc(-6svh - var(--bar)); }

  /* Every layer that is a pinned screen rather than flow content. Two
     declarations, not one: a browser without `dvh` drops the second and keeps
     the behaviour it has today rather than losing its floor entirely. */
  .entrance .layer,
  #about .layer,
  #projects .layer,
  #contact .layer{ min-height:100svh; min-height:100dvh; }
  /* Chapter 04 pins too, and its word hangs off the bottom of the layer the
     same way — it just never had a height of its own to hang from. */
  #settings .layer{ min-height:100svh; min-height:100dvh; }
}

/* ==========================================================================
   THE SHORTCUTS, SMALLER  (phones)

   Four entries with a line of explanation each, at the size they are on a
   desktop, take over half the screen on a phone — and they sit under the one
   thing the entrance is actually for, which is the street. They are shortcuts;
   they should read as a list you can take in at a glance, not as four
   paragraphs. Same words, about half the height.
   ========================================================================== */
@media (max-width: 560px){
  .chips{
    gap:11px;
    margin-top:clamp(12px,1.8vh,20px);
  }
  .chip{ gap:12px; align-items:baseline; }
  .chip .num{ font-size:1.1rem; }
  .chip b{ font-size:10px; letter-spacing:.16em; margin-bottom:2px; }
  .chip p{ font-size:.74rem; line-height:1.38; max-width:30em; }
  .cue{ margin-bottom:2px; }
}

/* ==========================================================================
   THE ENTRANCE KEEPS ITS STREET

   Every other chapter is a column of reading and needs ground under it. This
   one is not: it is the street, and four shortcuts. The wash was grading a
   picture nobody was reading over, and taking the lanterns down with it.

   The type keeps its legibility the way the walks already do — a shadow of its
   own, carried by the words rather than painted across the whole frame. The
   fixed `.grade` still deepens the corners, so there is a floor under the foot
   of the screen without a lid over the middle of it.
   ========================================================================== */
.entrance .scrim{ display:none; }

.entrance .cue,
.entrance .chips{
  text-shadow:0 1px 14px rgba(5,7,10,.92), 0 0 34px rgba(5,7,10,.72);
}

/* ==========================================================================
   THE WALKS, WITHOUT AN EDGE  (phones)

   This is the gold bar, and it was never gold — it is the street, unwashed.

   Between the bars the layer is ordinary flow content at this width, so it is
   only as tall as the line inside it, while the chapter around it is 62svh.
   Measured on a 390x844 frame: the walk's ground ran to y=346 and the next
   chapter's began at y=608, leaving 262px of alley with nothing over it and a
   hard horizontal cut where the wash stopped. The cut lands across the lit half
   of the street — lanterns, signage, the wet road — so it reads as a bright
   amber rule ruled across the page. It shows between ABOUT and PROJECTS, and
   again between PROJECTS and CONTACT, because that is where the walks are.

   A flat wash with a cut at each end cannot be made to disappear, so it stops
   being flat: dark under the line, nothing at all by the time it reaches either
   end. There is no edge left to see. And because both ends are transparent the
   block reads shorter as well as softer, which is the other half of the ask —
   the line needs the glare taken off the signs behind it, not the alley blacked
   out. The words already carry their own shadow; see `.say`.

   Desktop is untouched: the layer is a pinned screen there, so its ground fills
   the frame and there is no cut to find.
   ========================================================================== */
@media (max-width: 860px){
  .layer--walk{
    padding-top:calc(var(--navh) + 8px);
    padding-bottom:20px;
  }
  .layer--walk .scrim{
    /* back to the layer's own box — the shared bleed is for grounds that have
       to meet the chapter below them, and this one has to meet nothing */
    bottom:0;
    background:linear-gradient(to bottom,
      rgba(5,7,10,0)   0%,
      rgba(5,7,10,.72) 32%,
      rgba(5,7,10,.72) 68%,
      rgba(5,7,10,0)   100%);
  }
}

/* ==========================================================================
   NO CHAPTER ARRIVES WITH A RULED LINE  (phones)

   Softening the walks moved the bar rather than removing it: the cut that was
   the walk's ground ending became the cut of the next chapter's ground
   beginning. Same artefact, one section further down. Every ground on a phone
   is a rectangle of near-black over a lit street, and a rectangle has four
   sides — so the fix has to be the edge itself, not the chapter it happens to
   belong to this time.

   Overhang plus a fade, rather than a fade alone. The ground is pushed out past
   its layer at both ends and then masked back in over exactly that overhang, so
   the fade lives entirely in the part that hangs off. While the chapter is
   pinned — which is the whole time it is being read — the soft part is off the
   top and bottom of the screen and what you see is as solid as it ever was. It
   is only during the handoff, when the edge actually crosses the frame, that
   there is anything to fade. A reader gets full contrast under the words and
   never a line ruled across the alley.

   Static, so it rasterises once: the mask travels with the element instead of
   being recomputed per frame. That is the distinction the About reveal ran
   into, and the same reason `.pj-body` is allowed one.
   ========================================================================== */
@media (max-width: 860px){
  :root{ --edge: calc(6svh + var(--bar)); }

  .scrim{
    top:calc(-1 * var(--edge));
    bottom:calc(-1 * var(--edge));
    -webkit-mask-image:linear-gradient(to bottom,
      transparent 0, #000 var(--edge),
      #000 calc(100% - var(--edge)), transparent 100%);
    mask-image:linear-gradient(to bottom,
      transparent 0, #000 var(--edge),
      #000 calc(100% - var(--edge)), transparent 100%);
  }

  /* The walks keep the treatment written for them above — their ground is a
     soft band around one line, not a screen with edges to hide. */
  .layer--walk .scrim{
    top:0; bottom:0;
    -webkit-mask-image:none; mask-image:none;
  }
}
