/* ===== COLORS — monochrome, light theme =====
   Inverted from the original dark theme. These are perceptual mirrors, not
   arithmetic ones: the dark theme never used pure white on pure black (it sat
   at #e8e8e8 on #000), so the light theme keeps the same restraint in reverse —
   near-black ink on white rather than a harsh 21:1 slam.

   --muted is the one value that cannot simply be flipped. #9a9a9a carried ~7:1
   against black; that same grey on white collapses to ~2.8:1 and stops being
   readable, so light-theme muted is pulled well down to hold its contrast. */
:root {
  --bg:      #ffffff;   /* white page background */
  --text:    #171717;   /* near-black body text */
  --heading: #0a0a0a;   /* near-black display headings */
  --muted:   #6a6a6a;   /* mid grey text / labels (~5.3:1 on white) */
  --line:    rgba(0, 0, 0, 0.14);  /* hairline borders */

  /* Times New Roman throughout — serif */
  --font-display: "Times New Roman", Times, serif;
  --font-body:    "Times New Roman", Times, serif;
  --font-mono:    "Times New Roman", Times, serif;

  /* Site-nav face, kept deliberately separate from --font-display. Interior
     pages re-declare --font-display on :root (fintech3.css and sectors.css swap
     in the grotesk; index.html does the same inline), so a nav built on that
     variable would change typeface from page to page — serif on Essays, sans
     everywhere else. The nav is persistent chrome and must look identical on
     every page, so it gets its own stack that no page stylesheet overrides. */
  --font-nav: "aktiv-grotesk", "Helvetica Neue", Arial, sans-serif;

  /* headline treatment: heavy weight + tight tracking */
  --display-weight:  600;
  --display-tracking: -0.03em;

  /* Shared page-title ("H1") treatment so the hero title renders at the SAME
     size and weight on every page — Home ("Kyle Connor"), Essays, and Fintech 3
     ("Mapping the global financial system"). Set once here; all three move
     together. Sized to fill the home column at typical desktop widths (~92px),
     which the longer Fintech 3 headline meets by wrapping instead of shrinking. */
  --title-size:     clamp(2.75rem, 7vw, 6rem);
  --title-weight:   400;
  --title-tracking: -0.03em;

  /* ===== FLUID TYPE SCALE =====
     Every size below interpolates smoothly between a small-screen and a
     large-screen value across roughly the 360px-1600px viewport band, then locks
     at both ends so it can never run away on a phone or a 4K panel.

     The reason this exists: the home page used to set the name at a flat 32px
     and the tagline at a flat 16px, identical at 320px and at 2560px, while the
     photo beside them grew 2.5x and the gutter grew 10x. Frozen text next to
     fluid everything-else is what made the page read as a different design at
     each end. These move WITH the layout instead.

     The form is clamp(min, REM_BASE + VW_RATE, max) rather than plain vw: a
     rem term must be present or the text stops responding to the user's own
     browser font-size setting, which is an accessibility failure (WCAG 1.4.4). */
  --fs-name: clamp(1.6rem,  1.34rem + 1.06vw, 2.35rem);   /* 25.6 -> 37.6px */
  --fs-body: clamp(0.95rem, 0.90rem + 0.24vw, 1.10rem);   /* 15.2 -> 17.6px */
  --fs-nav:  clamp(0.95rem, 0.92rem + 0.16vw, 1.12rem);   /* 15.2 -> 17.9px */

  /* Composition cap. Past this the whole landing stops growing and simply
     centres, so an ultrawide monitor gets margins rather than a stretched
     layout with 256px of gutter. */
  --landing-max: 1760px;

  /* ===== SHARED HORIZONTAL GRID =====
     One set of edges for the whole composition, so the nav cannot drift out of
     step with the content under it. Before this the bar carried its own
     independent padding and sat 33px left of the name and 12px inside the
     photo's right edge.

     --page-pad-l is the LEFT rail: the nav wordmark and the landing copy both
     start here.

     --photo-inset is the RIGHT rail: how far the artwork sits in from the edge
     of the composition. The nav's right padding uses it too, which is what puts
     "Finance" flush with the right edge of the photo. js/dot-photo.js READS this
     value rather than hard-coding its own — the declaration here is the single
     source of truth, so changing it moves the art and the nav together.
     (2.25vw is not arbitrary: the photo half is 50% of the composition, and the
     renderer insets by 4.5% of that half, which is the same number.) */
  --page-pad-l:   clamp(1.5rem, 4.5vw, 5.5rem);
  --photo-inset:  clamp(1rem, 2.25vw, 1.75rem);

  /* How far the fixed nav sits from the top of the viewport. A variable rather
     than a literal because the hide-on-scroll transform has to move the bar by
     its own height PLUS this offset to clear the screen completely. The two have
     to agree, and they cannot be kept in agreement if the value is written in
     two places. */
  --nav-top: clamp(1.25rem, 2.5vw, 2rem);

  /* Swap-button ("See the work" style, ported from good-fella.com) — the CTA
     shared by the home page and Fintech 3. Orange fill, near-black ink, a mono
     label, and the reference power4 in-out ease. */
  --gf-brand: #fd551d;                      /* reference --color-brand */
  --gf-ink:   #141314;                      /* reference black on the button */
  --gf-ease:  cubic-bezier(.83, 0, .17, 1); /* reference --ease-power4-in-out */
  --gf-mono:  ui-monospace, "SF Mono", "Fira Code", "Roboto Mono", monospace;
}

/* ===== DARK STAGE (opt-in, per page) =====
   The site is light, but fintech2.html cannot be: its particle sphere is drawn
   with ADDITIVE blending, which only resolves against black — on white the
   particles sum past #fff and disappear. Rather than leave that page half
   converted (black canvas, near-black text), it opts back into the original dark
   palette so it is internally consistent: a deliberately dark stage, not a
   broken light page. Remove this class if the sphere is ever re-authored to
   render subtractively. */
.dark-stage {
  --bg:      #000000;
  --text:    #e8e8e8;
  --heading: #f5f5f5;
  --muted:   #9a9a9a;
  --line:    rgba(255, 255, 255, 0.14);
  /* The wipe has to invert with the page. Site-wide it is near-black so it reads
     against white; on this one dark page that would be a black square sweeping
     across black — the animation would run and be completely invisible. */
  --wipe-color: #f5f5f5;
  background: var(--bg);
  color: var(--text);
}

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

html {
  scroll-behavior: smooth;
  /* Reserve the scrollbar's width on EVERY page, scrolling or not. Writings is a
     long list and gets a scrollbar; Home fits and does not. Without this the
     layout viewport differs by ~15px between them, so the fixed nav and the
     content rails land in different places depending on which page you are on —
     the exact drift this grid exists to prevent. */
  scrollbar-gutter: stable;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: 300;
  line-height: 1.6;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }

/* ===== BACK-TO-HOME LINK (top-left on interior pages) =====
   The full nav bar is gone; interior pages (Essays, Fintech 3) get a single
   fixed link in the top-left corner that wipes back to the home page. */
.home-back {
  position: fixed;
  top: clamp(1.25rem, 2.5vw, 2rem);
  left: clamp(1.5rem, 3vw, 2.5rem);
  z-index: 50;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.78rem;
  color: var(--heading);
}
.home-back-mark {
  display: inline-block;
  transition: transform 0.5s cubic-bezier(0.625, 0.05, 0, 1);
}
.home-back:hover .home-back-mark { transform: translateX(-5px); }

