/* ---------------------------------------------------------------
   Charlie Bieger — theme prototypes, shared foundation
   Identical across A / B / C so the comparison isolates the motif.
   --------------------------------------------------------------- */

:root {
  --edge-fade: 44px;      /* depth of the top/bottom canvas dissolve on touch */
  --paper:    #F2F2F0;
  --ink:      #14161B;
  --navy:     #16203A;
  --amber:    #C07A38;
  --graphite: #3A3B3E;
  --stone:    #A89C8A;
  --gold:     #B08A4E;
  --greige:   #8C8B84;    /* decorative only: rules, hairlines, inactive marks */
  /* Text-safe greige. #8C8B84 on paper is 3.05:1 and fails WCAG AA everywhere
     it was used for words. #6E6D66 is ~4.6:1 and reads as the same grey family
     at the small sizes it appears in. */
  --greige-text: #6E6D66;

  --font-display: "Fraunces", "Times New Roman", serif;
  --font-body: "Inter", -apple-system, "Helvetica Neue", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", monospace;

  --gutter: clamp(24px, 5vw, 96px);
}

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

html {
  scroll-behavior: auto;
  /* The paper has to live on <html>, not just <body>: rubber-banding past
     either end of a phone scroll exposes the ROOT element's canvas, and with
     only body painted that showed as white bars top and bottom. */
  background: var(--paper);
  /* NOT overscroll-behavior: none. Suppressing the bounce also suppresses part
     of the scroll gesture iOS Safari watches to decide whether to collapse its
     toolbars — and with the paper painted here, an overscroll now just shows
     paper anyway, which is what it was added to prevent. */
}

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* `clip`, NOT `hidden`. `overflow: hidden` here makes <body> a scroll
     container, and iOS Safari then can't see the document scrolling — so it
     never collapses its toolbars into the floating pill, and they sit as solid
     bars for the whole visit. `clip` suppresses the same sideways overflow
     without creating that container. */
  overflow-x: clip;
  /* with viewport-fit=cover the page runs under the home indicator — keep the
     last of the content off it. Paper padding, so it reads as margin. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

::selection { background: var(--ink); color: var(--paper); }
/* Inputs need this spelled out. The autofill rule above sets
   -webkit-text-fill-color, and that wins over ::selection's `color` — so
   selected text in the signup field stayed ink on an ink highlight and was
   unreadable. Setting the fill colour in the selection state is what actually
   changes it. */
input::selection, textarea::selection {
  background: var(--ink); color: var(--paper); -webkit-text-fill-color: var(--paper);
}
input::-moz-selection, textarea::-moz-selection { background: var(--ink); color: var(--paper); }

/* ---- autofill ----
   Browsers repaint an autofilled field with their own pale yellow/blue and
   their own text colour, which breaks any considered field and is especially
   wrong on the glass signup capsule. There's no property to decline it, but the
   background arrives as a TRANSITION — parking that at an absurd delay means it
   never lands — and the text colour has its own `-webkit-text-fill-color`.
   Deliberately not the usual `box-shadow: inset` mask: that paints an opaque
   rectangle and would knock a hole in the glass. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active,
textarea:-webkit-autofill,
select:-webkit-autofill {
  /* -webkit-text-fill-color is deliberately NOT set here any more. WebKit gives
     it precedence over ::selection's colour, so pinning it to ink was what kept
     selected text black on a black highlight no matter how specific the
     ::selection rule got. Plain `color` styles autofilled text just as well in
     current Safari, and it yields to ::selection the way it should. */
  color: var(--ink);
  caret-color: var(--ink);
  transition: background-color 100000s ease-in-out 0s;
}

a { color: inherit; }

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

img { max-width: 100%; display: block; }

/* ---- the pinned 3D stage ---- */

.stage {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  /* The LARGE viewport height, not `inset: 0`.
     `inset: 0` resolves to whatever the viewport is right now, which on a phone
     is the height MINUS the browser chrome — so the stage stopped short and
     left a strip at the bottom, cutting off any shard crossing it. `lvh` is the
     tallest the viewport ever gets, so the stage always reaches the physical
     edges and sits behind the chrome rather than beside it. Standards-based,
     so it holds on any phone rather than being tuned to one.
     100vh first as the fallback: on older iOS that already means the large
     viewport, which is the behaviour we want here. */
  height: 100vh;
  height: 100lvh;
  z-index: 0;
  pointer-events: none;
  /* Safari 26 tints its Liquid Glass toolbars by sampling the background-color
     of `fixed`/`sticky` elements sitting within a few px of the viewport edge.
     This element spans all four, and its own background-color was transparent —
     the canvas only paints pixels, which the sampler doesn't read — so Safari
     fell back to white and drew the bars. Declaring the paper explicitly gives
     it the right colour to sample. */
  background: var(--paper);
}
/* fills the stage edge to edge — three is told not to write inline sizes, so
   this is what governs the canvas's displayed size */
