:root {
  --bg: #1d1d1f;
  --accent: #dc2626;
  --sans: Geist, "Segoe UI Variable", system-ui, sans-serif;
  /* type alphas (style guide: dim, nothing reads as bright white) */
  --t-big: rgba(255, 255, 255, .62);
  --t-body: rgba(255, 255, 255, .55);
  --t-label: rgba(255, 255, 255, .5);
  --t-faint: rgba(255, 255, 255, .38);
  --ease: cubic-bezier(.2, .7, .1, 1);
  /* safe-area insets, exposed for chapter DOM too */
  --sat: env(safe-area-inset-top, 0px);
  --sar: env(safe-area-inset-right, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
  --edge: 14px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  /* the grain layer lifts this to about #1d1d1f */
  background: #18181a;
  color: var(--t-big);
  overflow: hidden;
  overscroll-behavior: none;
}

body {
  position: fixed; inset: 0;
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;
  cursor: default;
}

button { font: inherit; color: inherit; -webkit-tap-highlight-color: transparent; }

#gl {
  position: fixed; inset: 0;
  width: 100%; height: 100%;
  display: block;
  z-index: 0;
  opacity: 0;
  transition: opacity .35s ease;
}
#gl.on { opacity: 1; }
/* (round 41) a wipe that waits for its scene: the old one recedes under a soft dark vignette (main.js sets its opacity) */
.sw-veil { position: fixed; inset: 0; z-index: 1; pointer-events: none; opacity: 0; background: radial-gradient(120% 95% at 50% 52%, rgba(24, 24, 26, .42), rgba(24, 24, 26, .84)); }

.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ---------- chapter DOM roots ---------- */
#scenes-ui { position: fixed; inset: 0; z-index: 2; pointer-events: none; }
#scenes-ui > .scene-ui { position: absolute; inset: 0; pointer-events: none; visibility: hidden; }
#scenes-ui > .scene-ui.on { visibility: visible; }
/* (round 32: in 01's lamp-lit room "All projects" sat on the light wall at about 3:1) while the room is on screen its top falls into a
   soft shade, as a ceiling does, under the scene's own labels: the top links read at 4.5:1 or more. 01's room is the one place the
   chrome sits on a light wall (its "Outside" button is up only there) */
#scenes-ui::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: calc(var(--sat) + 190px); pointer-events: none;
  background: linear-gradient(rgba(8, 8, 10, .56), rgba(8, 8, 10, .44) calc(var(--sat) + 104px), rgba(8, 8, 10, 0));
  opacity: 0; transition: opacity .8s ease;
}
body.in-chapter #scenes-ui:has(> .scene-ui.on .hc-out.on)::before { opacity: 1; }

/* ---------- the intro (the hanging line, the drip, the ring) and the entry gate: critical, so they are inlined in
   index.html (the page paints the gate before this file has loaded) ---------- */

/* (round 44) the wait for the crystal after the click (main.js earlyWait): a faint glint runs down the hanging line to the bead at its tip */
.intro-line::after {
  content: ""; position: absolute; left: -0.5px; top: 0; width: 2px; height: 28%;
  background: linear-gradient(rgba(235, 240, 248, 0), rgba(235, 240, 248, .85) 65%, rgba(235, 240, 248, 0));
  opacity: 0; pointer-events: none;
}
.intro.wait .intro-line { overflow: hidden; }
.intro.wait .intro-line::after { animation: line-glint 2.1s cubic-bezier(.5, 0, .7, 1) .2s infinite; }
@keyframes line-glint {
  0% { transform: translateY(-100%); opacity: 0; }
  20% { opacity: 1; }
  75% { opacity: .9; }
  100% { transform: translateY(257%); opacity: 0; }
}

/* ---------- the short lines ---------- */
.say {
  position: fixed; left: 50%; top: 50%; z-index: 5;
  width: min(86vw, 860px);
  margin: 0;
  transform: translate(-50%, -50%);
  text-align: center;
  font-weight: 300;
  font-size: clamp(22px, 2.3vw, 34px);
  line-height: 1.25;
  letter-spacing: -0.005em;
  color: var(--t-big);
  text-wrap: balance;
  pointer-events: none;
  opacity: 0;
  /* the halo the site's home uses: the line reads over a lit facet (the crystal's facets also step back behind it)
     (round 14, the crystal's request: the night blue's own shadow, not charcoal, which read as a grey smudge on the new page) */
  text-shadow: 0 0 18px rgba(16, 16, 18, .92), 0 0 5px rgba(16, 16, 18, .85), 0 1px 2px rgba(16, 16, 18, .7);
  /* (loop3 core 2) its own layer: the line's fade (opacity and a lift, set every frame from main.js) is composited, not a repaint
     of the 18 px halo each frame ("Thanks for stopping by." fades in right after the ending's landing) */
  will-change: opacity, transform;
}

.say .nw { white-space: nowrap; }
/* (round 14 check) the Russian landing line is longer: on a desktop it gets the room to stay at two rows, clear of the hold label */
@media (min-width: 641px) and (min-height: 501px) { html[lang="ru"] .say { width: min(90vw, 960px); } }
/* the crystal's first line: one sentence a line wider than a phone (a phone balances it over three lines) */
@media (min-width: 641px) { .say .say-s { display: block; } }
/* (core-fix) a short landscape screen (a turned phone): the lines sit to the left of the crystal (larger there, main.js LAND_K),
   never across its middle; left-aligned in the left third, clear of a notch */
