/* ============================================================
   Cyber-Oracle — jasonosaj.me
   Dark, nocturnal, technical. Nature fused with data.
   One theme (dark), one accent (icy blue), violet + antique gold rare.
   Hand-written; no framework. Motion is restrained and reduced-motion aware.
   ============================================================ */

/* ---- Fonts (self-hosted, no CDN at runtime) ---- */
@font-face { font-family: "Space Grotesk"; src: url("/assets/fonts/space-grotesk-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: "Space Grotesk"; src: url("/assets/fonts/space-grotesk-700.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: "Newsreader"; src: url("/assets/fonts/newsreader-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Newsreader"; src: url("/assets/fonts/newsreader-400-italic.woff2") format("woff2");
  font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: "Newsreader"; src: url("/assets/fonts/newsreader-600.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: "Space Mono"; src: url("/assets/fonts/space-mono-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap; }

/* ---- Tokens ---- */
:root {
  --bg:        #0B0D10;   /* near-black ground */
  --panel:     #12161C;   /* charcoal panel */
  --elev:      #171C24;   /* elevated panel */
  --hair:      rgba(233, 238, 245, 0.10);   /* silver hairline */
  --hair-2:    rgba(233, 238, 245, 0.16);

  --text:      #E9EEF5;   /* moonlight white */
  --dim:       #AEB7C4;   /* dimmed body (verified >=4.5:1 on --bg) */
  --faint:     #7C8797;   /* meta / captions (large text only) */

  --accent:    #7FC4FF;   /* icy blue — the single accent */
  --accent-ink:#0B1622;   /* text on accent fills */
  --violet:    #B9A6FF;   /* rare second accent (MAI) */
  --gold:      #CBAA6E;   /* antique gold — bridges to the Gardener-era cards, rare */
  --road:      #E8B55C;   /* lit gold — the movement thread, a path you travel */

  /* Brushed-silver material for the wordmark and single accent words. */
  --chrome-grad: linear-gradient(100deg, #A8B2C0 0%, #FFFFFF 20%, #C3D0E0 38%,
    #EAF2FB 54%, #A8B2C0 74%, #FFFFFF 100%);

  --ff-display:"Space Grotesk", ui-sans-serif, system-ui, sans-serif;
  --ff-body:   "Newsreader", Georgia, "Times New Roman", serif;
  --ff-mono:   "Space Mono", ui-monospace, "SFMono-Regular", monospace;

  --wrap: 1200px;
  /* The journal's binding, defined once. The hero spine and every entry-card
     spine are the SAME coil - previously 2px/16px and 2px/5px, which read as two
     different bindings on one page. Anything drawing a coil uses these three. */
  --coil-rung:   2px;    /* height of one ring */
  --coil-period: 16px;   /* ring to ring */
  --coil-w:      14px;   /* how far a ring crosses the page */
  --coil-ink:    rgba(203, 170, 110, .34);
  /* Radius scale, documented so the mixed values are a rule and not drift:
     panels + buttons = --r, imagery = --r-img, chat bubbles = --r-bubble. */
  --r: 4px;
  --r-img: 2px;
  --r-bubble: 12px;

  --s1: 0.5rem;  --s2: 0.875rem;  --s3: 1.25rem;  --s4: 2rem;
  --s5: 3rem;    --s6: 4.5rem;    --s7: 7rem;

  /* ---- Motion ------------------------------------------------------------
     The curves are the cubic-bezier equivalents of the GSAP eases this codebase
     already uses, so the CSS layer and the GSAP layer speak ONE language.
     Only the three the stylesheet actually needs are declared. If CSS ever has
     to match the other two GSAP eases in use, these are their values:
       expo.out     = cubic-bezier(.19,1,.22,1)    (trellis members, fx.js)
       power3.inOut = cubic-bezier(.77,0,.175,1)   (book open, journal.js)
     Durations are named by ROLE, not by number. Exit is deliberately ~65% of
     enter so dismissals feel quicker than arrivals. */
  --ease-out:      cubic-bezier(.215,.61,.355,1);   /* = gsap power2.out */
  --ease-out-firm: cubic-bezier(.165,.84,.44,1);    /* = gsap power3.out */
  --ease-exit:     cubic-bezier(.4,0,1,1);          /* accelerate away */

  --dur-micro: .12s;   /* press */
  --dur-fast:  .2s;    /* hover, colour */
  --dur-exit:  .28s;   /* leaving */
  --dur-base:  .35s;   /* state change */
  --dur-mid:   .45s;   /* small move */
  --dur-enter: .6s;    /* element arriving */
  --dur-slow:  .8s;    /* large reveal */
  --dur-draw:  1.6s;   /* motif line draw */

  /* ---- Type scale --------------------------------------------------------
     One step per ROLE. Replaces 38 distinct values across 65 declarations. */
  --fs-mono-xs: .68rem;
  --fs-mono:    .72rem;
  --fs-mono-lg: .78rem;
  --fs-ui:      .95rem;
  --fs-ui-lg:   1.06rem;
  /* Exactly 16px. Anything smaller makes iOS zoom the page on focus, so this
     one is pinned and must not be folded into --fs-ui. */
  --fs-input:   1rem;
  --fs-body:    clamp(1.02rem, .98rem + .25vw, 1.15rem);
  --fs-body-lg: clamp(1.06rem, 1rem + .3vw, 1.2rem);
  --fs-lede:    clamp(1.15rem, 1rem + .7vw, 1.5rem);
  --fs-h4:      clamp(1.1rem, .95rem + .6vw, 1.45rem);
  --fs-h3:      clamp(1.2rem, 1rem + .9vw, 1.75rem);
  --fs-h2:      clamp(1.5rem, 1.2rem + 1.1vw, 2.1rem);
  --fs-h2-lg:   clamp(1.8rem, 1.35rem + 1.9vw, 2.7rem);
  --fs-h1:      clamp(2.2rem, 1.5rem + 3vw, 3.6rem);
  --fs-display: clamp(1.4rem, 1rem + 1.9vw, 2.5rem);
}

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--ff-body);
  font-size: var(--fs-body);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
img, svg, iframe { display: block; max-width: 100%; }
img { height: auto; border-radius: var(--r-img); }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }
h1, h2, h3, h4 { font-family: var(--ff-display); font-weight: 700; line-height: 1.08;
  letter-spacing: -0.015em; text-wrap: balance; }
strong { color: var(--text); font-weight: 600; }
em { font-style: italic; }
hr { border: 0; border-top: 1px solid var(--hair); margin: var(--s4) 0; }

/* ---- Atmospheric ground: static starfield + faint data-grid ----
   The static gradient (::before) is the always-present base and the fallback
   when the canvas/WebGL field in #fx-field can't run (reduced-motion, no-JS,
   WebGL failure). fx.js paints the living node-graph + nebula into #fx-field. */
body::before {
  content: ""; position: fixed; inset: 0; z-index: -3; pointer-events: none;
  background:
    radial-gradient(1px 1px at 18% 24%, rgba(233,238,245,.35), transparent 60%),
    radial-gradient(1px 1px at 72% 16%, rgba(233,238,245,.28), transparent 60%),
    radial-gradient(1px 1px at 44% 62%, rgba(233,238,245,.22), transparent 60%),
    radial-gradient(1px 1px at 88% 74%, rgba(233,238,245,.30), transparent 60%),
    radial-gradient(1px 1px at 8% 82%, rgba(233,238,245,.20), transparent 60%),
    radial-gradient(1px 1px at 60% 40%, rgba(127,196,255,.28), transparent 60%),
    radial-gradient(1200px 700px at 78% -8%, rgba(127,196,255,.06), transparent 70%),
    radial-gradient(900px 600px at 8% 108%, rgba(185,166,255,.05), transparent 70%);
}
body::after {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; opacity: .5;
  background-image:
    linear-gradient(var(--hair) 1px, transparent 1px),
    linear-gradient(90deg, var(--hair) 1px, transparent 1px);
  background-size: 64px 64px;
  -webkit-mask-image: radial-gradient(120% 90% at 50% 0%, #000 15%, transparent 72%);
          mask-image: radial-gradient(120% 90% at 50% 0%, #000 15%, transparent 72%);
}

/* ============================================================
   Phase 4 — GSAP foundation: smooth scroll + native page transitions
   ============================================================ */

/* ScrollSmoother wrapper. Inert by default: with no smoother these are plain
   divs and the page lays out exactly as before. fx.js adds html.smooth only
   once ScrollSmoother actually initialises. */
#smooth-wrapper, #smooth-content { width: 100%; }
:root.smooth .site-head { position: fixed; left: 0; right: 0; }
:root.smooth #smooth-content { padding-top: var(--head-h, 64px); }

/* Cross-document view transitions. ~5 lines for a genuinely premium page
   change, no library and no routing hijack; unsupported browsers simply
   navigate normally. */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: vt-out var(--dur-exit) var(--ease-exit) both; }
::view-transition-new(root) { animation: vt-in var(--dur-exit) var(--ease-out) both; }
@keyframes vt-out { to { opacity: 0; transform: translateY(-8px); } }
@keyframes vt-in { from { opacity: 0; transform: translateY(10px); } }
/* Chrome persists across the transition instead of cross-fading with the page */
.site-head { view-transition-name: site-head; }
/* The gate -> hero morph. Slower than the root cross-fade on purpose: this one is
   a camera move into a space, and at --dur-exit it reads as a glitch rather than
   a movement. The UA default cross-fades the two images while it morphs the box,
   which is exactly what is wanted, so only the timing is overridden. */
::view-transition-group(gate-media) {
  animation-duration: var(--dur-slow); animation-timing-function: var(--ease-out-firm); }
#fx-field { view-transition-name: fx-field; }

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* ---- Layout ---- */
.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: clamp(1.1rem, 4vw, 2.5rem); }
.section { padding-block: clamp(3.5rem, 8vw, var(--s7)); }
.section + .section { padding-top: 0; }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ---- Shared type helpers ---- */
/* p.eyebrow (0,1,1) wins over container `p` color rules like .section-head p */
.eyebrow, p.eyebrow { font-family: var(--ff-mono); font-size: var(--fs-mono); letter-spacing: .22em;
  text-transform: uppercase; color: var(--accent); margin-bottom: var(--s2); }
.lede { font-size: var(--fs-lede); line-height: 1.5; color: var(--dim); max-width: 34ch; }
.prose { max-width: 66ch; }
.prose > * + * { margin-top: var(--s3); }
.prose h2 { font-size: var(--fs-h2); margin-top: var(--s5); }
.prose h3 { font-size: var(--fs-h4); margin-top: var(--s4); color: var(--text); }
.prose p, .prose li { color: var(--dim); }
.prose a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.prose ul, .prose ol { padding-left: 1.2em; }
.prose li + li { margin-top: .4em; }
.prose blockquote { border-left: 2px solid var(--gold); padding-left: var(--s3);
  font-style: italic; color: var(--text); }

/* ---- Skip link ---- */
.skip { position: fixed; top: -100px; left: 1rem; z-index: 100; background: var(--accent);
  color: var(--accent-ink); padding: .6rem 1rem; border-radius: var(--r); font-family: var(--ff-display); }
.skip:focus { top: 1rem; }

/* ---- Focus ---- */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--r-img); }