/* ===== SITE NAV (persistent, full-width bar) =====
   Pinned to the top of every main page: the name at the left margin, the links —
   Home · Writings · Finance — pushed to the opposite end. Supersedes the single
   .home-back link interior pages used to carry (that rule is kept above for the
   older fintech / fintech2 / players pages). The current page is flagged with
   aria-current="page" — heading colour + a lit underline — while the rest sit
   muted until hovered. Set in sentence case at a readable size so it reads as
   real navigation, not a small-caps label.

   Stretching left:0 → right:0 is what gives space-between something to push
   against; a shrink-to-fit bar would have nothing to distribute.

   "Finance" additionally opens a dropdown to the pages inside that section; see
   the SITE NAV DROPDOWN block below. */
.site-nav {
  position: fixed;
  top: var(--nav-top);
  /* left/right 0 give space-between something to push against; max-width +
     margin-inline:auto then cap and centre the bar on exactly the same box as
     .landing, so the two share a left and right edge at every viewport. */
  left: 0;
  right: 0;
  max-width: var(--landing-max);
  margin-inline: auto;
  z-index: 50;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: clamp(0.95rem, 2.1vw, 1.9rem);
  /* The two rails: wordmark lines up with the landing copy, link group lines up
     with the right edge of the artwork. */
  padding-left: var(--page-pad-l);
  padding-right: var(--photo-inset);
  transition: transform 0.4s var(--gf-ease);
}

/* ----- Hide while scrolling DOWN, restore on the way back UP -----
   The bar is fixed and has no background, so page content used to slide directly
   underneath it: on Writings the list rows crossed straight through the nav
   text. Both were legible on their own and neither was on top, which is the
   worst version of the problem.

   Of the three ways out, this is the one that gives up least:
     - a solid background would stop the text collision but the bar would still
       sit on top of the page, hiding a strip of content the whole way down;
     - un-fixing it gets the bar out of the way permanently, but persistent
       navigation was a deliberate decision (it replaced the old per-page
       back-link), so that trades the feature away to fix a paint bug;
     - moving out of the way while you read down, and coming straight back the
       instant you scroll up, covers nothing while still being one gesture away.

   The transform has to clear the bar's own top offset as well as its height,
   hence the calc — translateY(-100%) alone would leave --nav-top worth of bar
   still showing. js/site-nav.js owns when the class goes on. */
.site-nav--hidden {
  transform: translateY(calc(-100% - var(--nav-top)));
}
/* The nav must never be hidden while something inside it has keyboard focus —
   tabbing to an invisible link is a trap. :focus-within re-shows it and beats
   the rule above on specificity. */
.site-nav:focus-within {
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  /* Still hides — that is the fix, not decoration — but arrives instantly. */
  .site-nav { transition: none; }
}
/* The name, doubling as the home link. Set to match .site-nav__link EXACTLY —
   same face, size, tracking, weight, colour and hover — so the bar reads as one
   run of type rather than a wordmark plus three links.

   This used to separate the two by weight (600 against the links' 300) and
   colour (--heading against --muted), deliberately, so the name read as a
   wordmark. That was reversed on request: at --fs-nav the weight jump was large
   enough to look like a different typeface rather than a heavier cut of the same
   one, which is the opposite of what a wordmark should do.

   Anything typographic added to .site-nav__link from here has to be added here
   too, or the two drift apart again. The one thing deliberately NOT shared is
   the ::after underline: that is the "you are here" indicator, and this link
   points at index.html just like the Home link does, so giving it one would put
   two lit underlines on the home page. */
.site-nav__brand {
  font-family: var(--font-nav);
  font-size: var(--fs-nav);
  font-weight: 300;
  letter-spacing: 0.03em;
  line-height: 1;
  color: var(--muted);
  white-space: nowrap;
  transition: color 0.3s var(--gf-ease);
}
.site-nav__brand:hover { color: var(--heading); }
/* Holds the links together as one unit so space-between sees exactly two
   children — brand and link-group — and pushes them to opposite ends. */
.site-nav__links {
  display: inline-flex;
  align-items: baseline;
  gap: clamp(0.95rem, 2.1vw, 1.9rem);
}
/* Narrow screens: stack the name above the links, both flush left, rather than
   squeezing a wordmark and three links onto one short line. The reference site
   does the same thing below its own breakpoint. */
@media (max-width: 34rem) {
  .site-nav {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.6rem;
  }
}
.site-nav__link {
  position: relative;
  font-family: var(--font-nav);
  /* Sentence case, straight from the markup ("Home", "Writings", "Finance") —
     no text-transform. The wide 0.16em tracking that made the old small
     uppercase label legible reads as gappy at this size in mixed case, so it
     drops to a hairline 0.03em. */
  letter-spacing: 0.03em;
  /* was a flat 1.05rem; --fs-nav passes through almost exactly that at
     ~1280px, so the desktop look is unchanged while phones and very wide
     screens now get a size that suits them. */
  font-size: var(--fs-nav);
  line-height: 1;
  color: var(--muted);
  white-space: nowrap;
  transition: color 0.3s var(--gf-ease);
}
/* hairline underline: grows from the left on hover, stays lit for the current
   page so "you are here" reads at a glance. Sentence case puts a descender in
   "Writings", so the rule sits a touch lower than it did under all-caps to keep
   the g's tail from touching it. */
.site-nav__link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -0.45rem;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.4s var(--gf-ease);
}
.site-nav__link:hover { color: var(--heading); }
.site-nav__link:hover::after { transform: scaleX(1); }
/* Lit for the current page, and also for "Finance" while you are on any page
   inside the Finance section. Those are two different truths, so they get two
   different attributes: aria-current="page" is reserved for the link that really
   does point at the page you are on (assistive tech announces it that way), and
   data-section-current is a styling-only flag that says "you are somewhere in
   here". Using aria-current for the second case would tell a screen reader that
   Companies is Finance, which it isn't. */
.site-nav__link[aria-current="page"],
.site-nav__link[data-section-current] { color: var(--heading); }
.site-nav__link[aria-current="page"]::after,
.site-nav__link[data-section-current]::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .site-nav__link::after { transition: none; }
}

/* ===== SITE NAV DROPDOWN (the Finance menu) =====
   "Finance" is a link and a caret button sitting in one nav slot: the label goes
   straight to finance.html, the caret opens the pages within the section. On a
   pointer device the menu also opens on hover; on touch, where hover does not
   exist, the caret is the way in. js/site-nav.js drives the click/keyboard half.

   This replaces the old .section-nav second tier — the three links moved into
   the menu rather than sitting in a permanent row beneath the bar. */
.site-nav__group {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: 0.34rem;
}
/* strip the UA button chrome so the caret sits like type next to the label */
.site-nav__toggle {
  appearance: none;
  background: none;
  border: 0;
  padding: 0.15rem;
  margin: 0;
  font: inherit;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  transition: color 0.3s var(--gf-ease);
}
.site-nav__toggle:hover { color: var(--heading); }
.site-nav__group:hover .site-nav__toggle { color: var(--heading); }
.site-nav__caret {
  width: 0.6rem;
  height: 0.6rem;
  transition: transform 0.3s var(--gf-ease);
}
/* caret flips to point up while the menu is showing */
.site-nav__group.is-open .site-nav__caret { transform: rotate(180deg); }