@media (max-height: 500px) and (orientation: landscape) and (min-aspect-ratio: 13/10) {
  .say { --say-x: 0px; left: calc(max(env(safe-area-inset-left, 0px), 16px) + 3vw); width: min(31vw, 320px); text-align: left; font-size: clamp(18px, 2.4vw, 24px); }
  .say .say-s { display: inline; }
}

/* ---------- chrome ---------- */
.chrome { position: fixed; inset: 0; z-index: 20; pointer-events: none; }
.chrome > * { opacity: 0; transition: opacity .9s ease; }
body.ready .chrome > * { opacity: 1; }
/* (round 10) and out of the keyboard's way until then: Tab at the gate and in the intro went first to three buttons no one could see */
body:not(.ready) .chrome > button { visibility: hidden; }
.chrome button {
  pointer-events: auto;
  background: none; border: 0; padding: 0;
  cursor: pointer;
  touch-action: manipulation;
}
/* (round 30, the review: the 1 px ring at .4 white measured 2.3:1 against 05's grey morning) a white hairline ring with a dark band inside it:
   it clears 3:1 on the light scenes and the dark ones (the transparent outline still draws in forced colours) */
.chrome button:focus-visible { outline: 1px solid transparent; outline-offset: 2px; box-shadow: 0 0 0 2px rgba(17, 17, 19, .55), 0 0 0 3px rgba(255, 255, 255, .88); }

/* top-left: the R monogram (the site's own: a letter in a ring) */
.mark {
  position: absolute;
  left: calc(var(--sal) + var(--edge)); top: calc(var(--sat) + var(--edge));
  height: 44px; min-width: 44px;
  display: flex; align-items: center; gap: 10px;
  padding: 0 6px !important;
  color: var(--t-label);
}
.mark-r {
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .2);
  font-size: 13px; font-weight: 500; line-height: 1;
  color: rgba(255, 255, 255, .55);
  transition: border-color .4s ease, color .4s ease;
}
/* hover only where a real pointer hovers: on touch, :hover sticks after a tap */
@media (hover: hover) {
.mark:hover .mark-r { border-color: rgba(220, 38, 38, .75); color: rgba(255, 255, 255, .72); }
}
.mark-back {
  font-size: 11px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase; /* (20 Oct: 11 px everywhere, was 10 on a desktop) */
  color: var(--t-label);
  opacity: 0; transform: translateX(-6px);
  transition: opacity .5s ease, transform .6s var(--ease);
}
body.in-chapter .mark-back { opacity: 1; transform: none; }
/* polish: outside a chapter the invisible "Back" label leaves the hit box, so focus draws a ring
   around the 44 px circle only (was a 94 x 50 rectangle) */
body:not(.in-chapter) .mark { border-radius: 50%; }
body:not(.in-chapter) .mark-back { position: absolute; left: 50px; pointer-events: none; }
.chrome .mark:focus-visible { outline-offset: 0; }

/* top-centre: VIEW ALL PROJECTS */
.vap {
  position: absolute; left: 50%; top: calc(var(--sat) + 70px);
  transform: translateX(-50%);
  height: 44px;
  padding: 0 8px !important;
  display: flex; align-items: center;
  font-size: 11px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase; /* (20 Oct: was 10.5) */
  color: var(--t-label);
  white-space: nowrap;
}
.vap span {
  padding: 4px 0 6px;
  background: linear-gradient(rgba(255, 255, 255, .34), rgba(255, 255, 255, .34)) 0 100% / 100% 1px no-repeat;
  transition: background-size .35s ease, color .3s ease;
}
/* hover only where a real pointer hovers: on touch, :hover sticks after a tap */
@media (hover: hover) {
.vap:hover span { background-size: 0% 1px; background-position: 100% 100%; color: rgba(255, 255, 255, .7); }
}

/* top-right: thin hamburger */
.burger {
  position: absolute;
  right: calc(var(--sar) + var(--edge)); top: calc(var(--sat) + var(--edge));
  width: 44px; height: 44px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
}
.burger i { display: block; width: 20px; height: 1px; background: rgba(255, 255, 255, .55); transition: width .35s var(--ease), background-color .3s; }
.burger i:nth-child(2) { width: 14px; margin-left: 6px; }
/* (round 13, L3 / L4: on 04's lit door glass the burger's strokes were 1.3:1 to the panel, and "View all projects" was lost on a bright
   desk edge in 01's close-up of Theo and on 03's last beat) a soft dark halo under the strokes and the letters: black on black on the
   dark scenes, a shadow that keeps them apart from a bright one */
.burger i { box-shadow: 0 0 3px 1px rgba(0, 0, 0, .34); }
.vap span { text-shadow: 0 0 3px rgba(0, 0, 0, .55), 0 0 9px rgba(0, 0, 0, .4); }
/* (round 20, the review's L6: "All projects" still went under the white desk edge in 01's room and the snowy roofs of 05) inside a
   scene the top links sit on a soft shade of their own, a blur of dark that fades out well before its edge (no box, no line), and
   their letters get a firmer halo. The crystal's grey start has none of it */
/* (round 25, the review's look-2: on 05's grey and warm day that shade read as a dark smudge about 120 x 50 px behind each link) no shade any
   more: the dark follows the letters and the strokes themselves, tight and soft (black on black on a dark scene, an outline on a bright one) */