/* ---- Buttons ---- */
.btn {
  --bg-btn: transparent;
  display: inline-flex; align-items: center; gap: .5em; white-space: nowrap;
  font-family: var(--ff-display); font-weight: 500; font-size: var(--fs-ui); line-height: 1;
  padding: .85em 1.4em; border-radius: var(--r); border: 1px solid var(--hair-2);
  color: var(--text); background: var(--bg-btn);
  transition: transform var(--dur-micro) var(--ease-out), border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.btn:hover { text-decoration: none; border-color: var(--accent); color: var(--accent); }
.btn:active { transform: translateY(1px); }
.btn-solid { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn-solid:hover { background: #9BD1FF; border-color: #9BD1FF; color: var(--accent-ink); }
.btn-lg { padding: 1em 1.7em; font-size: var(--fs-ui-lg); }
.btn-head { padding: .6em 1.1em; }

/* ---- Header ---- */
.site-head { position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(10px); border-bottom: 1px solid var(--hair); }
.head-inner { display: flex; align-items: center; gap: var(--s3); min-height: 64px; }
/* Wordmark: NOSAJASON as machined silver ("silver hardware", per MAI canon).
   The gradient's darkest stop is #A8B2C0 so the mark still clears 4.5:1 even
   sitting over the lit part of a hero photo. */
.brand { font-family: var(--ff-display); font-weight: 700; font-size: var(--fs-ui-lg);
  text-transform: uppercase; letter-spacing: .085em; }
.brand, .foot-name { background: var(--chrome-grad); background-size: 260% 100%;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: sheen 8s linear infinite; }
.brand:hover { text-decoration: none; filter: brightness(1.18); }
.foot-name { font-family: var(--ff-display); font-weight: 700; font-size: var(--fs-ui-lg);
  text-transform: uppercase; letter-spacing: .085em; }
.nav { margin-left: auto; }
.nav ul { display: flex; gap: clamp(.9rem, 2vw, 1.6rem); list-style: none; padding: 0; }
.nav a { font-family: var(--ff-display); font-weight: 500; font-size: var(--fs-ui); color: var(--dim);
  padding-block: .4rem; }
.nav a:hover { color: var(--text); text-decoration: none; }
.head-actions { display: flex; align-items: center; gap: var(--s2); }
.ask-mai[hidden] { display: none; }
.nav-toggle { display: none; }

/* active nav state per page (set via body[data-page]) */
body[data-page="movement"]  .nav a[data-nav="movement"],
body[data-page="sound"]     .nav a[data-nav="sound"],
body[data-page="journal"]   .nav a[data-nav="journal"],
body[data-page="resources"] .nav a[data-nav="resources"],
body[data-page="about"]     .nav a[data-nav="about"] { color: var(--text); }
body[data-page="movement"]  .nav a[data-nav="movement"]::after,
body[data-page="sound"]     .nav a[data-nav="sound"]::after,
body[data-page="journal"]   .nav a[data-nav="journal"]::after,
body[data-page="resources"] .nav a[data-nav="resources"]::after,
body[data-page="about"]     .nav a[data-nav="about"]::after {
  content: ""; display: block; height: 1px; background: var(--accent); margin-top: 3px; }

/* ---- Hero ---- */
.hero { position: relative; padding-top: clamp(2.5rem, 6vw, 5.5rem);
  padding-bottom: clamp(3rem, 7vw, 6rem); }
.hero-grid { display: grid; grid-template-columns: 1.25fr 0.75fr; gap: clamp(1.5rem, 4vw, 4rem);
  align-items: center; }
.hero h1 { font-size: var(--fs-h1); }
.hero .lede { margin-top: var(--s3); max-width: 40ch; }
.hero-actions { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s4); }
.hero-figure { position: relative; }
.hero-figure img { width: 100%; border: 1px solid var(--hair-2); }
.hero-figure figcaption { font-family: var(--ff-mono); font-size: var(--fs-mono); color: var(--faint);
  margin-top: var(--s2); letter-spacing: .04em; }

/* ---- Section head ---- */
.section-head { max-width: 52ch; margin-bottom: var(--s5); }
.section-head h2 { font-size: var(--fs-h2-lg); }
.section-head p { color: var(--dim); margin-top: var(--s3); }

/* ---- Split feature (asymmetric text + image) ---- */
.split { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1.5rem, 5vw, 4.5rem); align-items: center; }
.split.reverse > :first-child { order: 2; }
.split-media img { width: 100%; border: 1px solid var(--hair-2); }
.split-body h2 { font-size: var(--fs-h2-lg); }
.split-body > * + * { margin-top: var(--s3); }
.split-body p { color: var(--dim); }

/* ---- Principles (numbered node list) ---- */
.principles { list-style: none; padding: 0; display: grid; gap: 0;
  border-top: 1px solid var(--hair); }
.principles li { display: grid; grid-template-columns: auto 1fr; gap: var(--s3);
  padding: var(--s3) 0; border-bottom: 1px solid var(--hair); align-items: baseline; }
.principles .n { font-family: var(--ff-mono); font-size: var(--fs-mono-lg); color: var(--accent); }
.principles h3 { font-size: var(--fs-h4); color: var(--text); display: inline; }
.principles p { color: var(--dim); margin-top: .3rem; }
/* That margin exists to clear the h3 above it on /about/. The EPK's facts list
   uses these rows as label/value with no h3, so the p is first-child there and
   the margin pushed the value 5px off the label's baseline. */
.principles li > div > p:first-child { margin-top: 0; }

/* ---- Releases (sound / EPK) ---- */
.releases { display: grid; gap: 0; border-top: 1px solid var(--hair); }
.release { display: grid; grid-template-columns: 1fr auto; gap: var(--s3) var(--s4);
  padding: var(--s4) 0; border-bottom: 1px solid var(--hair); align-items: start; }
.release h3 { font-size: var(--fs-h4); color: var(--text); }
.release .rel-label { font-family: var(--ff-mono); font-size: var(--fs-mono); color: var(--faint);
  letter-spacing: .08em; text-transform: uppercase; margin-bottom: var(--s1); }
.release p { color: var(--dim); margin-top: var(--s1); max-width: 52ch; }
.release .rel-links { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s2);
  font-family: var(--ff-display); font-size: var(--fs-ui); }
.embed { position: relative; }
.embed iframe { width: 100%; border: 1px solid var(--hair-2); border-radius: var(--r-img); }

/* ---- Article (single post) ---- */
.article { padding-block: clamp(2.5rem, 6vw, 4.5rem); }
.article-head { max-width: 66ch; margin-bottom: var(--s5); }
.article-head h1 { font-size: var(--fs-h2-lg); }
.article-head .meta { font-family: var(--ff-mono); font-size: var(--fs-mono-lg); color: var(--faint);
  letter-spacing: .06em; margin-bottom: var(--s3); text-transform: uppercase; }
.article-body { font-size: var(--fs-body-lg); }
.article-body h2 { color: var(--text); }
.back-link { font-family: var(--ff-mono); font-size: var(--fs-mono-lg); letter-spacing: .05em; }

/* ---- Resources (downloads) ---- */
.resource { display: grid; grid-template-columns: minmax(140px, 200px) 1fr; gap: var(--s4);
  padding: var(--s4); border: 1px solid var(--hair); border-radius: var(--r); background: var(--panel);
  align-items: start; }
.resource + .resource { margin-top: var(--s3); }
/* These are the real book covers, and they carry their own palettes (black/yellow,
   green) that deliberately do NOT match the site's single-accent lock. Rather than
   recolour them — which would misrepresent the actual PDFs — they are presented as
   OBJECTS sitting on the dark ground, the way sleeve art sits on a music site.
   A gold hairline, a lift shadow and a spine edge do that work. */
/* ---- The shelf ----
   /resources/'s member, and it now holds the actual goods. The previous version
   was a 1px rule floating inside the copy block at an arbitrary height, aligned
   to nothing, which is exactly why it read as a stray underline. */
.hero-shelf { min-height: min(92dvh, 780px); }
.shelf-grid { display: grid; grid-template-columns: 1fr .9fr;
  gap: clamp(2rem, 6vw, 5rem); align-items: center; }
.shelf { position: relative; display: flex; align-items: flex-end;
  justify-content: center; gap: 0; padding-bottom: 26px; }
.shelf-cover { width: clamp(120px, 15vw, 190px); height: auto; display: block;
  border: 1px solid rgba(203,170,110,.5); border-radius: var(--r-img);
  box-shadow: 0 26px 54px rgba(0,0,0,.62), 0 3px 10px rgba(0,0,0,.5);
  transition: transform var(--dur-enter) var(--ease-out-firm); }