.site-nav__menu {
  position: absolute;
  top: calc(100% + 0.85rem);
  /* Anchored to the group's RIGHT edge, so the menu opens inward. "Finance" is
     now the last item of a right-aligned bar and sits hard against the page
     margin — a left-anchored menu would hang off the side of the viewport. */
  right: 0;
  left: auto;
  min-width: 9.5rem;
  margin: 0;
  padding: 0.45rem 0;
  list-style: none;
  background: var(--bg);
  border: 1px solid var(--line);
  /* hidden by default. visibility (not just opacity) keeps the closed menu out
     of the tab order and unclickable; it is transitioned with a delay so the
     fade-out can finish before it is pulled from the a11y tree. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(-0.4rem);
  transition: opacity 0.28s var(--gf-ease),
              transform 0.28s var(--gf-ease),
              visibility 0s linear 0.28s;
}
/* Bridges the gap between the bar and the menu so travelling the mouse down to
   the menu doesn't cross un-hovered dead space and close it mid-journey. */
.site-nav__menu::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -0.85rem;
  height: 0.85rem;
}
.site-nav__group.is-open .site-nav__menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s, 0s, 0s;
}
/* Hover-to-open, but only where hovering is real. Touch screens report
   (hover: none) and would otherwise get a menu that opens on tap-and-sticks. */
@media (hover: hover) and (pointer: fine) {
  .site-nav__group:hover .site-nav__menu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    transition-delay: 0s, 0s, 0s;
  }
}
/* Narrow screens stack the bar (see the .site-nav rule above), which moves the
   link row back to the LEFT margin. Right-anchoring there would push the menu
   off the left edge instead, so below the breakpoint it goes back to opening
   rightward from the group. */
@media (max-width: 34rem) {
  .site-nav__menu { left: 0; right: auto; }
}
.site-nav__menu-link {
  display: block;
  padding: 0.5rem 1rem;
  font-family: var(--font-nav);
  /* Derived from the nav label rather than set as its own fixed number, so the
     menu tracks --fs-nav up and down the fluid scale instead of drifting out of
     proportion with the bar at the extremes. 0.9 keeps it clearly subordinate to
     the labels above it while staying comfortably readable. */
  font-size: calc(var(--fs-nav) * 0.9);
  letter-spacing: 0.03em;
  line-height: 1.2;
  color: var(--muted);
  white-space: nowrap;
  transition: color 0.25s var(--gf-ease), background-color 0.25s var(--gf-ease);
}
.site-nav__menu-link:hover { color: var(--heading); background: var(--line); }
/* Current page: the same near-black the primary nav uses for aria-current, not
   the brand orange it used to carry. Orange is the accent for CTAs and kickers;
   spending it on a nav state made the menu read as though the current page were
   a promoted item rather than simply where you are. */
.site-nav__menu-link[aria-current="page"] { color: var(--heading); }
/* Not-yet-built pages still read as real destinations, just dimmer — the menu
   tells the truth about what exists without hiding the roadmap. */
.site-nav__menu-link[data-stub] { opacity: 0.5; }
.site-nav__menu-link[data-stub]:hover { opacity: 0.85; }

@media (prefers-reduced-motion: reduce) {
  .site-nav__menu,
  .site-nav__caret { transition: none; }
}

/* Visually hidden, still read aloud — used for the caret button's label. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ===== SECTION NAV (second tier) — RETIRED =====
   Superseded by the Finance dropdown above. This was a permanent row of
   Map · Companies · Dashboard pinned under the bar on the three Finance pages;
   those links now live in the dropdown, which reaches them from ANY page rather
   than only from inside the section. It also had to go when the bar moved to the
   top centre — being fixed to the left margin, it would have been left stranded
   in the corner under nothing.

   Kept commented rather than deleted: restoring the tier means un-commenting
   this block, re-adding the <nav class="section-nav"> markup to finance.html /
   companies.html / dashboard.html, and re-centring the `left` below to match the
   bar. The matching .page-intro / .page-stub rules further down are still live —
   the Dashboard page uses them and is unaffected.

.section-nav {
  position: fixed;
  top: calc(clamp(1.25rem, 2.5vw, 2rem) + 2.15rem);
  left: clamp(1.5rem, 3vw, 2.5rem);
  z-index: 50;
  display: inline-flex;
  align-items: baseline;
  gap: clamp(0.75rem, 1.5vw, 1.3rem);
  max-width: calc(100vw - 2 * clamp(1.5rem, 3vw, 2.5rem));
}
.section-nav__link {
  font-family: var(--font-nav);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.66rem;
  line-height: 1;
  color: var(--muted);
  white-space: nowrap;
  opacity: 0.75;
  transition: color 0.3s var(--gf-ease), opacity 0.3s var(--gf-ease);
}
.section-nav__link:hover { color: var(--heading); opacity: 1; }
.section-nav__link[aria-current="page"] { color: var(--gf-brand); opacity: 1; }
.section-nav__link[data-stub] { opacity: 0.45; }
.section-nav__link[data-stub]:hover { opacity: 0.85; }

@media (max-width: 34rem) {
  .section-nav {
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .section-nav::-webkit-scrollbar { display: none; }
}
   ===== end retired section-nav ===== */

/* ===== PAGE INTRO + STUB =====
   Shared header treatment for interior pages, so a new page can be stood up
   without its own stylesheet (the companies page keeps its older .sectors-intro
   rules in sectors.css). .page-stub is the "planned, not built yet" state — used
   by the Dashboard now, and ready for whatever the fourth Finance page becomes. */
.page-intro {
  padding-top: clamp(8rem, 18vh, 12rem);
  padding-bottom: clamp(2.5rem, 6vh, 4rem);
  /* SITE GRID — same three values as .landing, .wr and .site-nav, so the fixed
     nav lines up with this page too. See the SHARED HORIZONTAL GRID note in
     style.css :root before changing any of them. */
  /* width:100% before the auto margins. In a COLUMN flex container the cross
     axis is horizontal, so auto inline margins override align-self:stretch and
     collapse the item to its content width — which silently killed the
     margin-left:auto that pushes .sector-count to the right rail. Stating the
     width keeps the box full-bleed and lets the auto margins do only what they
     are here for: centring once max-width binds. */
  width: 100%;
  max-width: var(--landing-max);
  margin-inline: auto;
  padding-left: var(--page-pad-l);
  padding-right: var(--photo-inset);
}
.page-intro-kicker {
  font-family: var(--gf-mono);
  text-transform: uppercase; letter-spacing: 0.24em;
  font-size: 0.7rem; color: var(--gf-brand);
  margin-bottom: 1.1rem;
}
.page-intro-title {
  font-family: var(--font-display);
  font-size: var(--title-size);
  font-weight: var(--title-weight);
  letter-spacing: var(--title-tracking);
  line-height: 1.04;
  max-width: 18ch;
  color: var(--heading);
}
.page-intro-sub {
  margin-top: 1.4rem;
  max-width: 46ch;
  font-size: clamp(1rem, 1.4vw, 1.125rem);
  line-height: 1.5;
  color: var(--muted);
}

/* the "what's going here" list on a stub page */
.page-stub {
  padding-top: 0;
  padding-bottom: clamp(6rem, 14vh, 9rem);
  /* SITE GRID — same three values as .landing, .wr and .site-nav, so the fixed
     nav lines up with this page too. See the SHARED HORIZONTAL GRID note in
     style.css :root before changing any of them. */
  /* width:100% before the auto margins. In a COLUMN flex container the cross
     axis is horizontal, so auto inline margins override align-self:stretch and
     collapse the item to its content width — which silently killed the
     margin-left:auto that pushes .sector-count to the right rail. Stating the
     width keeps the box full-bleed and lets the auto margins do only what they
     are here for: centring once max-width binds. */
  width: 100%;
  max-width: var(--landing-max);
  margin-inline: auto;
  padding-left: var(--page-pad-l);
  padding-right: var(--photo-inset);
}
.page-stub-label {
  display: block;
  font-family: var(--gf-mono);
  text-transform: uppercase; letter-spacing: 0.2em;
  font-size: 0.62rem; color: var(--muted);
  padding-bottom: 1.3rem;
}
.page-stub-list { list-style: none; max-width: 54ch; border-top: 1px solid var(--line); }
.page-stub-list li {
  display: flex; align-items: baseline; gap: 1.25rem;
  padding: 1.15rem 0;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: clamp(0.95rem, 1.3vw, 1.05rem);
  line-height: 1.45;
}
.page-stub-num {
  font-family: var(--gf-mono);
  font-size: 0.72rem;
  color: var(--gf-brand);
  flex-shrink: 0;
}