.stage canvas { display: block; width: 100%; height: 100%; }

/* ---- the edge fade (touch only) ----
   On iOS the browser keeps strips at the top and bottom of the screen that the
   page can only paint with its ROOT background — a `fixed` element is clipped
   to the viewport, and so is `sticky`; both were tested on an iOS 26 device and
   neither reaches. So the canvas stops short, and any shard crossing that line
   was cut dead flat against paper. That hard edge is the "bezel".
   Rather than police where every shard may travel, the canvas dissolves into
   the paper before its own edge: the strip beyond is the same paper, so the
   two meet invisibly and a shard leaving the frame simply fades out.
   Pointer-coarse only — a desktop window has no such strips.

   Done with two gradient overlays rather than `mask-image` on the canvas.
   A mask on a full-screen WebGL canvas costs an extra compositing pass EVERY
   frame on a phone GPU, which is what made scrolling stutter; painting paper
   over the canvas at falling opacity is the same picture for free.
   Measured on an iOS 26 device, and this is the whole constraint in one line:
   the strips take the root element's background-COLOUR and nothing else. A
   background-image is clipped to the positioning area, so a gradient offset up
   into the strip simply isn't painted; `fixed` and `absolute` elements are
   clipped to the viewport too. One flat colour is all that can ever go there —
   which is exactly why a dark-background site looks edge to edge and a paper
   one cannot: its strips happen to match its artwork.
   So the canvas dissolves into that flat paper before its own edge instead of
   being cut dead against it. */
@media (pointer: coarse) {
  html:not(.stage-inflow) .stage::before,
  html:not(.stage-inflow) .stage::after {
    /* 44, not 62. The fade eats the top of the frame: at 62px it reached NDC
       ~0.84, and the shards were framed to 0.96 — so they sat inside it and
       visibly dissolved. Shorter fade, more usable frame, same softening of the
       bezel it exists for.
       The height lives in a variable because the BOTTOM fade is positioned by
       subtracting it from the viewport height — when the two were written as
       separate literals, trimming one and not the other left the fade ending
       18px above the bezel, as a permanent gap. */
    content: ''; position: absolute; left: 0; right: 0; height: var(--edge-fade, 44px);
    z-index: 2; pointer-events: none;
  }
  /* Top is easy: the stage's top edge IS the visible top edge. */
  html:not(.stage-inflow) .stage::before { top: 0; background: linear-gradient(to bottom, var(--paper), rgba(242, 242, 240, 0)); }
  /* The bottom is NOT the stage's bottom edge. The stage is 100lvh — the
     TALLEST the viewport ever gets — so while the toolbar is expanded its
     bottom edge sits ~40px below what you can actually see, putting the fade
     off-screen and leaving a hard viewport clip through the shards. `dvh` is
     the CURRENT visible height, so this pins the fade to the real edge and
     lets it travel as the toolbar collapses and expands. */
  html:not(.stage-inflow) .stage::after {
    /* dvh is the CURRENT visible height, so this tracks the bezel as Safari's
       search bar collapses and expands. Subtracting the SAME variable as the
       height is what keeps the fade's bottom edge flush with the bezel's top. */
    top: calc(100vh - var(--edge-fade, 44px));    /* fallback for no-dvh browsers */
    top: calc(100dvh - var(--edge-fade, 44px));
    background: linear-gradient(to top, var(--paper), rgba(242, 242, 240, 0));
  }
}

/* ---- the in-flow stage: the actual fix for the bezels ----
   Measured on an iOS 26 device, and it is the whole thing in one sentence:
   `position: fixed` and `position: sticky` are laid out against the SMALLER
   visual viewport, which excludes the strips iOS reserves for its status bar
   and toolbar — but ORDINARY IN-FLOW CONTENT is laid out across the full
   screen and paints into them. That is the entire difference between the tour
   and the About / Contact / Legal pages: those are plain scrolling documents,
   so their content already runs edge to edge, while the tour's visual was a
   fixed canvas and could never reach.
   So the stage stops being fixed. It becomes a normal in-flow block, made
   taller than the viewport and lifted above it so it spans the whole physical
   screen, and it is held in place by a scroll-driven animation rather than by
   `position: fixed`. The pin runs on the compositor, so it tracks the finger
   exactly the way a fixed element did — driving it from a rAF callback instead
   would trail the async scroll thread and judder.
   .page is pulled back up by the stage's height so the document lays out
   exactly as it did before; --scroll-range is written by theme.js and rewritten
   on resize, since the timeline maps progress against the live scroll range. */