/* two objects standing on a shelf: angled, overlapping, the near one in front */
.shelf-cover-a { transform: rotate(-6deg) translateY(4px); z-index: 1; }
.shelf-cover-b { transform: rotate(5deg); margin-left: -14%; z-index: 2; }
.shelf:hover .shelf-cover-a { transform: rotate(-8deg) translateY(-4px); }
.shelf:hover .shelf-cover-b { transform: rotate(7deg) translateY(-4px); }
/* the ledge they stand on: full width of the shelf, with the light under them */
.shelf-ledge { position: absolute; left: -6%; right: -6%; bottom: 20px; height: 1px;
  transform-origin: 0 50%; pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(203,170,110,.62) 18%,
    rgba(203,170,110,.62) 82%, transparent); }
.shelf-ledge::after { content: ""; position: absolute; left: 12%; right: 12%; top: 0;
  height: 42px; background: radial-gradient(60% 100% at 50% 0,
    rgba(203,170,110,.16), transparent 70%); }
:root.js .shelf-ledge { animation: ledge-draw var(--dur-slow) var(--ease-out-firm) both; }
:root.js .shelf-cover { animation: shelf-set var(--dur-enter) var(--ease-out-firm) both; }
.shelf-cover-b { animation-delay: .12s; }
@keyframes ledge-draw { from { transform: scaleX(0); } }
@keyframes shelf-set { from { opacity: 0; transform: translateY(18px) rotate(0deg); } }

@media (max-width: 860px) {
  .shelf-grid { grid-template-columns: 1fr; }
  .shelf { padding-bottom: 18px; }
  .shelf-cover { width: clamp(104px, 32vw, 150px); }
}

.resource-cover { position: relative; }
.resource-cover img { display: block; width: 100%; border: 1px solid rgba(203,170,110,.5);
  border-radius: var(--r-img);
  box-shadow: 0 18px 40px rgba(0,0,0,.55), 0 2px 6px rgba(0,0,0,.4); }
/* the stacked page edges of a real book, on the binding side */
.resource-cover::after { content: ""; position: absolute; left: -3px; top: 5%; bottom: 5%;
  width: 3px; border-radius: var(--r-img) 0 0 var(--r-img); pointer-events: none;
  background: repeating-linear-gradient(180deg,
    rgba(203,170,110,.34) 0 2px, transparent 2px 5px); }
.resource h3 { font-size: var(--fs-h3); color: var(--text); }
.resource p { color: var(--dim); margin-top: var(--s2); }
.resource-actions { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s3); align-items: center; }

/* ---- Email opt-in (optional, non-blocking) ---- */
.optin { margin-top: var(--s5); padding: var(--s4); border: 1px solid var(--hair); border-radius: var(--r);
  background: var(--elev); max-width: 48rem; }
.optin h3 { color: var(--text); font-size: var(--fs-h4); }
.optin p { color: var(--dim); margin-top: var(--s1); font-size: var(--fs-ui); }
.optin form { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s3); align-items: center; }
.optin input[type="email"] { flex: 1 1 16rem; background: var(--bg); color: var(--text);
  border: 1px solid var(--hair-2); border-radius: var(--r); padding: .8em 1em; font-family: var(--ff-body);
  font-size: var(--fs-input); }
.optin input::placeholder { color: var(--faint); }
.optin input:focus-visible { border-color: var(--accent); }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---- Book (Cal.com) ---- */
.book-embed { border: 1px solid var(--hair-2); border-radius: var(--r); overflow: hidden;
  background: var(--panel); min-height: 640px; }
.book-embed iframe { width: 100%; min-height: 640px; border: 0; }

/* ---- CTA band ---- */
/* The scrim runs ACROSS, not down. A flat .86-.90 overlay was hiding the
   photograph completely - measured, it lifted the band by +0.6 luma out of 255,
   so 197KB shipped on every page for something invisible. It could not simply be
   lightened either: --dim body copy falls to 3.53:1 at a flat .68, under the
   4.5:1 floor.
   Directional solves both, because the band's own layout cooperates: .cta-inner
   is 1fr auto, so the heading and copy sit LEFT while the button sits RIGHT -
   and this photograph's left third is empty sky with the moon and detail right
   of centre. So the left stays heavily scrimmed for the type and the right opens
   up behind the button, which is solid --accent and needs no protection.
   Measured at 1440x314: --dim holds 7.28:1 while the photograph's contribution
   goes from +2.8 to +9.4. The 10% crop keeps the moon in frame. */
.cta-band { border-top: 1px solid var(--hair); border-bottom: 1px solid var(--hair);
  background:
    radial-gradient(700px 300px at 85% 0%, rgba(127,196,255,.09), transparent 70%),
    linear-gradient(90deg, rgba(11,13,16,.92) 0%, rgba(11,13,16,.88) 55%,
      rgba(11,13,16,.35) 100%),
    url("/assets/img/atmo-forest.jpg") center 10% / cover no-repeat,
    var(--panel);
  padding-block: clamp(3rem, 6vw, 5rem); }
.cta-inner { display: grid; grid-template-columns: 1fr auto; gap: var(--s4); align-items: center; }
.cta-inner h2 { font-size: var(--fs-h2-lg); }
.cta-copy { color: var(--dim); margin-top: var(--s2); max-width: 46ch; }

/* ---- Footer ---- */
.site-foot { border-top: 1px solid var(--hair); margin-top: 0; padding-top: var(--s6); padding-bottom: var(--s4); }
.foot-inner { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: var(--s4); }
.foot-name { font-family: var(--ff-display); font-weight: 700; font-size: var(--fs-ui-lg); color: var(--text); }
.foot-tag { color: var(--dim); margin-top: var(--s1); max-width: 34ch; }
.foot-loc { color: var(--faint); font-family: var(--ff-mono); font-size: var(--fs-mono-lg); margin-top: var(--s2); }
.foot-nav ul, .social { list-style: none; padding: 0; display: grid; gap: .5rem; }
.foot-nav a, .social a { color: var(--dim); font-family: var(--ff-display); font-size: var(--fs-ui); }
.foot-nav a:hover, .social a:hover { color: var(--accent); text-decoration: none; }
.foot-contact { display: grid; gap: var(--s3); align-content: start; }
.social { margin-top: 0; }
.copyright { color: var(--faint); font-family: var(--ff-mono); font-size: var(--fs-mono); letter-spacing: .04em;
  margin-top: var(--s6); padding-top: var(--s3); border-top: 1px solid var(--hair); }

/* ---- Scroll reveal (progressive enhancement) ----
   Hidden state is gated on html.js (set inline in <head>) so no-JS visitors
   always see content. JS adds .in on intersection; reduced-motion shows all. */
.reveal { transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out); }
:root.js .reveal { opacity: 0; transform: translateY(18px); }
:root.js .reveal.in { opacity: 1; transform: none; }

/* ============================================================
   Phase 2 — Visual uplevel (fx.js drives these; all degrade to static)
   ============================================================ */

/* ---- Living atmospheric field (canvas node-graph + WebGL nebula) ---- */
#fx-field { position: fixed; inset: 0; z-index: -2; pointer-events: none; overflow: hidden; }
#fx-field .fx-nebula, #fx-field .fx-nodes { position: absolute; inset: 0; width: 100%; height: 100%; }
#fx-field .fx-nebula { z-index: 0; }
#fx-field .fx-nodes  { z-index: 1; }

/* ---- Full-bleed hero ---- */
.hero-full { position: relative; isolation: isolate; display: flex; align-items: center;
  min-height: min(88vh, 760px); overflow: hidden;
  padding-block: clamp(3rem, 8vw, 6rem); }
.hero-bg { position: absolute; inset: -3%; z-index: -2; will-change: transform;
  background:
    radial-gradient(120% 90% at 78% 8%, rgba(127,196,255,.12), transparent 58%),
    radial-gradient(100% 85% at 6% 105%, rgba(185,166,255,.09), transparent 64%),
    var(--bg); }
/* Two per-page dials, both defaulting to today's values so every existing hero
   is untouched. --focus picks which band of a portrait photo survives the
   cover() crop; --photo-op lifts a subject that reads too dim at .5. The scrim
   is what protects the copy, not the opacity: measured behind the /about/
   headline, .8 still returns 6.5:1 against the 4.5:1 floor. */
.hero-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  object-position: center var(--focus, 50%);
  opacity: var(--photo-op, .5); filter: saturate(1.05) contrast(1.02); }
.hero-sound .hero-photo { opacity: .72; }
.hero-scrim { position: absolute; inset: 0; z-index: -1;
  background:
    linear-gradient(180deg, rgba(11,13,16,.5), rgba(11,13,16,.32) 42%, rgba(11,13,16,.82)),
    linear-gradient(90deg, rgba(11,13,16,.72), rgba(11,13,16,.15) 62%); }
.hero-inner { position: relative; width: 100%; }
.hero-full .hero-grid { display: grid; grid-template-columns: 1.25fr .75fr;
  gap: clamp(1.5rem, 4vw, 4rem); align-items: center; }
.hero-full h1 { font-size: var(--fs-h1); }
.hero-full .lede { margin-top: var(--s3); max-width: 42ch; }

/* Hero headline word reveal — fx.js only splits into words; the animation is
   pure CSS with fill:both so it always ENDS visible, even if nothing toggles a
   class. (A JS-toggled reveal risked leaving the headline permanently hidden.) */
.hero-full h1 .word, .trellis-copy h1 .word { display: inline-block; }
/* Motion only when GSAP is NOT driving it — otherwise the two would double up. */
:root:not(.gsap) .hero-full h1 .word,
:root:not(.gsap) .trellis-copy h1 .word {
  animation: word-in var(--dur-enter) var(--ease-out-firm) both;
  animation-delay: calc(var(--i, 0) * 65ms); }
@keyframes word-in { from { opacity: 0; transform: translateY(.5em); } to { opacity: 1; transform: none; } }

/* ---- Liquid-metal / chrome accent ----
   One shared material. Floor raised to #A8B2C0 (was #7f8a99) so chromed text
   stays >=4.5:1 anywhere it is used, including over a scrimmed hero photo. */