/* ===== END-OF-PAGE "NEXT" =====
   The third navigation mechanism. The two nav tiers serve people who arrive
   knowing where they want to go; this one serves the reader who just scrolled to
   the bottom and is deciding whether to stay. Centred, quiet label above the
   shared orange button so it reads as an invitation, not a dead end. */
.page-next {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.1rem;
  padding: clamp(4rem, 12vh, 8rem) 1.5rem clamp(5rem, 14vh, 9rem);
  border-top: 1px solid var(--line);
  margin-top: clamp(3rem, 8vh, 6rem);
}
.page-next-label {
  font-family: var(--gf-mono);
  text-transform: uppercase; letter-spacing: 0.22em;
  font-size: 0.62rem; color: var(--muted);
}

/* ===== PAGE HERO — a painted cover above the page's own content =====
   Not a separate page. This is a full-bleed section sitting ABOVE the real
   content, so moving between the two is ordinary scrolling: down into the page,
   back up to the painting, no navigation and no history entry.

   Each page points at its own artwork through --hero-art, so adding a cover to
   another page is a section plus one custom property — no new CSS.

   --hero-art MUST use a root-relative path (/assets/...). A url() inside a
   custom property is resolved against the stylesheet that CONSUMES it, not the
   document that declares it — so a document-relative path here resolves against
   css/ and 404s as /css/assets/... Leading slash sidesteps it entirely.

   The nav is hidden while this is on screen; see the hero-lock observer in
   js/site-nav.js and .site-nav--hero-lock below. */
.page-hero {
  position: relative;
  width: 100%;
  height: 100vh;
  height: 100dvh;                     /* the real visible height on mobile */
  overflow: hidden;
  background-color: #10203f;          /* deep blue: no white flash before decode */
  background-image: var(--hero-art);
  background-size: cover;
  background-position: var(--hero-pos, center);
  background-repeat: no-repeat;
}
/* keeps the cover title off whatever pale ground it lands on */
.page-hero::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 46%;
  background: linear-gradient(to top, rgba(8, 14, 26, 0.42), rgba(8, 14, 26, 0));
  pointer-events: none;
}
.page-hero-title {
  position: absolute;
  /* On the site grid, like every other leading edge. The hero is full-bleed, so
     this reproduces what max-width + margin-inline:auto does for block elements:
     centre the capped box, then step in by the left rail. */
  left: calc(max(0px, 100% - var(--landing-max)) / 2 + var(--page-pad-l));
  bottom: clamp(1.5rem, 3.6vw, 3.75rem);
  z-index: 2;
  margin: 0;
  font-family: "PT Serif", Georgia, "Times New Roman", serif;
  font-weight: 400;
  font-size: clamp(2.9rem, 10.5vw, 9.5rem);
  line-height: 0.9;
  letter-spacing: -0.005em;
  color: #fff;
  text-shadow: 0 2px 26px rgba(0, 0, 0, 0.4);
}
.page-hero-title span { display: block; }

/* The only affordance on the cover. Without it the screen has no nav, no visible
   link and nothing that looks scrollable — it reads as a dead end. */
.page-hero-cue {
  position: absolute;
  left: 50%;
  bottom: clamp(1.6rem, 3.4vw, 3rem);
  transform: translateX(-50%);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  font-family: "PT Serif", Georgia, serif;
  font-style: italic;
  font-size: clamp(0.85rem, 1vw, 1rem);
  color: rgba(255, 255, 255, 0.82);
  text-shadow: 0 1px 14px rgba(0, 0, 0, 0.6);
  transition: opacity 0.4s ease;
}
.page-hero-cue-line {
  width: 1px;
  height: clamp(1.6rem, 3vw, 2.4rem);
  background: linear-gradient(to bottom, rgba(255,255,255,0.85), rgba(255,255,255,0));
  animation: page-hero-drift 2.2s ease-in-out infinite;
}
@keyframes page-hero-drift {
  0%, 100% { transform: translateY(0);   opacity: 0.5; }
  50%      { transform: translateY(5px); opacity: 1;   }
}
/* the cue has done its job once you are past the cover */
.page-hero--passed .page-hero-cue { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .page-hero-cue-line { animation: none; }
}
@media (max-width: 34rem) {
  .page-hero-title { font-size: clamp(2.6rem, 14vw, 4.25rem); }
  /* centred, the cue would land on the name */
  .page-hero-cue { left: auto; right: 1.5rem; transform: none; }
}

/* Nav hidden while the cover is on screen. Kept as its own class rather than
   reusing .site-nav--hidden because the two are answering different questions —
   the auto-hide script owns that one and would fight this for control, and it
   shows the bar whenever you are near the top of the document, which is exactly
   where the cover is. .site-nav:focus-within still beats this, so a keyboard
   visitor tabbing into the bar can always see it. */
.site-nav--hero-lock {
  transform: translateY(calc(-100% - var(--nav-top)));
}

/* ===== LANDING (home page) ===== */
.landing {
  position: relative;
  /* dvh tracks the *actual* visible height on mobile, where the browser chrome
     slides away as you scroll; plain vh is the tallest state, which leaves the
     centred content sitting low behind the URL bar on first paint. vh first as
     the fallback for anything that does not know dvh. */
  min-height: 100vh;
  min-height: 100dvh;
  /* Capped and centred: past --landing-max the composition stops growing and
     gains margins instead. Both halves below are sized in % OF THIS BOX, not in
     vw, so they stay locked to each other once the cap is hit. */
  max-width: var(--landing-max);
  margin-inline: auto;
  display: flex;
  align-items: center;              /* vertical-centre the copy → lines up with
                                       the vertically-centred photo on the right */
  justify-content: flex-start;
  text-align: left;
  padding: 0;
  overflow: hidden;
}

/* The copy owns the LEFT half (the ASCII photo owns the right 50vw). The block
   is centred vertically by the flex parent and inset horizontally by a
   viewport-scaled pad, so it never hugs the edge on big screens and never
   crowds the photo on smaller ones — the whole thing is fluid. */