body.in-chapter .chrome > .mark, body.in-chapter .chrome > .burger { isolation: isolate; }
/* (the letters solid, the grey .5 white makes on a dark scene a little lighter: a halo under translucent letters greys them too) */
body.in-chapter .chrome > .vap span, body.in-chapter .mark-back { color: #c2c2c4; text-shadow: 0 0 1px rgba(0, 0, 0, .9), 0 0 2px rgba(0, 0, 0, .75), 0 1px 4px rgba(0, 0, 0, .5), 0 0 10px rgba(0, 0, 0, .22); }
body.in-chapter .mark-r { color: #c2c2c4; border-color: rgba(255, 255, 255, .3); text-shadow: 0 0 1px rgba(0, 0, 0, .9), 0 0 3px rgba(0, 0, 0, .6); box-shadow: 0 0 0 1px rgba(0, 0, 0, .35), 0 0 4px rgba(0, 0, 0, .3), inset 0 0 0 1px rgba(0, 0, 0, .3); }
body.in-chapter .chrome > .vap span { background-image: linear-gradient(rgba(255, 255, 255, .45), rgba(255, 255, 255, .45)); }
body.in-chapter .chrome > .burger i { box-shadow: 0 0 0 1px rgba(0, 0, 0, .45), 0 0 4px 1px rgba(0, 0, 0, .3); }
/* hover only where a real pointer hovers: on touch, :hover sticks after a tap */
@media (hover: hover) {
.burger:hover i { background: rgba(255, 255, 255, .78); }
.burger:hover i:nth-child(2) { width: 20px; margin-left: 0; }
}

/* bottom-centre: CLICK & HOLD */
.hold {
  position: absolute; left: 50%; bottom: calc(var(--sab) + 54px);
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  transition: opacity .6s ease;
}
/* (20 Oct) 11 px on every screen (the reviews flagged the desktop's 10 px; phones already had 11); .2em keeps the width */
.hold-label, .counter-n, .counter-hint, .hold-next {
  font-size: 11px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase;
  white-space: nowrap;
}
.hold-label { color: var(--t-label); }
.hold-track { position: relative; width: 240px; height: 1px; background: rgba(255, 255, 255, .08); overflow: hidden; }
.hold-fill { position: absolute; inset: 0; background: rgba(255, 255, 255, .6); transform: scaleX(0); transform-origin: 50% 50%; }
/* (round 44) the press is answered at once: the track lights up under it (and lets go softly), and the fill's light grows as it runs
   out (main.js --hk, 0..1), a cool bloom at the full bar */
.hold-track { transition: background-color .35s ease; filter: drop-shadow(0 0 calc(1px + 5px * var(--hk, 0)) rgba(225, 235, 255, calc(.75 * var(--hk, 0)))); }
.hold.on .hold-track { background: rgba(255, 255, 255, .17); transition-duration: .06s; }
.hold-fill { background: rgba(255, 255, 255, calc(.55 + .45 * var(--hk, 0))); }
.hold-next { color: var(--t-faint); font-weight: 500; letter-spacing: .18em; min-height: 1em; }

/* (round 14, story-04) after "Thanks for stopping by.": the contact in one quiet row just over the hold's label, until the next hold
   (main.js body.end-contact); never in a chapter or the ending's fly-through */
.endrow {
  position: absolute; left: 50%; bottom: calc(var(--sab) + 54px + 56px); /* (round 15: 46 -> 56 px, the email's underline clear of the label) */
  transform: translateX(-50%);
  margin: 0; display: flex; align-items: center; gap: 12px; white-space: nowrap;
}
body .chrome > .endrow { opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .3s ease, visibility 0s linear .3s; }
body.ready.end-contact:not(.in-chapter):not(.warp-outro) .chrome > .endrow { opacity: 1; visibility: visible; pointer-events: auto; transition: opacity 1.2s ease 1.2s, visibility 0s; }
.endrow a { display: inline-flex; align-items: center; min-height: 44px; padding: 0 4px; color: var(--t-big); text-decoration: none; }
.endrow-mail { font-size: 13px; letter-spacing: .01em; }
.endrow-mail { background: linear-gradient(rgba(255, 255, 255, .26), rgba(255, 255, 255, .26)) 4px calc(50% + 10px) / calc(100% - 8px) 1px no-repeat; }
.endrow-cal { font-size: 11px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase; color: var(--t-label) !important; }
.endrow-cal i { font-style: normal; margin-left: .5em; color: var(--accent); opacity: .85; }
.endrow-dot { color: var(--t-faint); }
.endrow a:focus-visible { outline: 1px solid rgba(255, 255, 255, .4); outline-offset: 2px; }
@media (hover: hover) { .endrow a:hover { color: rgba(255, 255, 255, .78) !important; } }
@media (max-width: 640px) { .endrow { bottom: calc(var(--sab) + 40px + 54px); } } /* (round 15: 44 -> 54 px, as on a desktop) */
@media (max-height: 500px) { .endrow { bottom: calc(var(--sab) + 16px + 22px); } }
/* (round 20, the review's story-01) the same row at the idle crystal's landing, a few seconds in (main.js body.land-contact):
   bottom-left on a desktop, clear of the landing's lines. (1 Nov) Roman: just the email and the call there (no fact line over them),
   and none of it on a phone, upright or turned: there the row is the ending's and the menu's */
.endrow-fact { display: none; }
body.ready.land-contact:not(.end-contact):not(.in-chapter):not(.warp-outro) .chrome > .endrow { opacity: 1; visibility: visible; pointer-events: auto; transition: opacity 1.4s ease .2s, visibility 0s; }
@media (min-width: 641px) and (min-height: 501px) {
  body.land-contact:not(.end-contact) .chrome > .endrow { left: calc(var(--sal) + var(--edge) + 18px); bottom: calc(var(--sab) + var(--edge) + 10px); transform: none; }
}
@media (max-width: 640px), (max-height: 500px) { body.land-contact:not(.end-contact) .chrome > .endrow { visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; } }

/* bottom-centre inside a chapter: 0N / 05 · hint */
.counter {
  position: absolute; left: 50%; bottom: calc(var(--sab) + 54px);
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  visibility: hidden;
}
.counter-n { color: var(--t-label); font-variant-numeric: tabular-nums; }
.counter-sep { width: 1px; height: 14px; background: rgba(255, 255, 255, .16); }
.counter-hint { color: rgba(255, 255, 255, .55); } /* (round 15: .38 -> .55, 'Back to the start' clears 4.5:1 on 07's dawn) */
/* a scene showing its own in-scene hint fades the counter's hint out (ctx.setHint('')); the number stays */
.counter-hint, .counter-sep { transition: opacity .3s ease; }
body.hint-off .counter-hint, body.hint-off .counter-sep { opacity: 0; }
/* (round 43: on a turned phone 05's hint sat on the bright morning lake at ~3.3:1, its number at ~2.4:1) inside a scene the counter's
   number and hint get the top links' solid letters and tight dark halo, so they keep 4.5:1 over a bright scene, wherever the counter sits */
body.in-chapter .counter-n, body.in-chapter .counter-hint { color: #c2c2c4; text-shadow: 0 0 1px rgba(0, 0, 0, .95), 0 0 2px rgba(0, 0, 0, .9), 0 0 3px rgba(0, 0, 0, .75), 0 0 6px rgba(0, 0, 0, .5), 0 0 12px rgba(0, 0, 0, .28); }
body.in-chapter .chrome .visit { text-shadow: 0 0 1px rgba(0, 0, 0, .95), 0 0 2px rgba(0, 0, 0, .9), 0 0 3px rgba(0, 0, 0, .75), 0 0 6px rgba(0, 0, 0, .5); }

/* right edge: thin up/down arrows */
.arrows {
  position: absolute; right: calc(var(--sar) + 8px); top: 50%;
  transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 6px;
  visibility: hidden;
}
.arr { width: 48px; height: 64px; display: grid; place-items: center; color: rgba(255, 255, 255, .42); transition: color .3s ease; }
.arr svg { width: 14px; height: 44px; fill: none; stroke: currentColor; stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; overflow: visible; }
/* hover only where a real pointer hovers: on touch, :hover sticks after a tap */
@media (hover: hover) {
.arr:hover { color: rgba(255, 255, 255, .78); }
}
.arr:active svg { transform: scale(.94); }
/* a scene said it is over (ctx.done): the next arrow pulses three times */
.arr-down.pulse { animation: arr-pulse 1.2s ease-in-out 3; }
@keyframes arr-pulse { 0%, 100% { color: rgba(255, 255, 255, .35); } 50% { color: rgba(255, 255, 255, .9); } }
@media (prefers-reduced-motion: reduce) { .arr-down.pulse { animation-iteration-count: 1; } }

/* state switches: the crystal shows the hold UI; a chapter shows the counter and the arrows */
body.in-chapter .hold { opacity: 0 !important; pointer-events: none; transition: opacity .18s ease; }
body.in-chapter .counter, body.in-chapter .arrows { visibility: visible; }
.counter, .arrows { opacity: 0 !important; transition: opacity .7s ease .15s; }
/* (core round 2, fixer) one pair of labels at a time: at the dive the hold's two lines leave first (.18 s) and the counter comes
   in only once they are gone (.26 s on, below); on the way back to the crystal the counter leaves first (.2 s) and the hold comes
   in after it (.22 s on). The review's frame 0.15 s after a release had four on top of each other: "01 / 07", "CLICK & HOLD",
   "TAP · DRAG · HOLD" and "01 / 07 · HOME" */
body:not(.in-chapter) .counter { transition: opacity .2s ease; }
.hold { transition: opacity .6s ease .22s; }
body.in-chapter.ready .counter, body.in-chapter.ready .arrows { opacity: 1 !important; }
/* (fix round) the cinematic moves (main.js body.moving: the warp dive, both endings, the plain way back, the card dives): the
   chrome steps back to .18 in .25 s, and comes back on its own fades (.9 s; the counter and the arrows .7 s) as the chapter's
   copy fades in. Still clickable: the menu and the R mark work as before mid-move */
body.ready.moving .chrome > .mark, body.ready.moving .chrome > .vap, body.ready.moving .chrome > .burger, body.ready.moving .chrome > .mailtop { opacity: .18; transition: opacity .25s ease; }
/* (round 39, the review: once 02-04 step through their beats a scroller met no way to reach Roman for minutes) inside a chapter or a
   project, the address itself, small, beside the menu, as the landing's quiet row writes it. A desktop only: a phone's top row is full
   (its menu says hi) */
.mailtop {
  position: absolute; right: calc(var(--sar) + var(--edge) + 52px); top: calc(var(--sat) + var(--edge));
  height: 44px; display: flex; align-items: center; padding: 0 4px;
  font-size: 13px; letter-spacing: .01em; color: #c2c2c4; text-decoration: none; white-space: nowrap;
  background: linear-gradient(rgba(255, 255, 255, .26), rgba(255, 255, 255, .26)) 4px calc(50% + 10px) / calc(100% - 8px) 1px no-repeat;
  text-shadow: 0 0 1px rgba(0, 0, 0, .9), 0 0 2px rgba(0, 0, 0, .75), 0 1px 4px rgba(0, 0, 0, .5), 0 0 10px rgba(0, 0, 0, .22);
  opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .3s ease, visibility 0s linear .3s;
}
body.ready.in-chapter .chrome > .mailtop { opacity: 1; visibility: visible; pointer-events: auto; transition: opacity .6s ease .4s, visibility 0s; }
.mailtop:focus-visible { outline: 1px solid rgba(255, 255, 255, .4); outline-offset: 2px; }
@media (hover: hover) { .mailtop:hover { color: rgba(255, 255, 255, .78); } }
@media (max-width: 899px), (max-height: 500px) { .chrome > .mailtop { display: none; } }
/* (round 41) over a lit background (04's hallway light behind the door glass, about 1.2:1) the light letters had only their thin edge:
   everywhere a firmer dark edge on the letters (black on black on a dark scene), and in 04, whose door glass lights up right behind the
   address, whatever is behind it is frosted and pressed down in a soft oval that fades out before its edge (no box, no line):
   contrast(.76) brightness(.37) leaves the night about where it is and brings any light down to .32 at most (the lit door: 1.45:1 ->
   about 4.7:1). Not on every scene: on a mid-grey sky the oval would read as a smudge */
body.in-chapter .chrome > .mailtop { text-shadow: 0 0 1px rgba(0, 0, 0, .95), 0 0 2px rgba(0, 0, 0, .9), 0 0 3px rgba(0, 0, 0, .7), 0 1px 5px rgba(0, 0, 0, .5); }
/* (round 43: that soft oval read as a brown smudge on the lit door, and the menu's strokes beside it had nothing) one crisp chip in the
   chrome's own charcoal with a hairline edge, as 01's "Pick it up" chip, behind the address and the menu together (its right edge is
   the menu's, 52 px past the address): the light behind it frosted and pressed down, so the letters clear 4.5:1 on the lit door too */
body.in-chapter:has(#scenes-ui > .scene-ui.on[data-chapter="rust"]) .chrome > .mailtop::before {
  content: ""; position: absolute; left: -10px; right: -52px; top: 5px; bottom: 5px; z-index: -1; pointer-events: none;
  border-radius: 999px; background: rgba(20, 20, 24, .78); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .16);
  -webkit-backdrop-filter: blur(6px) brightness(.6); backdrop-filter: blur(6px) brightness(.6);
}
/* and while 07's own contact block shows the address big in the middle, the small one steps out (never twice on one frame) */
body.ready.in-chapter:has(#scenes-ui > .scene-ui.on .ol-contact.show) .chrome > .mailtop { opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .4s ease, visibility 0s linear .4s; }
/* (round 44) a scene's own camera move under the top row (ctx.chromeQuiet, main.js): the row steps back to .15 in .3 s, and back in .4 s */
/* (only the label the camera passes under steps back: the R mark, the menu and the address stay put) */
body.ready.quiet .chrome > .vap { opacity: .15; transition: opacity .3s ease; }
body.ready.unquiet .chrome > .vap { transition: opacity .4s ease, visibility 0s linear .4s; }
body.ready.moving .chrome > .sound { opacity: .18; transition: background-color .4s, color .4s, opacity .25s ease !important; }
body.ready.in-chapter.moving .counter, body.ready.in-chapter.moving .arrows { opacity: .18 !important; transition: opacity .25s ease !important; }
body.ready.in-chapter.moving .counter { transition: opacity .25s ease .26s !important; }
body.ready.in-chapter.has-visit.moving .chrome .visit { opacity: .18 !important; transition: opacity .25s ease !important; }

/* bottom-right: the sound button, rebuilt from the site's SoundButton (components/ui/SoundButton.tsx +
   styles/header.css): a 3.2em circle with a two-sine wave drawn on a canvas, dim for this page */
.sound {
  position: absolute;
  right: calc(var(--sar) + var(--edge) + 2px); bottom: calc(var(--sab) + var(--edge) + 2px);
  font-size: 14px;
  width: 3.2em; height: 3.2em;
  border-radius: 50% !important;
  background: rgba(255, 255, 255, .055) !important;
  color: rgba(255, 255, 255, .55);
  overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  transition: background-color .4s, color .4s, opacity .9s ease !important;
}
/* hover only where a real pointer hovers: on touch, :hover sticks after a tap */
@media (hover: hover) {
.sound:hover { background: rgba(220, 38, 38, .82) !important; color: rgba(255, 255, 255, .85); }
}
/* (round 44) the button is a thing you press: it lifts a hair under a mouse, gives under the press at once and springs back */
.sound { scale: 1; transition: background-color .4s, color .4s, opacity .9s ease, scale .5s cubic-bezier(.3, 1.6, .5, 1) !important; }
@media (hover: hover) { .sound:hover { scale: 1.06; } }
.chrome .sound:active { scale: .9; transition: background-color .4s, color .4s, opacity .9s ease, scale .08s ease-out !important; }
.sound canvas { width: 100%; height: 100%; display: block; }

/* ---------- film grain (over everything) ---------- */
.grain {
  position: fixed; inset: -256px;
  z-index: 100;
  pointer-events: none;
  opacity: .05;
  background-size: 256px 256px;
  will-change: transform;
}

/* ---------- phones ---------- */
@media (max-width: 640px) {
  :root { --edge: 8px; }
  /* one top row: mark, VIEW ALL PROJECTS, menu */
  .vap { top: calc(var(--sat) + var(--edge)); }
  .say { width: 84vw; font-size: clamp(20px, 5.8vw, 25px); }
  .hold, .counter { bottom: calc(var(--sab) + 40px); }
  .hold-track { width: 170px; }
  .arrows { right: calc(var(--sar) + 2px); }
}
/* short landscape phones */
@media (max-height: 500px) {
  :root { --edge: 8px; }
  .mark-back { font-size: 11px; } /* no small caps under 11 px on a phone */
  .vap { top: calc(var(--sat) + var(--edge)); }
  .hold, .counter { bottom: calc(var(--sab) + 16px); gap: 8px; }
  .hold-next { display: none; }
  .counter-sep { height: 8px; }
  .arr { height: 52px; }
  .say { font-size: clamp(18px, 2.6vw, 24px); }
}

/* on phones the chrome's small caps read: 11 px, and the counter's hint at .55 on #1d1d1f (about 5.6:1; .45 at 10 px
   was 4.4:1, under AA for small text) */
@media (max-width: 640px), (max-height: 500px) {
  .counter-hint, .counter-n, .hold-label, .hold-next, .vap { font-size: 11px; }
  .counter-hint { color: rgba(255, 255, 255, .55); }
}
/* (round 9, the 05 request) on a 320 px phone held upright a 24-capital hint ran to 3 px of the sound button: tighter tracking there */
@media (max-width: 340px) and (orientation: portrait) {
  .counter-hint { letter-spacing: .07em; }
}
/* at 360-400 px the R ring alone is "Back" (its aria-label says "Back to the crystal"): "R BACK" and
   "VIEW ALL PROJECTS" were 5 px apart and read as one string, two tap targets touching */
@media (max-width: 400px) {
  body.in-chapter .mark-back { display: none; }
}

/* polish: very short landscape phones (844 x 390): the counter + hint move to the right edge under the
   arrows, so the bottom row belongs to each chapter's copy (was crowded against hello's body and
   squeezed between cliphub's copy and its clip counter) */
@media (max-height: 430px) and (min-width: 560px) {
  .counter {
    left: auto; right: calc(var(--sar) + 16px); bottom: calc(var(--sab) + 74px);
    transform: none; align-items: flex-end; gap: 6px;
  }
  .counter-sep { display: none; }
}

/* ---------- v3: the projects track ---------- */
/* a card dive / back: the canvas and the scene's DOM are lifted above the All Projects overlay (z 60+);
   the canvas draws a transparent outside, so the overlay shows through around the opening */
body.gl-top #gl { z-index: 64; pointer-events: none; }
body.gl-top #scenes-ui { z-index: 65; }

/* bottom-left inside a project scene: "Visit site", small and dim (new tab) */
.chrome .visit {
  position: absolute;
  left: calc(var(--sal) + var(--edge) + 6px); bottom: calc(var(--sab) + var(--edge) + 2px);
  min-height: 44px; min-width: 44px;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 0 6px;
  font-size: 11px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase; /* (20 Oct: was 10) */
  color: var(--t-label);
  text-decoration: none;
  pointer-events: none;
  opacity: 0 !important;
  transition: opacity .6s ease, color .3s ease !important;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.chrome .visit span { padding: 4px 0 5px; background: linear-gradient(rgba(255, 255, 255, .26), rgba(255, 255, 255, .26)) 0 100% / 100% 1px no-repeat; }
.chrome .visit i { font-style: normal; color: var(--accent); opacity: .85; }
.chrome .visit[hidden] { display: none; }
body.ready.in-chapter.has-visit .chrome .visit { opacity: 1 !important; pointer-events: auto; transition-delay: .25s, 0s !important; }
.chrome .visit:focus-visible { outline: 1px solid transparent; outline-offset: 2px; box-shadow: 0 0 0 2px rgba(17, 17, 19, .55), 0 0 0 3px rgba(255, 255, 255, .88); }
@media (hover: hover) { .chrome .visit:hover { color: rgba(255, 255, 255, .7); } }
.chrome .visit:active { color: rgba(255, 255, 255, .7); }
/* short landscape phones: the bottom-left corner belongs to each scene's copy there; the link sits in the
   top bar instead, left of the menu */
@media (max-height: 500px) and (min-width: 560px) {
  .chrome .visit { left: auto; bottom: auto; right: calc(var(--sar) + var(--edge) + 56px); top: calc(var(--sat) + var(--edge)); }
}

/* the gate's "Loading…" (a tap that came before main.js was live): inlined in index.html with the gate */

/* no WebGL: a plain page in the same dim style (window.__noGl in index.html builds it) */
html:has(body.no-gl) { overflow: auto; height: auto; }
body.no-gl { position: static; overflow: auto; height: auto; min-height: 100%; touch-action: auto; user-select: text; -webkit-user-select: text; background: #1d1d1f; }
body.no-gl .gate, body.no-gl #gl, body.no-gl .chrome, body.no-gl .intro, body.no-gl .say, body.no-gl #scenes-ui { display: none !important; }
.no-gl-page {
  max-width: 680px; margin: 0 auto;
  padding: calc(var(--sat) + 88px) calc(var(--sar) + 24px) calc(var(--sab) + 88px) calc(var(--sal) + 24px);
  color: rgba(255, 255, 255, .6);
}
.no-gl-page .ng-kick { margin: 0 0 14px; font-size: 11px; font-weight: 600; letter-spacing: .22em; text-transform: uppercase; color: rgba(255, 255, 255, .45); }
.no-gl-page .ng-title { margin: 0; font-weight: 200; font-size: clamp(34px, 6vw, 48px); line-height: 1.06; letter-spacing: -0.02em; color: rgba(255, 255, 255, .6); }
.no-gl-page .ng-body { margin: 14px 0 0; font-size: 16px; font-weight: 300; line-height: 1.55; color: rgba(255, 255, 255, .52); }
.no-gl-page .ng-links { display: flex; flex-wrap: wrap; gap: 6px 22px; margin: 28px 0 0; }
.no-gl-page .ng-links a {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: 11px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase; text-decoration: none;
  color: rgba(255, 255, 255, .55);
  background: linear-gradient(rgba(255, 255, 255, .3), rgba(255, 255, 255, .3)) 0 calc(50% + 10px) / 100% 1px no-repeat;
}
.no-gl-page .ng-links a:first-child { text-transform: none; letter-spacing: .02em; font-size: 14px; font-weight: 400; }
.no-gl-page .ng-links a:focus-visible { outline: 1px solid rgba(255, 255, 255, .4); outline-offset: 3px; }
@media (hover: hover) { .no-gl-page .ng-links a:hover { color: rgba(255, 255, 255, .78); } }
.no-gl-page .ng-h { margin: 64px 0 18px; font-size: 11px; font-weight: 600; letter-spacing: .22em; text-transform: uppercase; color: rgba(255, 255, 255, .45); }
.no-gl-page .ng-cards { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; }
.no-gl-page .ng-card { padding: 20px 22px 22px; border: 1px solid rgba(255, 255, 255, .08); border-radius: 2px; background: rgba(255, 255, 255, .02); }
.no-gl-page .ng-card .ng-kick { margin-bottom: 10px; font-size: 11px; } /* (20 Oct: the no-WebGL page's labels 11 px, were 10-10.5) */
.no-gl-page .ng-card h2 { margin: 0; font-weight: 300; font-size: 22px; line-height: 1.2; color: rgba(255, 255, 255, .62); }
.no-gl-page .ng-card p:last-child { margin: 8px 0 0; font-size: 14.5px; font-weight: 300; line-height: 1.5; color: rgba(255, 255, 255, .5); }
.no-gl-page .ng-card:first-child { border-left-color: rgba(220, 38, 38, .55); }

/* a resizing on-screen keyboard can leave a landscape phone ~190 px tall: the field owns the screen while typing, so
   the right-edge chrome (arrows, counter, sound) steps out instead of piling up (these outrank the !important rules) */
@media (max-height: 260px) {
  body .arrows, body .counter, body .sound, body.in-chapter.ready .arrows, body.in-chapter.ready .counter { opacity: 0 !important; pointer-events: none !important; }
}
/* the warp's ending (main.js WARP blocks, fx/outro.js): the crystal's CLICK & HOLD waits under the zoom-out; it fades in (its own .6 s) once the crystal is whole and its line is "Hold to start again" */
body.warp-outro .hold { opacity: 0 !important; transition: none !important; pointer-events: none; }
/* 07's page text leaves within 0.15 s once the night opens (its own .out fade is 0.4 s) */
body.warp-outro .hr { opacity: 0 !important; transition: opacity .15s linear !important; }

/* ---------- phones: calm and centred (23 Oct, Roman and a friend on a real phone: "too much stuff going on in the UI for
   the phone", "view all projects shouldn't even be there", "why do I see arrows up and down? everything should be
   centered", "what is the R button?") ----------
   Only where the main pointer is a finger (pointer: coarse, the same test as main.js isTouch) AND the screen is
   phone-sized (the phone breakpoints above). A mouse, or a wider screen, keeps every rule above exactly as it was. No
   markup of its own: the same buttons, restyled, so their jobs and labels are unchanged.
   - At the crystal there is nothing to go back to, so the R steps out there. (round 15, the review's LOOK-9: a lone close mark
     in the top centre read as "close the site" and dropped the R) inside a chapter or a project it is the desktop's R mark +
     "Back", top-left, the menu top-right, the same 44 px targets. Its job is unchanged: 01's room to the lake, a chapter to
     the crystal, a project back into its card. The phone's own Back does the same.
   - View all projects and the menu show only at the crystal (and through the ending, which lands there). A chapter
     shows its scene, the R + "Back", the menu, the counter and the sound.
   - Portrait: the arrows leave the right edge. Two small chevrons sit either side of the counter's number, on its row,
     so the navigation is one centred group at the bottom (the zone every scene already keeps clear for the counter).
     A turned phone keeps its right-hand column, where the counter and the arrows are already one column. */
@media (pointer: coarse) and (max-width: 640px), (pointer: coarse) and (max-height: 500px) {
  /* the R + "Back" (View all projects is not in this row inside a chapter, so the label has its room at 320-400 px too) */
  body.in-chapter .chrome .mark-back { display: inline; }
  .mark:active .mark-r { border-color: rgba(220, 38, 38, .75); color: rgba(255, 255, 255, .72); }
  /* stepped out: faded, then out of the hit test and the focus order (the !important outranks the moving / ready fades)
     (round 14, L9: the menu stays in a chapter and a project, top-right (the R + "Back" top-left), so the menu and the languages are
     one tap away and the mark reads as "close this chapter") */
  body:not(.in-chapter) .chrome > .mark,
  body.in-chapter .chrome > .vap {
    opacity: 0 !important; visibility: hidden; pointer-events: none;
    transition: opacity .25s ease, visibility 0s linear .25s !important;
  }
}
/* portrait: "↑ 01 / 07 ↓" as one centred row (the counter's number row: its centre is 95 px over the bottom inset, the
   counter's own 40 px + its 62 px column less half the number). The gap clears the number: "01 / 07" is 54 px wide,
   "PROJECT 01 / 04" 125 px, and each chevron sits ~15 px off it inside its 44 px target */
@media (pointer: coarse) and (max-width: 640px) and (orientation: portrait) {
  .arrows {
    right: auto; left: 50%; top: auto;
    bottom: calc(var(--sab) + 73px);
    transform: translateX(-50%);
    flex-direction: row; gap: 50px;
  }
  body.track-project .arrows { gap: 122px; }
  /* (round 13, phone-03: in 04, and on 01's lake right after the dive, the hint row was empty, the counter 48 px tall instead of 62,
     and its number sat 14 px under the chevrons' line) an empty hint keeps its row (a zero-width space: the same line box) */
  .counter-hint:empty::before { content: "\200b"; }
}
/* (mobile round 2, the reviews: a turned phone still had the desktop's 48 x 110 px arrow column at mid-height on the right edge, apart
   from the counter at its foot) a turned phone gets the same chevrons, either side of the counter's number: one group at the foot
   of the right edge, where its counter already sits (the bottom row is each chapter's copy there, see the short-landscape rule
   above). The counter steps 40 px left to make room for the down chevron; the number's centre is 101 px over the bottom inset
   (the counter's own 74 px + its hint and gap + half the number) */
/* (mobile round 3, core-ui) ...and not either side of it any more: with "Project 01 / 04" the up chevron sat 122 px left of the
   down one, on ClipHub's phone (its "9:16" label, 844 x 390). The two chevrons are one small vertical pair right of the number
   instead (a stepper: up over down, the number's row between them), in the 44 px column above the sound button (the targets
   end 1 px over it; the chevrons and the sound wave share one centre line). The number keeps its place (right: 56 px) and rises
   5 px, so its row (106 px over the bottom inset) is the pair's middle. Nothing reaches further left than the number */
@media (pointer: coarse) and (max-height: 430px) and (min-width: 560px) {
  .counter { right: calc(var(--sar) + 56px); bottom: calc(var(--sab) + 79px); }
  .arrows {
    top: auto; right: calc(var(--sar) + 12px);
    bottom: calc(var(--sab) + 62px);
    transform: none;
    flex-direction: column; gap: 0;
  }
}
/* the chevrons (portrait and a turned phone) */
@media (pointer: coarse) and (max-width: 640px) and (orientation: portrait), (pointer: coarse) and (max-height: 430px) and (min-width: 560px) {
  .arr { position: relative; width: 44px; height: 44px; color: rgba(255, 255, 255, .5); }
  .arr svg { display: none; }
  /* a chevron: two 1 px sides of a 7 px square, turned (its middle sits on the target's middle) */
  .arr::before {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: 7px; height: 7px; margin: -1px 0 0 -3.5px;
    border-left: 1px solid currentColor; border-top: 1px solid currentColor;
    transform: rotate(45deg);
    transition: transform .2s ease;
  }
  .arr-down::before { margin-top: -6px; transform: rotate(225deg); }
  .arr-up:active::before { transform: rotate(45deg) scale(.86); }
  .arr-down:active::before { transform: rotate(225deg) scale(.86); }
}
/* (round 3) the turned phone's pair: each chevron 9 px from the line between the two targets (the number's row), so they read as
   one small stepper 18 px tall, and 5 px left of its target's centre (12 px from the number) */
@media (pointer: coarse) and (max-height: 430px) and (min-width: 560px) {
  .arr-up::before { margin: 12px 0 0 -8.5px; }
  .arr-down::before { margin: -19px 0 0 -8.5px; }
}
/* (mobile round 2, the reviews: "the 01 room has two exits (X and Outside)") while 01's room is up on a touch
   screen (home3 sets body.h3-in-room on entering and leaving it; a mouse never does) the R + "Back" steps out, so the room has one
   way out, its own "Outside". Back on the lake the mark is there again. A body class, not :has() (that measured 100-500x the style
   work of every class toggle in the page) */
@media (pointer: coarse) and (max-width: 640px), (pointer: coarse) and (max-height: 500px) {
  body.in-chapter.h3-in-room .chrome > .mark {
    opacity: 0 !important; visibility: hidden; pointer-events: none;
    transition: opacity .25s ease, visibility 0s linear .25s !important;
  }
}
/* a phone or a tablet (a finger is the main pointer, the same test as main.js MOBILE): the film grain's layer ends at the
   screen's right and bottom edges. main.js only ever moves the tile right and down (0-255 px), so the 256 px the layer had
   past those two edges never came on screen; the layer drops from (W + 512) x (H + 512) to (W + 256) x (H + 256) CSS px. At
   390 x 844 @3 that is 11.0 -> 6.4 million pixels of GPU tiles. The tile still starts at the same corner: the same pixels */
@media (pointer: coarse) {
  .grain { right: 0; bottom: 0; }
}