.chrome { background: var(--chrome-grad); background-size: 260% 100%;
  -webkit-background-clip: text; background-clip: text;
  color: transparent; animation: sheen 7s linear infinite; }
@keyframes sheen { to { background-position: -260% 0; } }

/* ---- 3D coverflow oracle gallery ---- */
.coverflow-wrap { position: relative; }
.coverflow { position: relative; display: flex; align-items: center; gap: var(--s3);
  overflow-x: auto; padding-block: var(--s3); scroll-snap-type: x mandatory; }  /* flat fallback */
.coverflow .cf-card { position: static; flex: 0 0 auto; width: min(46vw, 190px);
  aspect-ratio: 603 / 921; scroll-snap-align: center; cursor: pointer;
  border: 1px solid var(--gold); border-radius: var(--r-img); overflow: hidden;
  box-shadow: 0 12px 44px rgba(0,0,0,.5); background: #000; }
.coverflow .cf-card img { width: 100%; height: 100%; object-fit: cover; }
/* 3D mode (fx.js adds .cf-on) */
.coverflow.cf-on { display: block; height: clamp(340px, 44vw, 480px); overflow: visible;
  perspective: 1500px; }
.coverflow.cf-on .cf-card { position: absolute; left: 50%; top: 50%; width: min(62vw, 250px);
  transform-style: preserve-3d; will-change: transform;
  transition: transform var(--dur-enter) var(--ease-out-firm), opacity var(--dur-enter) var(--ease-out);
  box-shadow: 0 26px 70px rgba(0,0,0,.62); }
.cf-controls { display: flex; gap: var(--s2); justify-content: center; margin-top: var(--s3); }
.cf-btn { font-family: var(--ff-display); font-size: var(--fs-ui-lg); line-height: 1; width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center; color: var(--text);
  background: var(--panel); border: 1px solid var(--hair-2); border-radius: var(--r); cursor: pointer; }
.cf-btn:hover { border-color: var(--accent); color: var(--accent); }
.cf-caption { text-align: center; font-family: var(--ff-mono); font-size: var(--fs-mono); color: var(--faint);
  letter-spacing: .08em; text-transform: uppercase; margin-top: var(--s2); }

/* ---- Line-art motifs (draw on scroll) ---- */
.motif { display: block; color: var(--accent); }
.motif svg { width: 100%; height: 100%; overflow: visible; }
.motif [data-draw] { fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round; }
/* The undrawn state is INVISIBLE, so it is gated behind :root.js — without JS
   there is no IntersectionObserver to add .drawn, and an ungated dashoffset
   would leave every motif (and all nine journal plates) permanently blank.
   This is the invisible-content rule applied to the motif system. */
:root.js .motif [data-draw] {
  stroke-dasharray: var(--len, 1200); stroke-dashoffset: var(--len, 1200); }
/* MUST out-specify the :root.js hidden state above (0,4,0), or the motif can
   never draw. Gating the hidden state behind :root.js fixed no-JS invisibility
   and silently broke every motif on the site until this selector matched it. */
:root.js .motif.drawn [data-draw] { transition: stroke-dashoffset var(--dur-draw) var(--ease-out); stroke-dashoffset: 0; }
.motif.drawn [data-draw] { stroke-dashoffset: 0; }
.motif .node { fill: currentColor; stroke: none; opacity: .9; }
:root.js .motif .node { opacity: 0; transition: opacity var(--dur-mid) var(--ease-out) var(--dur-slow); }
:root.js .motif.drawn .node { opacity: .9; }
.motif.drawn .node { opacity: .9; }
.motif-divider { max-width: 120px; margin: clamp(2rem,5vw,3.5rem) auto; opacity: .5; }
.motif-mandala, .motif-loop { display: block; width: clamp(150px, 20vw, 220px);
  margin-inline: auto; opacity: .6; }

/* ============================================================
   THE TRELLIS — home page only. "I build containers, not willpower."
   A frame is erected, the photograph is uncovered inside it pane by pane, and
   then the frame HOLDS while the copy travels through it. Its two verticals sit
   on exact thirds so they resolve into the dividers between the three gates:
   the container becomes the navigation. That is what earns the lines their place.
   ScrollTrigger pins the stage (CSS sticky is the no-GSAP fallback).
   ============================================================ */
:root {
  --trellis-scroll: 120vh;  /* TUNABLE: scroll the pinned sequence consumes */
  /* The two interior verticals. EXACT thirds, because they resolve into the
     dividers between the three gates below. Do not nudge these for looks —
     if they stop lining up with the gates the frame is only decoration. */
  --tr-c1: 33.3333%;
  --tr-c2: 66.6667%;
  /* The horizontals are deliberately NOT evenly spaced. Nothing is ruled. */
  --tr-r1: 30%;
  --tr-r2: 68%;
}

.trellis { position: relative; height: calc(100dvh + var(--trellis-scroll)); }
.trellis-stage { position: sticky; top: 0; height: 100dvh; overflow: hidden;
  display: flex; align-items: center; isolation: isolate; }
/* With GSAP present, ScrollTrigger pins the stage and its pin-spacer supplies the
   scroll distance — so CSS sticky must stand down or the two fight each other. */
:root.gsap .trellis { height: auto; }
:root.gsap .trellis-stage { position: relative; top: auto; }

.trellis-scene { position: absolute; inset: 0; z-index: 0; }
.trellis-photo { width: 100%; height: 100%; object-fit: cover; opacity: .58;
  filter: saturate(1.05) contrast(1.02); }
.trellis-veil { position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    linear-gradient(180deg, rgba(11,13,16,.5), rgba(11,13,16,.28) 45%, rgba(11,13,16,.88)),
    linear-gradient(90deg, rgba(11,13,16,.8), rgba(11,13,16,.1) 66%); }

/* ---- The container itself ---- */
.trellis-frame { position: absolute; inset: 0; z-index: 2; pointer-events: none; }

/* Shutter panes. The photograph does not fade in, it is UNCOVERED pane by pane,
   like panels being taken off a frame.
   RESTING STATE IS TRANSPARENT. GSAP animates FROM opacity 1, so a trigger that
   never fires leaves the photograph visible rather than boarded over. This is the
   invisible-content rule, which this project has been bitten by four times. */
.tr-pane { position: absolute; opacity: 0; background: var(--bg);
  left: var(--l); top: var(--t); width: var(--w); height: var(--h); }

/* Members. Resting state is fully extended; JS scales them from 0. */
.tr-v { position: absolute; top: 0; bottom: 0; left: var(--x); width: 1px;
  transform-origin: 50% 0;
  background: linear-gradient(180deg, transparent, rgba(127,196,255,.4) 12%,
    rgba(127,196,255,.4) 88%, transparent); }
.tr-h { position: absolute; left: 0; right: 0; top: var(--y); height: 1px;
  transform-origin: 0 50%;
  background: linear-gradient(90deg, transparent, rgba(233,238,245,.15) 10%,
    rgba(233,238,245,.15) 90%, transparent); }
/* Nodes mark where members actually meet. No outer glow: a tinted dot, same
   family as the motif nodes elsewhere on the site. */
.tr-node { position: absolute; left: var(--x); top: var(--y);
  width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px; border-radius: 50%;
  background: var(--accent); opacity: .45; }

/* The About portrait uses the same frame at hero scale. Sits above the scrim so
   the members read against the photograph, below the copy so text stays legible. */
/* Jason's name written twice as a sigil, so it closes the page the way a
   signature closes a letter: centred on a hairline, small, desaturated. It is a
   sign-off, not another piece of art competing with the section above it. */
.signature { margin-top: var(--s6); padding-top: var(--s5);
  border-top: 1px solid var(--hair); }
/* margin-inline, not text-align: line 115 makes every img display:block, so
   text-align has nothing inline to centre. */
.signature img { width: clamp(64px, 9vw, 104px); height: auto;
  margin-inline: auto; opacity: .55; filter: saturate(0); }

/* RESTING STATE IS FADED. The frame is the page's argument while it is being
   built, so the reveal starts it at full strength - then it has nothing left to
   do. On the home page these members go on to become the gate dividers, which is
   what earns their weight; here they hand off to nothing, so once the photograph
   is uncovered the container recedes and lets the portrait hold the frame.
   This value is the FINISHED state on purpose: fx.js animates down to it, so a
   script error or reduced motion leaves the quiet frame, never the loud one. */
.about-frame { position: absolute; inset: 0; z-index: 2; pointer-events: none;
  opacity: .6; }
/* The verticals ship at .4 accent against the horizontals' .15 white - nearly 3x
   the alpha, and the only coloured member. Trimmed here since they no longer
   need to survive a handover; hue kept, because this must still read as the same
   container the home page builds. Effective at rest: .26 x .6 = .156. */
.about-frame .tr-v { background: linear-gradient(180deg, transparent,
  rgba(127,196,255,.26) 12%, rgba(127,196,255,.26) 88%, transparent); }
.about-frame .tr-node { opacity: .34; }
.hero-framed .hero-inner { position: relative; z-index: 3; }

.trellis-inner { position: relative; z-index: 3; width: 100%; }
.trellis-grid { display: grid; grid-template-columns: 1.25fr .75fr;
  gap: clamp(1.5rem, 4vw, 4rem); align-items: center; }
/* Hero headline: bigger and tighter for weight, with a bloom behind the type.
   The chrome is applied to the LAST WORD ONLY — a full gradient fill across a
   multi-line headline reads cheap and drags contrast down. Applying it to the
   .word spans (not the h1) also keeps the staggered reveal working, since element
   opacity fades a background-clipped span but not a parent's clipped background. */
.trellis-copy h1 { font-size: var(--fs-h1);
  letter-spacing: -.028em; line-height: 1.03;
  text-shadow: 0 0 42px rgba(127, 196, 255, .17); }
/* Chrome is MATERIAL, not motion — kept separate so whichever system drives the
   reveal (GSAP or the CSS keyframe) never has to also carry the gradient. */
.trellis-copy h1 .word:last-of-type {
  background: var(--chrome-grad); background-size: 260% 100%;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  text-shadow: none;                       /* a shadow under transparent text ghosts */
  animation: sheen 8s linear infinite; }
.trellis-copy .lede { margin-top: var(--s3); max-width: 42ch; }
.trellis-figure img { width: 100%; border: 1px solid var(--hair-2); }
.trellis-figure figcaption { font-family: var(--ff-mono); font-size: var(--fs-mono); color: var(--faint);
  margin-top: var(--s2); letter-spacing: .04em; }

/* ---- Destination gates: full-height expanding panels ----
   The gates OVERLAP the tail of the trellis (negative margin) and their opacity is
   scrubbed by the same scroll progress, so the two cross-dissolve instead of one
   sliding over the other. The top edge is feathered rather than a hard cut, and a
   faint afterglow carries the aperture's light into the new space. */
.gates { position: relative; z-index: 2; min-height: 100dvh; display: flex; flex-direction: column;
  background: linear-gradient(to bottom, rgba(11,13,16,0) 0, var(--bg) 20vh); }
/* The trellis verticals, continued. fx.js scrubs scaleY from 0 as the frame
   releases, so the structure grows down into the gates rather than the two
   sections simply cross-fading past each other. Resting state is full height:
   no JS means these are just quiet dividers, never missing content. */
/* Selector must out-specify `.gates > *` below, which sets position:relative on
   every direct child to lift content above the ::before glow. At equal
   specificity that rule wins on source order, which silently flattened these to
   position:relative -> top/bottom create no height -> zero-height, invisible.
   z-index 3 puts them ABOVE the gate panels, since these replaced the panels'
   border-right and have to be visible at the boundaries. */
.gates > .gates-divider { position: absolute; top: 0; bottom: 0; left: var(--x);
  width: 1px; z-index: 3; transform-origin: 50% 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(127,196,255,.40),
    rgba(127,196,255,.12) 55%, transparent);
  /* opacity ONLY. The trellis scrub writes transform: scaleY() to these inline
     every frame, so transitioning transform would make each frame chase a
     lagging tween - the same bug that hit .gate's opacity during the scrub. */
  transition: opacity var(--dur-mid) var(--ease-out); }