.landing-content {
  position: relative;
  z-index: 2;
  /* % of the capped .landing, not vw — see the note on .landing */
  width: 50%;
  box-sizing: border-box;
  /* Was clamp(1.75rem, 10vw, 18rem), which never reached its 18rem ceiling until
     ~2880px and so behaved as pure 10vw: 24px of gutter at 320px and 256px at
     2560px. That 10x swing was most of why wide screens felt like a different
     page. This tops out an order of magnitude sooner. */
  padding-left: var(--page-pad-l);
  padding-right: clamp(1.5rem, 3vw, 4rem);
}
.landing-name {
  font-family: var(--font-display);
  /* Deliberately NOT on the shared --title-size any more. The home page name is
     set at body size, the stefanzweifel.dev treatment: the name is not a display
     headline, and hierarchy comes from weight and colour instead of scale. The
     Essays / Finance / Companies titles still ride --title-* and are unchanged —
     which is exactly why this overrides locally rather than editing the variable,
     since that would shrink every page title on the site. */
  font-size: var(--fs-name);   /* fluid: 25.6px on a phone, 37.6px on a wide screen */
  font-weight: var(--title-weight);
  /* --title-tracking is -0.03em, a display-type correction that only makes sense
     at ~90px. At 16px it just reads cramped, so tracking goes back to normal. */
  letter-spacing: normal;
  white-space: nowrap;
  line-height: 1.5;
  color: var(--heading);
}
.landing-tagline {
  color: var(--muted);
  font-size: var(--fs-body);
  font-weight: 300;
  /* Was the body's inherited 1.6. At weight 300 in --muted, leading that tight
     makes the two paragraphs read as a single grey slab. 1.75 also pairs with
     the wider measure set below: the longer a line runs, the more leading the
     eye needs to land reliably on the next one. The two changes are a pair —
     narrowing the measure again without also closing the leading would leave
     the copy over-spaced. */
  line-height: 1.75;
  /* Was a flat 1.6rem — the same 25.6px gap on a phone as on a 1440 screen,
     which is why the block looked most cramped exactly where there was most
     room to spare. Now fluid: ~21px on a phone -> ~29px at 1440 -> ~32px at the
     --landing-max cap, so the rhythm grows with the type.

     Tuned down once from clamp(1.6rem, 1.2rem + 1.2vw, 2.6rem), which put 36px
     here at 1440 and read as too loose against the 1.75 leading inside each
     paragraph — the gap has to stay clearly bigger than the line gap to separate
     the blocks, but once it runs much past that the two paragraphs stop reading
     as one section. This sits between the two. */
  margin-top: clamp(1.3rem, 1.05rem + 0.85vw, 2rem);
  /* Was 46ch, which ended the copy ~200px short of the artwork at 1440 and left
     the left half looking half-empty against a full-height image. 64ch is the
     upper end of the 45-75ch readable measure, chosen so the cap only starts
     doing work on genuinely wide screens.

     ch, not px or %, on purpose: the measure is tied to the font size, which is
     itself fluid (--fs-body), so a line stays ~64 CHARACTERS at every viewport.
     Character count is what governs readability, so this holds the reading
     experience constant while the pixel width scales.

     Which constraint binds changes with the screen, and both outcomes are
     wanted. Up to ~1500px the column is narrower than 64ch, so the copy simply
     fills its column and the cap is inert — that is what closes the gap to the
     art at laptop sizes. Past that the cap takes over and the line stops growing
     at 64 characters, so an ultrawide screen gets a readable column plus margin
     rather than one 90-character line.

     Tried 58ch first: it held the line at 604px, which at 1760 left ~225px of
     dead column between the copy and the art — the same emptiness this was
     meant to fix, just further out. 64ch keeps that near ~150px without pushing
     the measure out of readable range.

     The copy still deliberately stops short of the art — what is left between
     them is .landing-content's padding-right, which is breathing room rather
     than the dead space it replaced. */
  max-width: 64ch;
}
/* The name sits a level above the body copy in the hierarchy, so it gets more
   air beneath it than the paragraphs get between each other. Same fluid curve,
   scaled up — without this the paragraph gap would equal the gap under the name,
   and the name would stop reading as a heading.

   Tuned down alongside the paragraph gap above (was clamp(2rem, 1.5rem + 1.6vw,
   3.4rem), 47px at 1440). What matters is that the two curves move TOGETHER:
   this one stays ~1.27x the paragraph gap at every width, so tightening the
   group never flattens the name into the copy. Change one, change both. */
.landing-tagline:first-of-type {
  margin-top: clamp(1.7rem, 1.35rem + 1.05vw, 2.6rem);
}

/* ----- Home CTA buttons: the "swap-button" (ported from good-fella.com) -----
   Solid orange pills with a "+" that leaps from the right end to the left on
   hover while the label slides across to reveal it. Three pieces share the row:
   a left "+" that rotates/scales in, the label that slides right, and a right
   "+" that rotates/scales out. The absolute right icon keeps the layout width
   stable so the button never reflows. All three ease on the reference power4
   curve. Tokens (--gf-*) live in :root; the same component styles Fintech 3. */
/* Currently unused — the home page's featured CTA was removed, and this row was
   its only markup. Kept because it is the ready-made wrapper (spacing + wrap
   behaviour) for whenever a featured button returns to the landing. The .gf-cta
   rules below are still very much live: the Finance pages use them. */
.home-cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1.1rem;
  margin-top: 2.6rem;
}
.gf-cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  font-family: var(--gf-mono);
  font-weight: 500;
  font-size: 14px;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--gf-ink);
  -webkit-font-smoothing: antialiased;
}
.gf-cta-inner {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
}
.gf-cta-ico,
.gf-cta-label {
  background: var(--gf-brand);
  color: var(--gf-ink);
  transition: transform 0.7s var(--gf-ease);
}
.gf-cta-ico {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
}
.gf-cta-ico svg { width: 0.75em; height: 0.75em; }   /* ref size-[0.75em] */
.gf-cta-ico--left {
  transform-origin: left center;
  transform: rotate(-45deg) scale(0);
}
.gf-cta-ico--right {
  position: absolute;
  right: 0;
  z-index: 2;
  transform-origin: right center;
  transform: rotate(0deg) scale(1);
}
.gf-cta-label {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  justify-content: center;
  height: 40px;
  padding: 0 14px;
  white-space: nowrap;
  transform: translateX(-46px);            /* ref -translate-x-[calc(40px+6px)] */
}
.gf-cta:hover .gf-cta-ico--left  { transform: rotate(0deg) scale(1); }
.gf-cta:hover .gf-cta-label      { transform: translateX(0); }
.gf-cta:hover .gf-cta-ico--right { transform: rotate(-45deg) scale(0); }
.gf-cta:focus-visible {
  outline: 2px solid var(--gf-brand);
  outline-offset: 3px;
}

/* ----- Interactive dot photo on the right half of the home page -----
   A photograph rebuilt out of coloured dots (js/dot-photo.js). The cursor pushes
   the dots it passes near out of the way and they spring back behind it, so the
   picture reads as a live panel opposite the name. Pinned to the right half, art
   centred. Replaced the earlier ASCII-glyph treatment. */
.photo-wrap {
  position: absolute;
  top: 0; right: 0;
  /* % of the capped .landing so the two halves stay locked together; the old
     50vw/100vh kept growing after the container had stopped. */
  width: 50%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  /* The artwork's inset from the edge of the composition, declared HERE as real
     padding rather than as a number inside the renderer. js/dot-photo.js reads
     it back off this element, so the art and the nav's right rail are driven by
     one value and cannot drift apart. */
  padding: var(--photo-inset);
  z-index: 1;
  cursor: crosshair;
}
.photo-wrap #dot-photo {
  display: block;
  /* No drop-shadow here. The old one was a soft blue glow to lift the art off a
     BLACK page; on white it just muddies the dots. It was also a full-canvas
     filter, which the renderer would have to pay for on every animated frame. */
}
/* ----- Narrow screens: STACK the two halves rather than drop one -----
   This used to be `display: none` on the photo below 820px, which meant every
   phone and small tablet got a text-only home page — the one image on the site
   was exactly what small screens never saw. Stacking keeps the same composition
   everywhere and only changes its axis, which is the whole point of the page
   being consistent across sizes.

   The switch is at 900px rather than 820: between those two the side-by-side
   split leaves the copy under ~380px wide and the photo under ~400px, so both
   halves are compromised. Better to go single-column while each still has room. */