html.stage-inflow .stage {
  position: relative;
  top: auto; left: auto;
  width: 100%;
  height: calc(100lvh + var(--stage-bleed, 200px));
  /* Pinned by a scroll-driven animation, which runs on the COMPOSITOR. Doing
     it from rAF instead was tried and was far worse: iOS scrolls on a separate
     thread, so a per-frame transform trails the finger and the whole scene
     bounces on every gesture. Longhands because the `animation` shorthand
     resets animation-timeline and sets duration to 0s; a scroll timeline needs
     `auto`. */
  /* MEASURED on an iOS 26 device: the stage was jumping by exactly 70px — the
     lift — while the page was not scrolling at all. A scroll-driven animation
     stops applying whenever its timeline is momentarily inactive, and the
     element then reverts to its BASE style. With the animation supplying the
     only transform, that base was `none`, so every deactivation threw the whole
     stage 70px down the screen and back. Declaring the start pose statically
     means an inactive timeline leaves the stage where it already was instead of
     dropping it. */
  transform: translateY(calc(-1 * var(--stage-lift, 70px)));
  animation-name: stage-pin;
  animation-duration: auto;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: scroll(root block);
}
@keyframes stage-pin {
  from { transform: translateY(calc(-1 * var(--stage-lift, 70px))); }
  to   { transform: translateY(calc(var(--scroll-range, 0px) - var(--stage-lift, 70px))); }
}
html.stage-inflow .page { margin-top: calc(-1 * (100lvh + var(--stage-bleed, 200px))); }

/* A short dissolve at the two screen edges, so a small shard drifting out of
   frame fades away instead of ending on a hard horizontal cut.
   Both are PAPER. An earlier version faded to a colour sampled from whatever
   was behind it, which meant the strip swung yellow or tan as a warm shard
   passed under it — worse than the seam it was hiding. Paper is safe now for
   the reason it wasn't before: the framing keeps the photo shards clear of the
   top edge, so what is up there is already paper.
   Anchored to the CANVAS's own edges, not the screen's. The two differ — the
   stage is lifted 70px but the status bar is only ~59, so the canvas overhangs
   the screen by ~11px at the top and ~10 at the bottom — and the overhang is
   not a number CSS can know. Starting the gradient 59px too far down was the
   visible gap: an unfaded strip, then a hard line where the fade began. From
   the canvas edge the fade is simply already underway by the time it reaches
   the screen, which is what a dissolve should look like. It is also why the
   bottom one appeared to be missing — it was parked below the screen. */
html.stage-inflow .stage::before,
html.stage-inflow .stage::after {
  content: ''; position: absolute; left: 0; right: 0;
  z-index: 2; pointer-events: none;
}
html.stage-inflow .stage::before {
  top: 0; height: 76px;
  background: linear-gradient(to bottom, var(--paper), rgba(242, 242, 240, 0));
}
/* Taller than the top one on purpose: the canvas hangs ~70px BELOW the screen,
   so the opaque end of this gradient is off-screen entirely and only its tail
   lands in view. At 116px almost nothing of it was visible, which is why the
   bottom looked like it had no fade at all. */
html.stage-inflow .stage::after {
  bottom: 0; height: 190px;
  background: linear-gradient(to top, var(--paper), rgba(242, 242, 240, 0));
}
/* Out of flow, the stage could never disturb the scroll position. In flow it is
   a real ~950px box at the top of the document, so the browser's scroll
   anchoring starts counting it — and any late layout settle (fonts, the canvas
   sizing) nudges the page back, which landed /#veloro on the raw fragment
   offset instead of on the stop the tour had just scrolled to. */
html.stage-inflow, html.stage-inflow body { overflow-anchor: none; }

/* ---- content flows above the stage ---- */

.page { position: relative; z-index: 1; }

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

.eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--greige-text);
}

.display {
  font-family: var(--font-display);
  font-variation-settings: "opsz" 144;
  font-weight: 340;
  line-height: 0.98;
  letter-spacing: -0.015em;
}