/* The dividers sit at fixed thirds (--tr-c1/--tr-c2) because they inherit the
   trellis columns, but a hovered gate grows to flex-grow: 2.4 - so the boundary
   moves and the line does not, leaving it drawn across the middle of the
   expanded image. They mark boundaries; once one panel is opened there are no
   boundaries to mark, so they step out. Same trigger as the expansion itself
   (.gates-row:hover / :focus-within) so the two can never disagree. */
.gates:has(.gates-row:hover) > .gates-divider,
.gates:has(.gates-row:focus-within) > .gates-divider { opacity: 0; }

/* The overlap only applies once fx.js is actually driving the cross-dissolve.
   Without it (no-JS, reduced motion, script error) the sections simply stack. */
.trellis-on .gates { margin-top: calc(-1 * var(--gates-overlap, 100vh)); }
.gates::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 62vh;
  pointer-events: none; z-index: 0;
  background: radial-gradient(125% 82% at 50% -12%, rgba(127,196,255,.11), transparent 72%); }
.gates > * { position: relative; z-index: 1; }
.gates-head { padding-block: clamp(2rem, 5vw, 3.5rem); }
.gates-head h2 { font-size: var(--fs-h2-lg); max-width: 24ch; }
.gates-row { flex: 1; display: flex; min-height: 0; border-top: 1px solid var(--hair); }

/* No border-right here: .gates-divider already draws the boundary, full-height
   and on the same --tr-c1/--tr-c2 tokens as the trellis frame. Two 1px lines at
   the same x read as one smudged 2px line. */
.gate { position: relative; flex: 1 1 0; min-width: 0; overflow: hidden; isolation: isolate;
  display: flex; align-items: flex-end; text-decoration: none;
  transition: flex-grow var(--dur-enter) var(--ease-out-firm), opacity var(--dur-mid) var(--ease-out), filter var(--dur-mid) var(--ease-out); }
.gate:hover { text-decoration: none; }
.gate-media { position: absolute; inset: 0; z-index: 0; background-size: cover;
  background-position: center; opacity: .34; transform: scale(1.04);
  transition: transform var(--dur-slow) var(--ease-out-firm), opacity var(--dur-mid) var(--ease-out); }
/* per-gate scrim keeps label contrast well above AA over any image */
.gate::after { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(11,13,16,.45) 0%, rgba(11,13,16,.72) 55%, rgba(11,13,16,.93) 100%); }
/* cursor spotlight */
.gate::before { content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; opacity: 0;
  background: radial-gradient(340px circle at var(--mx, 50%) var(--my, 50%),
    rgba(127,196,255,.16), transparent 62%);
  transition: opacity var(--dur-base) var(--ease-out); }
.gate:hover::before, .gate:focus-visible::before { opacity: 1; }

.gate-body { position: relative; z-index: 3; display: block; padding: clamp(1.4rem, 2.4vw, 2.2rem); }
.gate-kicker { display: block; font-family: var(--ff-mono); font-size: var(--fs-mono); letter-spacing: .22em;
  text-transform: uppercase; color: var(--accent); margin-bottom: var(--s2); }
.gate-title { display: block; font-family: var(--ff-display); font-weight: 700; line-height: 1.1;
  letter-spacing: -.015em; color: var(--text); font-size: var(--fs-h3); }
.gate-desc { display: block; color: var(--dim); margin-top: var(--s2); max-width: 34ch;
  font-size: var(--fs-ui); opacity: .55; transition: opacity var(--dur-mid) var(--ease-out); }
.gate-go { display: inline-block; font-family: var(--ff-mono); font-size: var(--fs-mono-lg); letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent); margin-top: var(--s3); }
.gate-go::after { content: " \2192"; }

/* hover/focus: expand this one, recede the others */
.gates-row:hover .gate,
.gates-row:focus-within .gate { flex-grow: .82; opacity: .5; filter: saturate(.55); }
.gates-row .gate:hover,
.gates-row .gate:focus-within { flex-grow: 2.4; opacity: 1; filter: none; }
.gates-row .gate:hover .gate-media,
.gates-row .gate:focus-within .gate-media { transform: scale(1.12); opacity: .5; }
.gates-row .gate:hover .gate-desc,
.gates-row .gate:focus-within .gate-desc { opacity: 1; }

/* Arrive as the frame resolves into them. Default state is VISIBLE — fx.js
   adds .armed only once it can guarantee the reveal, so a JS failure can never
   leave the destinations invisible. */
.gates.armed { opacity: 0; transform: scale(.985);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out-firm); }
.gates.armed.in { opacity: 1; transform: none; }

/* ---- Film grain + vignette (fixed overlay; never on a scrolling container) ---- */
#fx-grain { position: fixed; inset: 0; z-index: 80; pointer-events: none; opacity: .04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
#fx-grain::after { content: ""; position: absolute; inset: 0;
  box-shadow: inset 0 0 24vmax rgba(0,0,0,.5); }

/* ---- Scroll progress ---- */
#fx-progress { position: fixed; top: 0; left: 0; height: 2px; width: 0; z-index: 85;
  background: linear-gradient(90deg, var(--violet), var(--accent)); pointer-events: none; }

/* ============================================================
   Pillar 1 — SOUND: waves and spectrums
   ============================================================ */

/* The waveform occupies the lower band of the hero so it never sits under the
   copy (text over a moving, variable-luminance surface is the thing to avoid).
   20% clears the CTA row at 900px; capped in px so tall viewports don't grow it
   back into the content. */
.sound-wave { position: absolute; left: 0; right: 0; bottom: 0;
  height: min(20%, 190px); z-index: 2; pointer-events: none; width: 100%; }

.sound-player { display: flex; align-items: center; gap: var(--s3); margin-top: var(--s4);
  flex-wrap: wrap; }
.sound-play { display: inline-flex; align-items: center; gap: .7em; cursor: pointer;
  font-family: var(--ff-display); font-weight: 500; font-size: var(--fs-ui); line-height: 1;
  padding: .8em 1.35em; min-height: 44px; border-radius: var(--r);   /* 44px touch min */
  color: var(--text); background: rgba(18,22,28,.72); backdrop-filter: blur(6px);
  border: 1px solid var(--hair-2);
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), transform var(--dur-micro) var(--ease-out); }
.sound-play:hover { border-color: var(--accent); color: var(--accent); }
.sound-play:active { transform: translateY(1px); }
/* play triangle / pause bars, driven by data-state */
.sound-play-icon { width: 12px; height: 14px; position: relative; display: block; }
.sound-play-icon::before, .sound-play-icon::after { content: ""; position: absolute; inset: 0; }
.sound-play[data-state="paused"] .sound-play-icon::before {
  border-left: 11px solid currentColor; border-top: 7px solid transparent;
  border-bottom: 7px solid transparent; width: 0; height: 0; }
.sound-play[data-state="playing"] .sound-play-icon::before,
.sound-play[data-state="playing"] .sound-play-icon::after {
  width: 4px; background: currentColor; }
.sound-play[data-state="playing"] .sound-play-icon::after { left: 8px; }
.sound-track { font-family: var(--ff-display); font-size: var(--fs-ui); color: var(--text);
  display: inline-flex; align-items: baseline; gap: .55em; }
.sound-track-meta { font-family: var(--ff-mono); font-size: var(--fs-mono-xs); letter-spacing: .12em;
  text-transform: uppercase; color: var(--faint); }
.sound-status { font-family: var(--ff-mono); font-size: var(--fs-mono); letter-spacing: .06em;
  color: var(--faint); }

/* Release sparklines. Visible by default; GSAP DrawSVG animates them in when
   available, so a missing library leaves a complete waveform rather than nothing. */
.sparkline { display: block; width: 100%; max-width: 340px; height: 34px;
  margin: var(--s2) 0 var(--s1); color: var(--accent); opacity: .75; }