@media (max-width: 900px) {
  /* Re-point the two grid tokens rather than overriding each consumer. The nav,
     the copy and the artwork all read these, so redefining them here keeps the
     bar aligned with the stacked content for free — the previous version
     overrode the copy and the photo but not the nav, which is exactly how the
     three drifted apart on narrow screens. */
  :root {
    --page-pad-l:  clamp(1.5rem, 7vw, 3rem);
    --photo-inset: clamp(1.5rem, 7vw, 3rem);
  }

  .landing {
    flex-direction: column;
    justify-content: center;
    /* height: auto so a short phone in landscape can grow and scroll instead of
       clipping the photo; overflow returns to visible for the same reason. */
    height: auto;
    overflow: visible;
    /* Tuned against measured stack heights, not guessed: at 480x800 and 900x900
       the column was overrunning the viewport by 30px and 6px, almost entirely
       in the bottom pad. Top pad stays generous — it has to clear the fixed nav. */
    padding-block: clamp(5rem, 13vh, 7.5rem) clamp(2rem, 5vh, 2.5rem);
    gap: clamp(2rem, 5vh, 3.25rem);
  }
  .landing-content {
    width: 100%;
    /* both sides on the same rail now that the column is centred, not split */
    padding-left: var(--page-pad-l);
    padding-right: var(--page-pad-l);
  }
  /* out of absolute positioning and back into the column flow, under the copy */
  .photo-wrap {
    position: static;
    width: 100%;
    /* Height comes from the source 4:3 rather than the viewport, but is held
       under half the screen so the copy above it is never pushed out of view on
       a short device. js/dot-photo.js reads this box to size its canvas. */
    height: auto;
    aspect-ratio: 4 / 3;
    /* Horizontal inset only. The base rule pads all four sides, but here the
       box height is already capped by max-height below — paying the inset
       vertically as well would eat ~96px out of that cap and shrink the art for
       no gain, since nothing sits above or beside it in a single column. */
    padding-block: 0;
    /* 42vh, not 46: the copy above needs the difference on shorter screens. On a
       very small phone (~320x640) the copy alone is 333px and the hero still
       scrolls — accepted, since shrinking the art further would make it pointless
       before it made the page fit. */
    max-height: 42vh;
  }
}

/* Very short landscape phones: the stacked column cannot fit a 4:3 photo and the
   copy at once, so the art steps aside — the words matter more. */
@media (max-width: 900px) and (max-height: 460px) {
  .photo-wrap { display: none; }
}

/* ----- Page transition: a rotating-square wipe (ported from good-fella.com).
   A huge black square is pinned by its transform-origin to a bottom corner of
   the viewport and simply ROTATES: it swings up from -90deg to 0 to cover the
   page, then (on the incoming page) the origin hops to the opposite corner and
   it swings on to +90deg to reveal the new page — so the motion arcs in from
   one side and out the other. Driven by js/page-transition.js. On writings.html
   this hands off to that page's own pull-up loader, so the seam stays
   white-on-white and seamless.

   --wipe-ease: ease-in-out-quart, the exact curve the reference uses.
   --wipe-color: near-black. This one MUST be inverted with the theme, not left
   alone: the wipe was white so it would sweep boldly across a black page, and
   leaving it white on a white page makes the whole transition invisible. Ink is
   now the contrasting colour, so the square sweeps dark. ----- */
:root {
  --wipe-extend: 0px;
  --wipe-ease: cubic-bezier(0.76, 0, 0.24, 1);
  --wipe-color: #0a0a0a;
}
@media (orientation: portrait) { :root { --wipe-extend: 100vw; } }

/* The dark page's wipe colour has to win on <html> as well, because that is where
   the pre-paint cover is painted. The .dark-stage rule near the top of this file
   sets --wipe-color, but on <html> it ties with the :root default above on
   specificity and LOSES on source order — so the cover came out near-black while
   the plate that follows it came out near-white, i.e. a colour pop mid-wipe.
   html.dark-stage outranks both. */
html.dark-stage { --wipe-color: #f5f5f5; }

/* Pre-paint cover — see the note in js/page-transition.js. Painted the instant
   that script flags <html> from <head>, which is before the body exists and so
   before the destination can paint a frame of its own content. This is the fix
   for the flash between the two rotations; the animated .page-wipe below cannot
   do it, because it needs a <body> to be appended to. */
html.pt-cover::before {
  content: '';
  position: fixed;
  inset: 0;
  background: var(--wipe-color);
  z-index: 9999;
  pointer-events: none;
}

.page-wipe {
  position: fixed;
  width: 200vmax; height: 200vmax;
  bottom: 0; left: calc(-50vw - var(--wipe-extend));
  background: var(--wipe-color);
  z-index: 9999;
  pointer-events: none;
  will-change: transform;
  transform-origin: calc(50vw + var(--wipe-extend)) 100%;
  transform: rotate(-90deg);                 /* parked: swung off-screen */
}
/* outgoing: swing up to cover */
.page-wipe.is-covering {
  transition: transform 1s var(--wipe-ease);
  transform: rotate(0deg);
}
/* incoming: start covering, but origin hopped to the far corner (no transition) */
.page-wipe.is-hold {
  transform-origin: calc(50vw + var(--wipe-extend) + 100vw) 100%;
  transform: rotate(0deg);
}
/* incoming: swing on and off to reveal (0.1s beat before it goes, per reference) */
.page-wipe.is-leaving {
  transform-origin: calc(50vw + var(--wipe-extend) + 100vw) 100%;
  transition: transform 1s var(--wipe-ease) 0.1s;
  transform: rotate(90deg);
}

/* ===== INNER PAGES (Writings, Fintech) ===== */
.page {
  min-height: 100vh;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  padding: 6rem 1.5rem 2rem;
}
.page-title {
  font-family: var(--font-display);
  font-size: clamp(2.4rem, 7vw, 4.5rem);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  color: var(--heading);
}
.page-note {
  font-family: var(--font-mono);
  color: var(--muted);
  margin-top: 1.2rem;
  font-size: 0.8rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* =====================================================================
   FINTECH SCROLLYTELLING PAGE (Getty-style)
   ===================================================================== */

/* ----- Cinematic title screen ----- */
.ft-hero {
  min-height: 100vh;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  padding: 6rem 1.5rem;
}
.ft-hero-kicker {
  font-family: var(--font-mono);
  text-transform: uppercase; letter-spacing: 0.3em;
  font-size: 0.7rem; color: var(--muted);
  margin-bottom: 1.6rem;
}
.ft-hero-title {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: clamp(2.6rem, 8vw, 6rem);
  line-height: 1.05; letter-spacing: var(--display-tracking);
  color: var(--heading);
}
.ft-hero-sub {
  max-width: 48ch; color: var(--muted);
  margin-top: 1.8rem;
  font-size: clamp(1rem, 2.2vw, 1.2rem);
}
.ft-hero-hint {
  font-family: var(--font-mono);
  text-transform: uppercase; letter-spacing: 0.25em;
  font-size: 0.7rem; color: var(--muted);
  margin-top: 3.5rem;
  animation: ft-bob 1.8s ease-in-out infinite;
}
@keyframes ft-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(8px); } }

/* ----- Timeline: each era is a full-screen white stage, content centered ----- */
.ft-timeline { position: relative; }
.ft-era {
  position: sticky; top: 0;
  height: 100vh; overflow: hidden;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  gap: 2.5rem;
  padding: 6rem 1.5rem;
  background: var(--bg);
}

/* the photo for each era — centered, hovering above the text */
.ft-visual {
  position: relative; z-index: 2;   /* sits above the scattered photos */
  width: min(620px, 78vw);
  aspect-ratio: 3 / 2;
  background: #161616 center / cover no-repeat;
  border-radius: 4px;
  box-shadow: 0 30px 60px rgba(0, 0, 0, 0.12);
  opacity: 0; transform: translateY(30px) scale(0.98);
  transition: opacity 1s ease, transform 1s ease;
}
.ft-era.is-active .ft-visual { opacity: 1; transform: none; }
.ft-ghost-year { display: none; }

