/* fern Studio — Colour World, R2: a scrollcraft worldflight.
   One fixed canvas (#world), six legs crossfaded by the engine, a copy layer
   of [data-sc-copy] windows on top, and no top bar: the skip link is the
   only chrome. The engine (scrollcraft.js/.css) owns segment opacity/visibility/
   z-index and .sc-world's position; this file never touches those. */

/* ------------------------------------------------------------ @font-face --
   Only the faces in use: Instrument Serif Italic for the lines, DM Sans
   400/500 for the rest (both kept verbatim from the R1 build), and Dancing
   Script 500 (OFL, Google Fonts' latin subset) for the one handwritten
   hero line. font-display: block on the script so the hero never starts
   writing itself in a fallback face. */
@font-face {
  font-family: "Dancing Script";
  font-style: normal;
  font-weight: 500;
  font-display: block;
  src: url("assets/fonts/dancing-script-latin-500.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Instrument Serif";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("assets/fonts/instrument-serif-latin-italic.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "DM Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("assets/fonts/dm-sans-latin-400.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "DM Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("assets/fonts/dm-sans-latin-500.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ---------------------------------------------------------------- tokens --
   The R1 token block, kept. --act-* are no longer read by anything (the
   world's colour now lives in the WebGL scene, not in section backgrounds)
   but are left in place rather than pulled out of a file that isn't broken. */
:root {
  --canvas:      #000000;
  --surface:     rgba(26, 14, 62, 0.86);
  --ink:         #ffffff;
  --ink-soft:    #ded4f6;
  --accent:      #ffffff;
  --accent-ink:  #2a1650;
  --hairline:        rgba(255, 255, 255, 0.18);
  --hairline-strong: rgba(255, 255, 255, 0.42);

  --act-arrival: #9cc6f2;
  --act-portal:  #b3a6ee;
  --act-world:   #2c1e84;
  --act-return:  #9aa0d8;

  --font:  "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --serif: "Instrument Serif", Georgia, "Times New Roman", serif;
  --script: "Dancing Script", "Instrument Serif", cursive;

  --ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
  --dur-1: 160ms;
  --dur-2: 320ms;

  --gut:  clamp(20px, 5vw, 56px);
  --maxw: 64rem;

  /* --- engine theme tokens (scrollcraft.css tokens layer, overridden) ----- */
  --sc-canvas:      #000;
  --sc-surface:     var(--surface);
  --sc-ink:         #f7f2ff;
  --sc-ink-soft:    var(--ink-soft);
  --sc-accent:      #ffffff;
  --sc-accent-ink:  #20164a;
  --sc-font-display: 'Instrument Serif';
  --sc-font-text:    'DM Sans';
}

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

/* Black document, colour on the stage: Chrome on Android paints its bottom
   nav bar with the document background (verified on tre-nonni-demo,
   2026-09-17). Ignition itself opens black, so this is also just the scene. */
html, body { margin: 0; background: #000; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  font-family: var(--font);
  font-size: 1.0625rem;
  line-height: 1.55;
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; }
:focus-visible { outline: 2px solid #fff; outline-offset: 3px; border-radius: 6px; }

/* Fixed, not absolute: focusing an absolute link at the top of the document
   scrolls the page back to it, so a Tab at the finale threw the reader to
   the start of the flight (and hid the pills it was about to reach). */
.skip {
  position: fixed;
  z-index: 300;
  left: var(--gut);
  top: -4rem;
  padding: 0.75rem 1rem;
  background: var(--accent);
  color: var(--accent-ink);
  border-radius: 999px;
  font-weight: 500;
  text-decoration: none;
}
.skip:focus-visible { top: 1rem; outline-color: var(--accent-ink); }

/* ----------------------------------------------------------------- stage --
   #world sits inside [data-sc-world] (.sc-world once the engine mounts,
   fixed by the engine's own CSS — never restyle that position here). It
   starts transparent so the R1-era stills / plain segments (or nothing, pre-
   JS) show through; html.gl fades it in once the scene has a first frame. */
#world {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100lvh;
  z-index: 130;
  opacity: 0;
}
html.gl #world { opacity: 1; transition: opacity 300ms; }
/* Once the live scene is on, a phone never needs to decode six poster
   stills sitting underneath it. */
html.gl .sc-world__seg { display: none; }
html.no-gl #world,
html.no-gl #scan { display: none; }
@media (prefers-reduced-motion: reduce) {
  #world { display: none; }
}

/* The opening: one screen of plain flow above the flight. It holds nothing;
   the fixed stage shows through it, and site.js draws the studio's LiDAR
   scan on #scan (above #world, same box) while the page is inside it, so the
   first scroll travels down into the flight. svh, so the toolbar sliding away never moves the flight's
   top. Not laid out without the engine or under reduced motion, where the
   page opens on the flight as before. */
.intro { height: 100vh; height: 100svh; pointer-events: none; }
#scan {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100lvh;
  z-index: 131;
  display: block;
}
#scan[hidden] { display: none; }
html:not(.js) .intro { display: none; }
@media (prefers-reduced-motion: reduce) {
  .intro { display: none; }
}

/* ------------------------------------------------------------------ copy --
   Base typographic voice for every [data-sc-copy] block. Positioning is the
   engine's .sc-copy / .sc-copy--lead / --center / --trail vocabulary, plus
   one local addition (--lead-top) for the one window that anchors high. */
.line {
  margin: 0;
  max-width: 13ch;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(2.25rem, 8.5vw, 4.75rem);
  line-height: 1.04;
  letter-spacing: -0.01em;
  text-wrap: balance;
  color: var(--sc-ink);
  text-shadow: 0 1px 22px rgba(0, 0, 0, 0.55);
}
h1.line, h2.line { font-size: clamp(2.6rem, 11vw, 6rem); max-width: 12ch; }

/* The hero writes itself out: a left-to-right mask with a soft leading edge
   (1.2em, about a letter), so it reads as a pen moving, not a hard wipe.
   One line (nowrap), since a wipe across two lines writes both at once.
   The padding/negative margin grow the border box, which is the mask's
   painting area, around the script's swashes and descenders.
   Opt-in only: head.js adds html.write-wait (hidden) and swaps it for
   html.write-go once the face has loaded and the engine has mounted, or
   drops it after 1.5 s so the line shows static. No JS, reduced motion or
   no mask support: no class, the line is simply there. The mask sits on the
   h1, the engine's hero fade on its block, so the two never fight.
   The gradient is sized 2W + 1.2em: at position 100% the box sees only its
   transparent half, at 0% only its opaque half. */
h1.line.write {
  font-family: var(--script);
  font-style: normal;
  font-weight: 500;
  font-size: clamp(3rem, 15vw, 6.5rem);
  line-height: 1.2;
  letter-spacing: 0;
  max-width: none;
  white-space: nowrap;
  padding: 0.12em 0.35em;
  margin: -0.12em -0.35em;
}
html.write-wait h1.line.write,
html.write-go h1.line.write {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(50% - 0.6em), transparent calc(50% + 0.6em));
  mask-image: linear-gradient(90deg, #000 calc(50% - 0.6em), transparent calc(50% + 0.6em));
  -webkit-mask-size: calc(200% + 1.2em) 100%;
  mask-size: calc(200% + 1.2em) 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: 100% 0;
  mask-position: 100% 0;
}
html.write-go h1.line.write { animation: write 1.6s cubic-bezier(0.3, 0.12, 0.6, 0.94) both; }
@keyframes write {
  from { -webkit-mask-position: 100% 0; mask-position: 100% 0; }
  to   { -webkit-mask-position: 0 0; mask-position: 0 0; }
}
@media (prefers-reduced-motion: reduce) {
  html.write-wait h1.line.write,
  html.write-go h1.line.write { -webkit-mask-image: none; mask-image: none; animation: none; }
}

.sc-copy--lead-top {
  inset-inline: var(--gut);
  top: clamp(4.5rem, 14vh, 8rem);
  bottom: auto;
}

/* Centred blocks are centred with both insets and auto margins, never the
   engine's .sc-copy--center (left: 50% + translate -50%): below 860px the
   engine's mobile rule sets inset-inline on .sc-copy after it, which resets
   left to the gutter while the -50% translate stays, and the finale hung off
   the left edge at 390 ("s this."). */

/* The vortex line: centred across, in the upper third. Dead centre put it on
   the tube's light core (the vanishing point), 1.02:1 on the composited
   frame. Its plate is a soft ellipse on the text only, so the core stays lit
   below it. */
.sc-copy.sc-copy--crown {
  inset-inline: var(--sc-gutter);
  margin-inline: auto;
  max-width: 40rem;
  top: 18%;
  bottom: auto;
  text-align: center;
}
.sc-copy--crown .line { margin-inline: auto; max-width: 18ch; }
/* "eye-catching" never breaks at its hyphen ("eye- / catching" at 1440). */
.nowrap { white-space: nowrap; }
.sc-world__scrim.sc-scrim--crown {
  background: radial-gradient(ellipse min(62rem, 95%) 26% at 50% 27%,
    color-mix(in oklab, var(--sc-canvas) 90%, transparent) 0%,
    color-mix(in oklab, var(--sc-canvas) 74%, transparent) 50%,
    transparent 100%);
}

/* Arrival: the camera has turned left at the crossing and settles in the
   north transept, facing its end wall, with the rose (a dark round opening
   in stone tracery) framed high: nave.js aims its centre at AIM_Y, 26% of
   the screen height. No light there since R2.1. The finale sits centred on
   the dark wall below the rose: the title, then the pills (solid, dark ink
   on the white accent). The title and each pill are separate copy blocks on
   the same window, so the contrast pass grades each against what is really
   behind it. .finale is only layout. */
.finale {
  position: absolute;
  inset-inline: var(--sc-gutter);
  bottom: calc(50% - 5.5rem);   /* title + pills (~11rem) centred on the screen */
  margin-inline: auto;
  max-width: 40rem;
  text-align: center;
}
.finale__title .line { margin-inline: auto; }
/* Each copy window carries its own sibling scrim (never a pseudo-element on
   the text itself, so a harness that strips pseudo-elements to read the copy
   still sees a real contrast layer). All are absolute/inset:0 inside the
   fixed copy layer; each is weighted toward its own window's anchor and
   fades to nothing well short of the opposite edge, so stacking six of them
   reads as a soft vignette rather than a flattened frame. */
/* Hidden until site.js hands each scrim its block's opacity (every frame, in
   every state). If site.js never runs, the copy still carries its own text
   shadow over a black stage. */
html.js .sc-world__scrim { opacity: 0; }

.sc-world__scrim.sc-scrim--lead-top {
  background: linear-gradient(to bottom,
    color-mix(in oklab, var(--sc-canvas) 88%, transparent) 0%,
    color-mix(in oklab, var(--sc-canvas) 60%, transparent) 20%,
    color-mix(in oklab, var(--sc-canvas) 20%, transparent) 40%,
    transparent 60%);
}

/* ---------------------------------------------------------------- finale --
   Pills for the closing CTA. Min 48px hit target either way. */
.pills {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
  margin: 1.5rem 0 0;
}
.pill {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding: 0 1.25rem;
  border: 1px solid var(--hairline-strong);
  border-radius: 999px;
  color: var(--sc-ink);
  font-weight: 500;
  text-decoration: none;
  overflow-wrap: anywhere;
  transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.pill:hover { border-color: #fff; background: rgba(255, 255, 255, 0.08); }
.pill--solid { background: var(--sc-accent); color: var(--sc-accent-ink); border-color: transparent; }
.pill--solid:hover { background: rgba(255, 255, 255, 0.86); }

/* The engine re-anchors both corner anchors to the full width below 860px.
   Below 640px (phones) the lead scrim becomes the same bottom band the
   engine already gives the trail one, so the density sits where the text
   is; between the two the corner gradient still covers it. */
@media (max-width: 640px) {
  .sc-scrim--lead {
    background: linear-gradient(to top,
      color-mix(in oklab, var(--sc-canvas) 94%, transparent) 0%,
      color-mix(in oklab, var(--sc-canvas) 74%, transparent) 26%,
      color-mix(in oklab, var(--sc-canvas) 30%, transparent) 48%,
      transparent 64%);
  }
}

html.bare .sc-world__copy,
html.bare [data-sc-world-copy] { display: none; }

/* ------------------------------------------------------------- no-script --
   Default state is visible, always (see [[reveal-on-scroll-must-be-opt-in]]):
   if the engine never mounts, the copy is a plain readable page rather than
   six absolutely-positioned blocks pinned to one screen. head.js's watchdog
   strips html.js if scrollcraft.js never reaches sc-ready, so this also
   covers "JS ran but the engine failed", not only "no JS at all". */
html:not(.js) [data-sc-world] {
  position: static;
  height: auto;
}
html:not(.js) #world { display: none; }
html:not(.js) [data-sc-segment] { display: none; }
html:not(.js) [data-sc-spacer] { display: none; }
html:not(.js) [data-sc-world-copy] {
  position: static;
  inset: auto;
  display: block;
  padding: calc(var(--gut) * 2) 0;
}
html:not(.js) [data-sc-copy] {
  position: static;
  inset: auto;
  opacity: 1;
  pointer-events: auto;
  transform: none;
  translate: none;
  max-width: none;
  padding: var(--gut);
}
html:not(.js) .sc-world__scrim { display: none; }
html:not(.js) .finale { position: static; padding: 0 var(--gut) calc(var(--gut) * 2); }
html:not(.js) .finale [data-sc-copy] { padding: 0; }
html:not(.js) .finale .pill { padding: 0 1.25rem; }