.hero-name {
  font-size: clamp(3rem, 9vw, 8rem);
}

.section-title {
  font-size: clamp(2.2rem, 6vw, 5.2rem);
}

.sub {
  font-size: clamp(0.95rem, 1.2vw, 1.1rem);
  font-weight: 400;
  color: var(--ink);
  max-width: 34em;
}

.caption {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--greige-text);
  line-height: 1.8;
}

/* ---- section scaffolding ---- */

section.beat {
  position: relative;
  min-height: 160vh;
  padding: 0 var(--gutter);
}

section.hero {
  min-height: 200vh;
}

.beat-label {
  position: sticky;
  top: 42vh;
  max-width: 560px;
}

.beat-label .eyebrow { display: block; margin-bottom: 1.4em; }
.beat-label .sub { margin-top: 1.6em; }

/* ---- photo treatment ---- */

.photo-frame {
  position: relative;
  overflow: hidden;
  background: var(--ink);
}
.photo-frame img {
  width: 100%; height: 100%;
  object-fit: cover;
}
.photo-frame .caption {
  position: absolute;
  left: 1.1em; bottom: 0.9em;
  color: rgba(242, 242, 240, 0.85);
}

/* ---- marker sections (unbuilt beats, deliberately quiet) ---- */

section.marker {
  min-height: 120vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 var(--gutter);
  border-top: 1px solid rgba(20, 22, 27, 0.08);
}
section.marker .display { color: rgba(20, 22, 27, 0.34); }
section.marker .eyebrow { margin-bottom: 1em; }

/* ---- footer / colophon ---- */

footer.colophon {
  padding: 18vh var(--gutter) 12vh;
  border-top: 1px solid rgba(20, 22, 27, 0.08);
}

/* ---- theme switcher (injected by shared/switcher.js) ---- */

.theme-switcher {
  position: fixed;
  top: 22px;
  right: clamp(20px, 3vw, 40px);
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 2px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
}

.theme-switcher a {
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  border-radius: 50%;
  text-decoration: none;
  color: var(--greige-text);
  transition: color 0.25s ease, background 0.25s ease;
}
.theme-switcher a:hover { color: var(--ink); }
.theme-switcher a[aria-current="page"] {
  color: var(--paper);
  background: var(--ink);
}
.theme-switcher .dot {
  color: rgba(20, 22, 27, 0.25);
  user-select: none;
}
.theme-switcher .home {
  width: auto;
  margin-right: 10px;
  padding: 0 4px;
  border-radius: 2px;
  color: var(--greige-text);
  text-decoration: none;
}
.theme-switcher .home:hover { color: var(--ink); }

@media (max-width: 480px) {
  .theme-switcher { top: 14px; }
}

/* ---- reduced motion: themes read this class on <html> ---- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* The hero wordmark is centred on the VIEWPORT, but the prism behind it is
   centred on the canvas — which is taller and lifted, so its middle sits a
   little lower. theme.js measures the difference; this puts the name back on
   the stone. Touch only: --stage-center-shift is unset elsewhere. */
html.stage-inflow .hero-lockup { transform: translateY(var(--stage-center-shift, 0px)); }

/* ---- one view, no zoom ----
   Pinch- and double-tap-zoom break a full-bleed 3D scene: the canvas is sized
   to the screen, so magnifying it just shows a blurred crop with the layout
   stranded. `touch-action` stops the double-tap and the browser's own pinch;
   iOS additionally needs the gesture events cancelled in theme.js, since it has
   ignored user-scalable=no since iOS 10.
   Note this does remove pinch-to-zoom as an accessibility affordance — it is a
   deliberate call for this page, not an oversight. */
html.stage-inflow, html.stage-inflow body { touch-action: pan-y; }

/* The tour is a fixed view driven BY scroll, not a page you scroll through, so
   the rubber-band at either end reads as a glitch — the whole scene detaches
   and slides. `contain` stops the bounce without blocking the scroll gesture
   Safari watches to collapse its toolbars (`none` cost us that once already). */
html.stage-inflow { overscroll-behavior-y: none; }
/* `none`, not `contain`. contain only stops scroll CHAINING to a parent — on
   the root scroller in Safari the rubber-band still happens, which is the hero
   wordmark drifting and snapping back when you pull down at the top. `none` is
   what actually removes the bounce. It costs some of the gesture Safari watches
   to collapse its toolbars; that mattered when the toolbars were solid bars
   beside the content, and matters much less now the canvas runs under them. */