/* ----- Scattered background photos (Getty-style) -----
   Built by JS: smaller era images ringed around the centered photo.
   Positioned by inline left/top %, sized by inline width. */
.ft-scatter {
  position: absolute; z-index: 0;
  width: 150px; aspect-ratio: 4 / 3;
  background: #161616 center / cover no-repeat;
  border-radius: 3px;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.12);
  pointer-events: none;
  opacity: 0;
  transform: translate(calc(-50% + var(--dx, 0px)), calc(-50% + var(--dy, 0px)))
             rotate(var(--r, 0deg)) scale(0.9);
  transition: opacity 1.2s ease, transform 1.2s ease;
}
.ft-era.is-active .ft-scatter {
  opacity: 0.85;
  transform: translate(-50%, -50%) rotate(var(--r, 0deg)) scale(1);
}
/* logos: show whole mark on a white card instead of cropping */
.ft-scatter.is-logo {
  background-color: #fff; background-size: 62% auto;
  border: 1px solid var(--line);
}

/* the caption text, hovering in the middle of the screen */
.ft-caption {
  position: relative; z-index: 2;   /* above the scattered photos */
  max-width: 640px;
  opacity: 0; transform: translateY(24px);
  transition: opacity 1s ease 0.1s, transform 1s ease 0.1s;
}
.ft-era.is-active .ft-caption { opacity: 1; transform: none; }
.ft-tag {
  display: block;
  font-family: var(--font-mono);
  text-transform: uppercase; letter-spacing: 0.22em;
  font-size: 0.7rem; color: var(--muted);
  margin-bottom: 1.1rem;
}
.ft-caption h2 {
  font-family: var(--font-display); font-weight: var(--display-weight);
  font-size: clamp(1.8rem, 4.5vw, 3.4rem);
  line-height: 1.1; letter-spacing: var(--display-tracking);
  margin-bottom: 1.1rem;
  color: var(--heading);
}
.ft-caption p {
  font-family: var(--font-body); font-weight: 300;
  font-size: clamp(1rem, 2vw, 1.2rem);
  line-height: 1.6; margin: 0 auto;
  color: var(--text);
}

/* ----- Finale: scatter → sphere → fly into the grid ----- */
#ft-finale { position: relative; background: var(--bg); }
#ft-finale-spacer { height: 160vh; }   /* scroll room for scatter → sphere */

/* fixed overlay that holds the flying logo tiles */
.ft-stage {
  position: fixed; inset: 0; z-index: 30;
  pointer-events: none;
  opacity: 0;                          /* shown by JS only while the finale plays */
  transition: opacity 0.4s ease;
}
.ft-stage.is-on { opacity: 1; }
.ft-sphere { position: absolute; left: 50%; top: 50%; width: 0; height: 0; }
.ft-uni-tile {
  position: absolute; left: 0; top: 0;
  width: 58px; height: 58px;
  display: flex; align-items: center; justify-content: center;
  will-change: transform, opacity;
  /* light chip so the (often dark) logo art stays legible on a black page */
  background: rgba(255, 255, 255, 0.95);
  border-radius: 9px;
  padding: 6px;
}
.ft-uni-tile img {
  max-width: 100%; max-height: 100%;
  object-fit: contain; display: block;
}
.ft-uni-label {
  position: absolute; left: 0; top: 0;
  font-family: var(--font-mono);
  text-transform: uppercase; letter-spacing: 0.14em;
  font-size: 0.62rem; color: #111;   /* dark text on the light chip */
  background: rgba(255, 255, 255, 0.92);
  padding: 0.32rem 0.62rem; border-radius: 999px;
  border: 1px solid rgba(0, 0, 0, 0.12);
  white-space: nowrap; will-change: transform, opacity;
}
.ft-stage-caption {
  position: absolute; left: 50%; bottom: 7%;
  transform: translateX(-50%);
  text-align: center; pointer-events: none; will-change: opacity;
}
.ft-stage-caption h2 {
  font-family: var(--font-display); font-weight: var(--display-weight);
  font-size: clamp(1.8rem, 4vw, 3rem);
  letter-spacing: var(--display-tracking);
  color: var(--heading);
}

/* ----- Interactive dot-sphere hero (fintech 2) ----- */
.dot-hero {
  position: relative;
  height: 100vh;
  overflow: hidden;
  background: var(--bg);
}
.dot-hero-canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block; z-index: 0;
}
.dot-hero-caption { z-index: 2; }   /* sits above the canvas */
.dot-hero-hint {
  position: absolute; left: 50%; bottom: 2.2rem;
  transform: translateX(-50%);
  z-index: 2; pointer-events: none;
  font-family: var(--font-mono);
  text-transform: uppercase; letter-spacing: 0.2em;
  font-size: 0.68rem; color: var(--muted);
  text-align: center; white-space: nowrap;
}

/* ----- Tunnel words: headline that pops up at the END of the tunnel -----
   Lives inside the pinned hero (above the canvas), so the tunnel plays across
   the whole scroll and these words simply fade up over the final frame — no
   separate section to scroll to and no black gap. JS toggles .show near p≈1. */
.tunnel-words {
  position: absolute;
  top: 50%;
  left: clamp(1.5rem, 7vw, 7rem);
  transform: translateY(-50%);
  max-width: min(90vw, 1000px);
  z-index: 3;                 /* above the canvas (0) and the opening caption (2) */
  pointer-events: none;
}
.tunnel-words > * {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.7s cubic-bezier(0.2, 0.7, 0.2, 1),
              transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.tunnel-words.show > * { opacity: 1; transform: none; }
.tunnel-words.show > *:nth-child(2) { transition-delay: 0.1s; }
.tunnel-words.show > *:nth-child(3) { transition-delay: 0.2s; }

.tunnel-words-title {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: clamp(3.4rem, 10vw, 8rem);
  line-height: 1.02;
  letter-spacing: var(--display-tracking);
  color: var(--heading);
}
.tunnel-words-sub {
  margin-top: 1.6rem;
  max-width: 34ch;
  font-family: var(--font-body);
  font-weight: 300;
  font-size: clamp(1.05rem, 2.2vw, 1.5rem);
  line-height: 1.5;
  color: var(--muted);
}
.tunnel-words-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  margin-top: 2.4rem;
  padding: 0.95rem 1.5rem;
  border: 1px solid var(--line);
  border-radius: 2px;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.72rem;
  color: var(--text);
  transition: opacity 0.7s cubic-bezier(0.2, 0.7, 0.2, 1),
              transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1),
              border-color 0.3s ease, box-shadow 0.3s ease;
}
.tunnel-words.show .tunnel-words-cta { pointer-events: auto; }  /* clickable only once shown */
.tunnel-words-cta span { transition: transform 0.3s ease; }
.tunnel-words-cta:hover {
  color: var(--heading);
  border-color: rgba(87, 224, 160, 0.6);
  box-shadow: 0 0 24px rgba(74, 163, 255, 0.18), inset 0 0 18px rgba(87, 224, 160, 0.06);
}
.tunnel-words-cta:hover span { transform: translateX(5px); }

/* ----- $100 bill character-art: stood up on the RIGHT of the end-screen -----
   A real bill scan rebuilt out of tiny coloured glyphs (js/bill-ascii.js).
   Fades/rises in together with the tunnel words (same .show toggle). ----- */