.sparkline path { fill: none; stroke: currentColor; stroke-width: 1.2;
  stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.release:hover .sparkline { opacity: 1; }

/* char-level heading reveal (SplitText) */
.ch { display: inline-block; will-change: transform; }

@media (max-width: 900px) {
  /* The hero is content-height on mobile, so a percentage band would sit under
     the copy. Fixed band + matching padding guarantees separation at any height. */
  .sound-wave { height: 96px; }
  .hero-sound .hero-inner { padding-bottom: 112px; }
  .sparkline { max-width: none; height: 28px; }
}

/* ============================================================
   Pillar 2 — MOVEMENT: one unbroken line threading the page
   ============================================================ */
/* Hero: the body flows, the ground holds. Fluidity is the pose sequence
   cross-fading as you scroll; stability is the ground rule and the anchors,
   which never move at all. That contrast is the whole point of the page. */
.hero-movement { min-height: 100dvh; }
.hero-movement .hero-bg { overflow: hidden; }
.move-ground { position: absolute; left: 0; right: 0; bottom: 22%; height: 1px; z-index: 2;
  background: linear-gradient(90deg, transparent, rgba(127,196,255,.55) 18%,
    rgba(127,196,255,.55) 82%, transparent); pointer-events: none; }

@media (max-width: 900px) {
  .hero-movement { min-height: 92dvh; }
  .move-ground { bottom: 18%; }
}

.thread-wrap { position: relative; }
.thread { position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 0; overflow: visible; }
/* Fully drawn by default. GSAP DrawSVG animates it when available, so a missing
   library leaves a complete line rather than an empty page. */
/* 10px, not 2.6px. Below ~7px the goo filter's alpha crush erases the stroke
   entirely and the road can never fuse with the pools - it just stays a line. */
.thread [data-thread-path] { fill: none; stroke: url(#road-metal); stroke-width: 6;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-linecap: round; opacity: .45; }
/* The road. Gold, not icy blue: on this page the line is the PATH you travel,
   while the plumb beside it is the container that holds. Giving the two roles
   different colours is what stops the page reading as one undifferentiated
   scribble. --road is a brighter cousin of --gold, warm enough to read as lit
   rather than antique. */
/* A tight halo, not a spill. At 22px/.20 under a 10px seam this read as brown
   haze around the edge and was a large part of why the whole thing looked muddy. */
.thread [data-thread-bed] { fill: none; stroke: rgba(232,181,92,.11);
  stroke-width: 11; stroke-linecap: round; stroke-linejoin: round; }
/* Molten pieces. These are FILLED, not stroked, so the goo filter can fuse them
   with the road into one mass — a stroked circle would keep a visible outline
   through the merge and break the illusion. */
/* Pools, not beads. Lens-shaped and wider than tall, so the seam visibly
   swells and narrows again instead of having circles threaded onto it. */
.thread-node { fill: url(#road-metal); opacity: 1; }
/* The plumb: this page's container. Quieter than the thread on purpose — it is
   what holds, not what moves, and it must never compete with the line crossing
   it. Resting state is the full-height line movement.js measures. */
.thread-plumb { stroke: rgba(127,196,255,.15); stroke-width: 1; }
.thread-wrap > section { position: relative; z-index: 1; }

@media (max-width: 700px) {
  .thread [data-thread-path] { stroke-width: 1; opacity: .3; }
  .thread-node { rx: 5; ry: 3; }
}

/* ============================================================
   Photo layout families — these exist to break the hero/split/split rhythm.
   Adding more photos inside the same split layout would not fix monotony;
   introducing genuinely different section shapes does.
   ============================================================ */

/* --- 1. Full-bleed band: edge to edge, one line of copy, no container --- */
/* NO BORDER, and taller. This sits immediately above the template's .cta-band on
   every page that uses it, and both used to be full-bleed hairline-bordered dark
   blocks with left-aligned display type - the same object twice, meeting at a
   0px gap that drew TWO 1px lines. The band is the photographic breath and the
   CTA is the contained panel that closes; only one of them should be a framed
   box. Dropping the border here leaves .cta-band's own border-top as the single
   seam, and the extra height lets the photograph read as a moment rather than a
   strip. bands() in fx.js needs nothing: [data-band-img] is inset -6% / 112% of
   this element, so the parallax scales with whatever height it gets. */
.band { position: relative; isolation: isolate; overflow: hidden;
  min-height: clamp(380px, 62vh, 660px); display: flex; align-items: center; }
.band-img { position: absolute; inset: -6% 0; width: 100%; height: 112%;
  object-fit: cover; opacity: .55; z-index: -2; will-change: transform; }
.band::after { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(90deg, rgba(11,13,16,.86), rgba(11,13,16,.35) 62%, rgba(11,13,16,.7)); }
.band-copy { position: relative; }
.band-line { font-family: var(--ff-display); font-weight: 700; line-height: 1.12;
  letter-spacing: -.02em; font-size: var(--fs-display);
  max-width: 22ch; color: var(--text); text-wrap: balance; }
.band-sub { color: var(--dim); margin-top: var(--s3); max-width: 44ch; }

/* --- 2. Triptych: three portraits, one per audience --- */
.tri { list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(3, 1fr); gap: clamp(1rem, 2.2vw, 1.75rem); }
/* No photographs: the numeral is the anchor, and a hairline separates it from the
   heading. --fs-mono-lg is the same step .principles .n uses, so the two numbered
   lists on the site agree. The .tri-media rules that held the portraits (and their
   hover scale) are deleted, not overridden - nothing references them. */
.tri-item { position: relative; }
.tri-n { display: block; font-family: var(--ff-mono); font-size: var(--fs-mono-lg);
  letter-spacing: .2em; color: var(--accent); margin-top: 0; }
.tri-item h3 { font-size: var(--fs-h4); color: var(--text);
  margin-top: var(--s3); padding-top: var(--s3); border-top: 1px solid var(--hair); }
.tri-item p { color: var(--dim); margin-top: var(--s2); font-size: var(--fs-ui); }

/* --- 3. Portrait + pull-quote: asymmetric, not another 50/50 split --- */
.portrait-quote { display: grid; grid-template-columns: 0.85fr 1.15fr;
  gap: clamp(1.5rem, 4vw, 3.5rem); align-items: center; }
.portrait-quote figure { margin: 0; }
.portrait-quote img { width: 100%; border: 1px solid var(--hair-2); border-radius: var(--r-img); }
.pq-line { font-family: var(--ff-body); font-style: italic;
  font-size: var(--fs-h3); line-height: 1.45; color: var(--text);
  border-left: 2px solid var(--gold); padding-left: var(--s4); }
.pq-attr { font-family: var(--ff-mono); font-size: var(--fs-mono); letter-spacing: .16em;
  text-transform: uppercase; color: var(--faint); margin-top: var(--s3); padding-left: calc(var(--s4) + 2px); }

/* --- 4. Press gallery (EPK) --- */
/* Exactly four columns for exactly four shots — auto-fit generated a fifth,
   empty track at desktop widths. */
.press-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s3); }
@media (max-width: 820px) { .press-grid { grid-template-columns: repeat(2, 1fr); } }
.press-grid a { display: block; overflow: hidden; border: 1px solid var(--hair);
  border-radius: var(--r-img); aspect-ratio: 3 / 4; }
.press-grid img { width: 100%; height: 100%; object-fit: cover; opacity: .72;
  transition: transform var(--dur-enter) var(--ease-out-firm), opacity var(--dur-base) var(--ease-out); }
.press-grid a:hover img, .press-grid a:focus-visible img { transform: scale(1.06); opacity: 1; }

@media (max-width: 820px) {
  .tri { grid-template-columns: 1fr; gap: var(--s4); }
  .portrait-quote { grid-template-columns: 1fr; }
  .portrait-quote figure { max-width: 22rem; }
  .band { min-height: 320px; }
}

/* ============================================================
   Pillar 3 — JOURNAL: an actual journal, not a list
   Most entries were published in twos (reflection + the research it stands on).
   A flat list hid that entirely; facing pages make the structure the design.
   Still dark: "paper" here is an elevated charcoal panel with a warm binding,
   never a light-mode section (the page theme stays locked).
   ============================================================ */
/* --- About hero: a moving key light ---
   Contrast does the work. A single soft highlight drifts across the portrait
   while the edges fall away; almost nothing actually moves. Defaults are a
   flattering fixed position, so no-JS and reduced-motion still look composed. */
.hero-lit { --lx: 62%; --ly: 38%; --lit: 0; }
.hero-lit .hero-photo {
  filter: contrast(calc(1 + var(--lit) * .28)) saturate(.9) brightness(.92); }
.hero-lit::before { content: ""; position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
  background: radial-gradient(48% 58% at var(--lx) var(--ly),
    rgba(233,238,245,.20), rgba(233,238,245,.07) 42%, transparent 72%); }
.hero-lit::after { content: ""; position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
  box-shadow: inset 0 0 calc(14vmax + var(--lit) * 8vmax) rgba(0,0,0,.72); }
.hero-lit .hero-inner { position: relative; z-index: 2; }

/* --- Journal hero: the book opens ---
   Leaves rest OPEN. GSAP animates *from* closed, so every failure mode
   (no JS, reduced motion, script error) leaves the headline readable. */
.book-hero { position: relative; isolation: isolate; overflow: hidden;
  min-height: min(72vh, 600px); display: flex; align-items: center; }
/* ---- The spread ----
   The hero at full scale: binding seam, page edges, ruled verso, copy on the
   recto. Deliberately NOT hairlines - the previous version was a 1px rule at 55%
   opacity, which is below the threshold of being noticed at all. */
.book-hero { --seam: rgba(203,170,110,.55); }
/* 1.15fr follows the KEY, whichever page it is on. The key is seven items and
   the copy is three lines, so the wider column must go to the denser content -
   otherwise a third of the key's page is dead. The key is now on the recto, so
   the wide column moved with it. The gap is a token because the seam is
   positioned off it - see .spread-seam. */
.spread { --spread-gap: clamp(2rem, 6vw, 5rem);
  position: relative; display: grid;
  grid-template-columns: 1fr 1.15fr;   /* verso : recto - .spread-seam depends on this */
  gap: var(--spread-gap); align-items: start; }
/* Both pages begin on the same line, the way a real spread does. Centring each
   column independently left the title floating in the middle of an empty recto;
   aligned to the top, the space below it reads as the rest of the page. */
.spread-verso, .spread-recto { padding-block: var(--s3); }

/* Ruled paper under both pages. This is a journal, so the ground is ruled like
   one: warm, very faint, and radially masked so it dissolves at the edges rather
   than ending on a hard line. Sits behind the type, never over it. */
.spread::before { content: ""; position: absolute; inset: -12% -3%; z-index: -1;
  pointer-events: none;
  background: repeating-linear-gradient(180deg,
    transparent 0 37px, rgba(203,170,110,.075) 37px 38px);
  -webkit-mask-image: radial-gradient(115% 85% at 50% 50%, #000 40%, transparent 86%);
  mask-image: radial-gradient(115% 85% at 50% 50%, #000 40%, transparent 86%); }
.spread-recto { position: relative; }

/* the binding: a warm seam with real weight, brightest at the middle */
/* The binding sits in the CENTRE OF THE GRID GAP, derived - not the old hardcoded
   42.5%, which tracked nothing and drew the seam 49px inside the verso column,
   straight through the key's text. fr cannot go in calc(), so the 1.15/2.15 here
   is the one place that must stay in step with grid-template-columns above. */
/* THE BINDING IS THE COIL, and it belongs at the spine. This used to be a solid
   line here while the identical perforated gradient ran down the OUTER margins -
   so the same mark meant "binding" on the nine entry cards (.leaf-edge, left:-1px)
   and "page edge" in the hero. One mark, one meaning: coil = spine, everywhere.
   Wider and longer-period than .leaf-edge because it binds a spread, not a card.
   mask, not a gradient background, since the paint is now a repeating pattern;
   drop-shadow, not box-shadow, so the glow follows the rings and not the box. */
.spread-seam { position: absolute; top: -10%; bottom: -10%; width: var(--coil-w);
  left: calc((100% - var(--spread-gap)) * (1 / 2.15) + var(--spread-gap) / 2);
  margin-left: calc(var(--coil-w) / -2);
  z-index: 1; pointer-events: none; transform-origin: 50% 50%;
  /* --seam over --coil-ink: same coil, brighter ink. The spread binds the whole
     page and should out-rank the card spines it sits above. */
  background: repeating-linear-gradient(180deg,
    var(--seam) 0 var(--coil-rung), transparent var(--coil-rung) var(--coil-period));
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 13%, #000 87%, transparent);
  mask-image: linear-gradient(180deg, transparent, #000 13%, #000 87%, transparent);
  filter: drop-shadow(0 0 5px rgba(203,170,110,.45)); }
/* stacked page edges at the outer margins of the block */
/* The outer margin is a cut page edge, so it is solid. It was the perforated
   pattern, which is the binding's mark - see .spread-seam. A hairline, not the
   old 4px band: the spine should be the loudest thing on the spread. */
.spread-edge { position: absolute; top: -6%; bottom: -6%; width: 1px; z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg, transparent, rgba(203,170,110,.34) 12%,
    rgba(203,170,110,.34) 88%, transparent); }
.spread-edge-l { left: -22px; }
.spread-edge-r { right: -22px; }

/* the verso: ruled lines, like the facing page of a notebook */
.spread-verso { display: grid; align-content: center; min-height: 15rem; }

@media (max-width: 860px) {
  /* One page, not two. The verso used to be empty rules and was hidden here as
     noise; the key is content and must survive, so only the binding furniture
     goes. No `order` needed any more: the key sits second in the source now, so
     stacking in document order already puts it after the title. */
  .spread { grid-template-columns: 1fr; gap: var(--s5); }
  .spread-seam, .spread-edge { display: none; }
  .spread-verso { min-height: 0; }
}
.book-ground { position: absolute; inset: 0; z-index: -2;
  background:
    radial-gradient(120% 90% at 50% 8%, rgba(127,196,255,.13), transparent 60%),
    radial-gradient(90% 80% at 50% 110%, rgba(185,166,255,.08), transparent 65%),
    var(--bg); }
.book-inner { position: relative; z-index: 3; width: 100%; }

@media (max-width: 760px) { .book-hero { min-height: 70vh; } }
/* ---- Journal plates ----------------------------------------------------
   One SVG per TOPIC, included in both halves of a spread. The wrapper class
   picks the rendering, so a pair is the same geometry twice:

     .plate--reflection   icy blue, continuous          -> felt
     .plate--research     gold, tick marks exposed      -> measured
     .plate--mirror       blue, second copy alongside   -> two views, one idea

   Colour rides on `currentColor`, which .motif already uses for its strokes,
   so switching the wrapper's `color` recolours the whole plate. Adding a new
   entry means composing one glyph from the kit and picking a class. */
/* Sized to actually be seen. At 208x72 the art rendered 168x17 — a flat hairline
   that read as nothing at all. Wider box, taller box, heavier stroke, and the
   glyphs themselves redrawn to use their full artboard rather than a flat band
   through the middle of it. */
.plate { display: block; width: min(100%, 300px); height: clamp(72px, 8vw, 104px);
  margin-bottom: var(--s2); opacity: 1; }
.plate [data-draw] { stroke-width: 2.1; }
.plate .plate-svg { width: 100%; height: 100%; overflow: visible; }
.plate--reflection { color: var(--accent); }
.plate--research   { color: var(--gold); }
.plate--mirror     { color: var(--accent); }
/* the two conditional layers, hidden unless their rendering asks for them */
.plate .plate-ticks, .plate .plate-echo { display: none; }
.plate--research .plate-ticks { display: block; }
.plate--research .plate-ticks path { stroke: currentColor; stroke-width: 1; opacity: .7; }
.plate--mirror .plate-echo { display: block; opacity: .55; }
/* research reads as measurement: a finer, cooler stroke than the felt version */
.plate--research [data-draw] { stroke-width: 1.1; }
.leaf-page:has(a:hover) .plate { opacity: 1; }

/* ---- The key ----
   The marks carry meaning, so it is stated once rather than left to be guessed.
   Deliberately quiet: mono labels, a hairline above, no panel. It is a caption
   for the page, not a feature of it. */
/* The key lives on the verso of the hero spread, so it is sized to that column
   (~484px at 1200px wrap), not to a full-width section: two columns for the four
   suits, three for the kinds. Measured at 620px tall inside a 627px hero, which
   is why the glyphs are 44px and the prose lede was dropped. */
.key-head { font-family: var(--ff-mono); font-size: var(--fs-mono); letter-spacing: .18em;
  text-transform: uppercase; color: var(--faint); margin: var(--s5) 0 var(--s3); }
.key-head:first-child { margin-top: 0; }
.key-list { list-style: none; padding: 0; margin: 0; display: grid;
  grid-template-columns: repeat(2, 1fr); gap: clamp(1.1rem, 2.4vw, 1.8rem); }
.key-list-3 { grid-template-columns: repeat(3, 1fr); }
.key-list li { display: grid; gap: var(--s1); }
.key-glyph { width: min(100%, 180px); height: 44px; margin: 0 0 var(--s1); opacity: .95; }
.key-name { font-family: var(--ff-mono); font-size: var(--fs-mono); letter-spacing: .18em;
  text-transform: uppercase; color: var(--text); }
.key-desc { color: var(--faint); font-size: var(--fs-ui); max-width: 30ch; }
@media (max-width: 900px) { .key-list, .key-list-3 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .key-list, .key-list-3 { grid-template-columns: 1fr; } }

.leaves { display: grid; gap: clamp(1.75rem, 4vw, 3rem); }

.leaf { position: relative; display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(1.5rem, 3.5vw, 3rem);
  padding: clamp(1.5rem, 3vw, 2.5rem) clamp(1.5rem, 3vw, 2.75rem);
  background: linear-gradient(100deg, var(--elev), var(--panel));
  border: 1px solid var(--hair); border-radius: var(--r);
  transition: border-color var(--dur-base) var(--ease-out), transform var(--dur-mid) var(--ease-out-firm); }
.leaf:hover { border-color: rgba(203,170,110,.42); transform: translateY(-2px); }
.leaf-single { grid-template-columns: 1fr; max-width: 46rem; }

/* the binding: a warm seam down the gutter, plus stacked page edges at the spine */
.leaf::before { content: ""; position: absolute; top: 12%; bottom: 12%; left: 50%;
  width: 1px; background: linear-gradient(180deg, transparent,
    rgba(203,170,110,.30) 22%, rgba(203,170,110,.30) 78%, transparent); }
.leaf-single::before { display: none; }
/* The card's spine, drawn with the SAME coil as the hero spine - same rung, same
   period, same width, straight off the tokens. It was 2px/5px and 3px wide,
   which at card size read as stitching rather than the binding the hero uses.
   No border-radius: a coil is discrete rings, so there is no bar to round off.
   drop-shadow, not box-shadow, so the glow follows the rings not the box. */
.leaf-edge { position: absolute; left: -2px; top: 8%; bottom: 8%; width: var(--coil-w);
  background: repeating-linear-gradient(180deg,
    var(--coil-ink) 0 var(--coil-rung), transparent var(--coil-rung) var(--coil-period));
  filter: drop-shadow(0 0 4px rgba(203,170,110,.28)); }

.leaf-date { grid-column: 1 / -1; font-family: var(--ff-mono); font-size: var(--fs-mono);
  letter-spacing: .18em; text-transform: uppercase; color: var(--faint); }
/* The title link alone is a ~21px tap target, too small for the page's primary
   navigation. Stretch it over its half of the spread so the whole page is
   tappable, while the <a> keeps its real text for screen readers. */
.leaf-page { min-width: 0; position: relative; }
.leaf-title a::after { content: ""; position: absolute; inset: 0; }
.leaf-page:has(a:hover) .leaf-title a { color: var(--accent); }
.leaf-tag { display: inline-block; font-family: var(--ff-mono); font-size: var(--fs-mono-xs);
  letter-spacing: .18em; text-transform: uppercase; color: var(--violet);
  border: 1px solid rgba(185,166,255,.4); border-radius: var(--r-img);
  padding: .18em .5em; margin-bottom: var(--s2); }
.leaf-title { font-size: var(--fs-h4); line-height: 1.2; }
.leaf-title a { color: var(--text); }
.leaf-title a:hover { color: var(--accent); text-decoration: none; }
.leaf-ex { color: var(--dim); margin-top: var(--s2); font-size: var(--fs-ui); }

@media (max-width: 760px) {
  .leaf { grid-template-columns: 1fr; }
  .leaf::before { top: auto; bottom: auto; left: 0; right: 0; width: auto; height: 1px;
    top: 50%; background: linear-gradient(90deg, transparent, rgba(203,170,110,.3), transparent); }
}

/* ---- Article: made to be read ---- */
/* Opt-in, not :first-of-type — four of the nine articles open on a curly quote,
   and a 60px floating quotation mark is not a drop cap. journal.js adds the
   class only when the paragraph actually starts with a letter. */
.article-body p.dropcap::first-letter {
  float: left; font-family: var(--ff-display); font-weight: 700; font-size: 3.4em;
  line-height: .82; padding: .06em .12em 0 0; color: var(--accent); }
.article-body { max-width: 68ch; }

/* bookmark ribbon: reading progress, hung from the top edge */
.ribbon { position: fixed; top: 0; left: clamp(10px, 3vw, 34px); width: 10px; z-index: 60;
  height: 0; pointer-events: none;
  background: linear-gradient(180deg, var(--gold), rgba(203,170,110,.35));
  border-radius: 0 0 2px 2px; }
.ribbon::after { content: ""; position: absolute; left: 0; right: 0; bottom: -6px;
  border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-top: 6px solid rgba(203,170,110,.55); }
@media (max-width: 900px) { .ribbon { display: none; } }

/* line-by-line reveal; default visible so a failed trigger can never hide prose */
.article-body .line { display: block; }
:root.gsap .article-body .line-mask { overflow: hidden; }

/* ---- MAI chat panel (dormant until body[data-mai="1"]) ---- */
.mai-panel { position: fixed; inset: 0; z-index: 90; }
.mai-scrim { position: absolute; inset: 0; background: rgba(6, 8, 11, .6);
  backdrop-filter: blur(3px); opacity: 0; transition: opacity var(--dur-fast) var(--ease-out); }
.mai-panel.mai-in .mai-scrim { opacity: 1; }
.mai-card { position: absolute; top: 0; right: 0; height: 100%; width: min(440px, 100%);
  display: flex; flex-direction: column; background: var(--panel);
  border-left: 1px solid var(--hair-2); box-shadow: -20px 0 60px rgba(0,0,0,.5);
  transform: translateX(100%); transition: transform var(--dur-exit) var(--ease-out-firm); }
.mai-panel.mai-in .mai-card { transform: none; }
.mai-head { display: flex; align-items: center; justify-content: space-between;
  padding: var(--s3) var(--s3); border-bottom: 1px solid var(--hair); }
.mai-title { font-family: var(--ff-display); font-weight: 700; font-size: var(--fs-h4); letter-spacing: .02em;
  background: linear-gradient(100deg,#8b93a1,#e9eef5 30%,#b9a6ff 60%,#e9eef5);
  background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; }
.mai-x { background: none; border: 0; color: var(--dim); font-size: var(--fs-h3); line-height: 1; cursor: pointer;
  width: 40px; height: 40px; border-radius: var(--r); }
.mai-x:hover { color: var(--text); background: var(--elev); }
.mai-log { flex: 1; overflow-y: auto; padding: var(--s3); display: flex; flex-direction: column; gap: var(--s2); }
.mai-msg { max-width: 88%; padding: .7em 1em; border-radius: var(--r-bubble); font-size: var(--fs-ui); line-height: 1.5;
  white-space: pre-wrap; }
.mai-mai { align-self: flex-start; background: var(--elev); color: var(--text);
  border: 1px solid var(--hair); border-left: 2px solid var(--violet); }
.mai-user { align-self: flex-end; background: rgba(127,196,255,.14); color: var(--text);
  border: 1px solid rgba(127,196,255,.3); }
.mai-typing { color: var(--faint); letter-spacing: .1em; }
.mai-form { display: flex; gap: var(--s2); padding: var(--s2) var(--s3); border-top: 1px solid var(--hair);
  align-items: flex-end; }
.mai-form textarea { flex: 1; resize: none; max-height: 120px; background: var(--bg); color: var(--text);
  border: 1px solid var(--hair-2); border-radius: var(--r); padding: .7em .9em; font-family: var(--ff-body);
  font-size: var(--fs-input); line-height: 1.4; }
.mai-form textarea:focus-visible { border-color: var(--accent); }
.mai-send { padding: .7em 1.1em; }
.mai-send:disabled { opacity: .5; cursor: default; }
.mai-foot { padding: 0 var(--s3) var(--s2); color: var(--faint); font-family: var(--ff-mono);
  font-size: var(--fs-mono-xs); letter-spacing: .03em; }

/* ---- Responsive ---- */
@media (max-width: 900px) {
  .hero-grid, .hero-full .hero-grid { grid-template-columns: 1fr; }
  .hero-figure { order: -1; max-width: 26rem; }
  .hero-full { min-height: auto; }
  /* Long pins are punishing on phones: shorten the sequence and cap the zoom.
     The overlap shrinks with it, since it must stay well inside the shorter pin. */
  :root { --trellis-scroll: 80vh; --gates-overlap: 50vh; }
  .trellis-grid { grid-template-columns: 1fr; }
  .trellis-figure { order: -1; max-width: 20rem; }
  .trellis-copy .lede { max-width: none; }
  .gates-row { flex-direction: column; border-top: 0; }
  .gate { flex: 1 1 auto; min-height: 46vh; border-right: 0; border-top: 1px solid var(--hair); }
  /* no hover expansion on touch — every panel reads fully, taps go straight through */
  .gates-row:hover .gate, .gates-row:focus-within .gate { flex-grow: 1; opacity: 1; filter: none; }
  .gate-desc { opacity: 1; }
  .split, .split.reverse > :first-child { grid-template-columns: 1fr; order: 0; }
  .split-media { max-width: 34rem; }
  .foot-inner { grid-template-columns: 1fr 1fr; }
  /* Stacked, the copy now spans the full width and would run into the light end
     of the band's directional scrim. Flatten it back to even protection here. */
  .cta-inner { grid-template-columns: 1fr; }
  .cta-band { background:
    radial-gradient(700px 300px at 85% 0%, rgba(127,196,255,.09), transparent 70%),
    linear-gradient(rgba(11,13,16,.88), rgba(11,13,16,.9)),
    url("/assets/img/atmo-forest.jpg") center 10% / cover no-repeat,
    var(--panel); }
  .door-sound, .door-movement, .door-journal { grid-column: 1 / -1; }
}
@media (max-width: 720px) {
  .nav-toggle { display: inline-flex; align-items: center; justify-content: center;
    order: 3; margin-left: auto; width: 44px; height: 44px; background: transparent;
    border: 1px solid var(--hair-2); border-radius: var(--r); cursor: pointer; }
  .nav-toggle-bars, .nav-toggle-bars::before, .nav-toggle-bars::after {
    content: ""; display: block; width: 18px; height: 2px; background: var(--text); position: relative; }
  .nav-toggle-bars::before { position: absolute; top: -6px; }
  .nav-toggle-bars::after  { position: absolute; top: 6px; }
  .head-inner { flex-wrap: wrap; }
  .head-actions { order: 2; margin-left: auto; }
  .nav { flex-basis: 100%; margin-left: 0; display: none; }
  .nav.open { display: block; }
  .nav ul { flex-direction: column; gap: 0; padding-block: var(--s2); }
  .nav li { border-top: 1px solid var(--hair); }
  .nav a { display: block; padding: .8rem 0; }
  .principles li { grid-template-columns: 1fr; }
  .release { grid-template-columns: 1fr; }
  .resource { grid-template-columns: 1fr; }
  .resource-cover { max-width: 12rem; }
  .foot-inner { grid-template-columns: 1fr; }
}

/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  :root.js .reveal { opacity: 1; transform: none; transition: none; }
  .btn, .door { transition: none; }
  .door:hover { transform: none; }
  .chrome, .brand, .foot-name { animation: none; }   /* no silver sheen, material stays */
  .hero-full h1 .word, .trellis-copy h1 .word:last-of-type {
    animation: none; opacity: 1; transform: none; }
  .motif [data-draw] { stroke-dashoffset: 0; }    /* motifs render complete */
  .shelf-ledge, .shelf-cover { animation: none; transform: none; }
  .motif .node { opacity: .9; transition: none; }
  .mai-scrim, .mai-card { transition: none; }
  .mai-title { animation: none; }
  /* Trellis collapses to a plain static hero: no pin, no shutter reveal.
     The frame stays drawn, the photograph stays uncovered, all copy reachable. */
  .trellis { height: auto; }
  .trellis-stage { position: static; height: auto; min-height: 100dvh;
    padding-block: clamp(3rem, 8vw, 6rem); }
  .tr-pane { opacity: 0 !important; }
  .tr-v, .tr-h { transform: none !important; }
  .trellis-copy { opacity: 1 !important; transform: none !important; }
  .trellis-figure { opacity: 1 !important; transform: none !important; }
  .gates { opacity: 1 !important; transform: none !important; transition: none;
    margin-top: 0 !important; background: var(--bg); }
  .gates::before { display: none; }
  .gate, .gate-media, .gates-divider { transition: none; }
}