.bill-ascii-wrap {
  position: absolute;
  top: calc(50% + 39px);       /* centred in the space BELOW the fixed 77px nav */
  right: clamp(1rem, 4vw, 4.5rem);
  transform: translateY(-50%) translateX(24px);
  z-index: 3;                 /* same layer as the words, opposite side */
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.9s cubic-bezier(0.2, 0.7, 0.2, 1),
              transform 0.9s cubic-bezier(0.2, 0.7, 0.2, 1);
  transition-delay: 0.15s;    /* trails the headline in slightly */
}
.bill-ascii-wrap.show { opacity: 1; transform: translateY(-50%) translateX(0); }
.bill-ascii {
  display: block;
  /* let the bill glow softly out of the black, like the good-fella portrait */
  filter: drop-shadow(0 0 26px rgba(120, 190, 150, 0.14));
}
/* keep the big headline clear of the bill: reserve ~470px on the right for it */
.tunnel-words { max-width: min(720px, calc(96vw - 470px)); }
/* on narrower screens the bill would crowd the words — retire it, restore width */
@media (max-width: 1000px) {
  .bill-ascii-wrap { display: none; }
  .tunnel-words { max-width: min(90vw, 1000px); }
}

/* =====================================================================
   FINTECH 2 — WebGL particle sphere + scroll handoff
   ===================================================================== */

/* the point-cloud canvas: fixed full-screen, behind everything, drawn by
   js/fintech2.js. Its opacity is animated by the scroll handler. */
.sphere-canvas {
  position: fixed;
  inset: 0;
  width: 100%; height: 100%;
  display: block;
  z-index: 0;
  background: var(--bg);
}

/* opening caption + scroll hint over the sphere; fades out as you scroll */
.sphere-hero {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  will-change: opacity;
}

/* end-screen overlay: holds the headline + bill, crossfades in on scroll.
   Its children (.tunnel-words / .bill-ascii-wrap) fade via their .show class,
   so this layer only needs to sit above the canvas and pass clicks through. */
.sphere-end {
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

/* the scroll distance that powers the whole fly-in (transparent spacer) */
.scroll-driver { height: 280vh; }

/* ----- Wallet grid: every company as a card in a wallet ----- */
.ft-grid-section { position: relative; background: var(--bg); padding: 8rem 1.5rem 9rem; }
.ft-back {
  position: absolute;
  top: 6rem;
  left: clamp(1.2rem, 4vw, 2.5rem);
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.68rem;
  color: var(--muted);
  transition: color 0.25s ease;
}
.ft-back span { transition: transform 0.25s ease; }
.ft-back:hover { color: var(--text); }
.ft-back:hover span { transform: translateX(-4px); }
.ft-grid-head { text-align: center; max-width: 640px; margin: 0 auto 4rem; }
.ft-grid-head h2 {
  font-family: var(--font-display); font-weight: var(--display-weight);
  font-size: clamp(2rem, 5vw, 3.4rem);
  letter-spacing: var(--display-tracking);
  color: var(--heading); margin-top: 0.4rem;
}
.ft-grid-sub {
  font-family: var(--font-mono); text-transform: uppercase;
  letter-spacing: 0.18em; font-size: 0.7rem;
  color: var(--muted); margin-top: 1rem;
}

/* each industry is its own block, with a label above its grid */
.ft-sector { max-width: 1500px; margin: 0 auto 5rem; }
.ft-sector-title {
  font-family: var(--font-mono);
  text-transform: uppercase; letter-spacing: 0.2em;
  font-size: 0.85rem; font-weight: 500;
  color: var(--heading);
  margin-bottom: 1.2rem;
}

/* hairline matrix (gaps reveal the line colour behind the white cells) */
.ft-grid {
  width: 100%;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}
.ft-wallet-cell {
  background: var(--bg);
  aspect-ratio: 1;
  display: flex; align-items: center; justify-content: center;
  padding: 13%;
}

/* the graphic sits on a square stage */
.ft-wallet { position: relative; width: 100%; aspect-ratio: 1; }

/* the debit card — landscape, pokes out the top, slides up on hover */
.ft-card {
  position: absolute; left: 50%; bottom: 20%;
  transform: translate(-50%, 0);
  width: 88%; aspect-ratio: 1.586;     /* real bank-card proportions */
  background: #fafafa;
  border: 1px solid var(--line);
  border-radius: 9px;
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.12);
  z-index: 1;
  transition: transform 0.4s cubic-bezier(.22, 1, .36, 1), box-shadow 0.4s ease, opacity 0.5s ease;
}
.ft-card img {
  position: absolute; top: 11%; left: 9%;
  max-width: 46%; max-height: 26%;
  object-fit: contain; object-position: left top;
  transition: opacity 0.4s ease;
}
.ft-card-chip {
  position: absolute; top: 50%; left: 9%;
  transform: translateY(-50%);
  width: 15%; aspect-ratio: 4 / 3; border-radius: 2px;
  background: linear-gradient(135deg, #ddc77f, #b9942f);
}

/* the wallet pocket — covers the lower half of the card */
.ft-wallet-pocket {
  position: absolute; left: 0; bottom: 0;
  width: 100%; height: 46%;
  background: linear-gradient(180deg, #e9e9e9, #d6d6d6);
  border-radius: 8px 8px 13px 13px;
  z-index: 2;
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.6), 0 10px 20px rgba(0, 0, 0, 0.10);
  transition: opacity 0.5s ease;
}

/* while the sphere logos are still flying in, hide the card + wallet art so
   the overlay tiles appear to BE the logos; everything fades in on landing */
#ft-grid-section.is-forming .ft-card,
#ft-grid-section.is-forming .ft-card img,
#ft-grid-section.is-forming .ft-wallet-pocket { opacity: 0; }
.ft-wallet-pocket::before {            /* the slot opening */
  content: ''; position: absolute; top: 7px; left: 8%;
  width: 84%; height: 3px; border-radius: 3px;
  background: rgba(0, 0, 0, 0.13);
}
.ft-wallet-cell:hover .ft-card {
  transform: translate(-50%, -46%);
  box-shadow: 0 18px 28px rgba(0, 0, 0, 0.18);
}

/* ----- Outro ----- */
.ft-outro {
  position: relative; z-index: 5;
  min-height: 70vh;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center; padding: 3rem 1.5rem;
  background: var(--bg);
}
.ft-outro h2 {
  font-family: var(--font-display); font-weight: var(--display-weight);
  font-size: clamp(2rem, 6vw, 4rem); letter-spacing: var(--display-tracking);
  color: var(--heading);
}
.ft-outro p {
  font-family: var(--font-mono);
  text-transform: uppercase; letter-spacing: 0.2em;
  font-size: 0.75rem; color: var(--muted); margin-top: 1rem;
}

/* ----- Bottom timeline scrubber (year + progress) ----- */
.ft-scrubber {
  position: fixed; left: 0; right: 0; bottom: 1.6rem; z-index: 40;
  margin: 0 auto;
  width: min(380px, 84vw);
  padding: 2.2rem 0 1.1rem;
  color: var(--heading); pointer-events: none;
  opacity: 0; transform: translateY(120%);
  transition: opacity 0.4s ease, transform 0.4s ease;
}
.ft-scrubber.is-visible { opacity: 1; transform: none; }
.ft-scrubber-track {
  height: 2px; position: relative;
  background: var(--line);
}
.ft-scrubber-fill {
  position: absolute; left: 0; top: 0; height: 100%; width: 0%;
  background: var(--heading);
}
.ft-scrubber-year {
  position: absolute; bottom: 100%; left: 0;
  transform: translateX(-50%);
  margin-bottom: 0.5rem;
  font-family: var(--font-display); font-weight: 500;
  font-size: 1.4rem; line-height: 1; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--heading);
  white-space: nowrap;
}
