/* ClearFund's own three-tone brand, same values the plugin ships (class-settings.php).
   The site is built as one continuous world: the visitor loads in at ground level and
   scrolling raises the camera until, by the Free-and-Pro section, they are in the cloud
   layer. Content rides on that world as glass. See site.js for the camera. */
:root {
  --deep:   #04613F;
  --accent: #068355;
  --mid:    #09A76C;
  --decor:  #00C583;
  --ink:    #10201a;
  --body:   #3f5349;
  --muted:  #4c5c54;
  --border: #dfeae4;
  --radius: 14px;
  --shadow: 0 1px 2px rgba(16,32,26,.05), 0 8px 24px rgba(16,32,26,.06);
  --wrap: 1080px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;

  /* Glass, and as of the perf pass the panes get their glass from OPACITY rather than
     from a backdrop-filter. .92 is the measured floor: at the old .90 the ridgeline and a
     glass tower read straight through the hero pane as hard-edged shapes, and .92 is
     where they stop. See the note above .pane for why the filter went. */
  --pane-bg:      rgba(255,255,255,.92);
  --pane-edge:    rgba(255,255,255,.62);
  --pane-lift:    0 1px 2px rgba(4,44,28,.05), 0 20px 50px -18px rgba(4,44,28,.30);
  --pane-inset:   inset 0 1px 0 rgba(255,255,255,.85), inset 0 -1px 0 rgba(255,255,255,.22);

  --danger:    #a8331a;
  --danger-bg: rgba(216,96,47,.13);
  --danger-edge: rgba(216,96,47,.45);
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

/* ── moving between pages ─────────────────────────────────────────────
   Cross-document view transitions: navigating to /about/ or /contact/ cross-fades rather
   than blanking to white and repainting. No JavaScript is involved — this is a browser
   feature, so a browser without it simply navigates the way it always did, which is the
   behaviour we already had. Nothing to fail closed.

   The sky and the nav are given names of their own so they are treated as the SAME
   element either side of the navigation and are not cross-faded with the rest. The whole
   point of the world is that it is continuous, and a page change should feel like walking
   through it, not like being handed a different one. */
@view-transition { navigation: auto; }

/* The transition fires correctly — verified via the pageswap event — but the first
   version was invisible, and for a good reason: the sky and the nav persist by design, so
   the only thing left to animate was one glass pane cross-fading into another glass pane
   on an identical background, over 280ms, moving 6px. There was nothing to see.

   Longer, further, and with a little scale, so the content clearly recedes and the new
   page arrives. The persistent sky is what makes this read as moving THROUGH the world
   rather than between two pages, so it stays. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) { animation: vt-out .34s cubic-bezier(.4,0,1,1) both; }
  ::view-transition-new(root) { animation: vt-in  .46s cubic-bezier(0,0,.2,1) both; }
}
@keyframes vt-out {
  to   { opacity: 0; transform: translateY(-22px) scale(.975); }
}
@keyframes vt-in {
  from { opacity: 0; transform: translateY(26px) scale(.985); }
}

.sky { view-transition-name: cf-sky; }
.nav { view-transition-name: cf-nav; }

/* No background on the body: the fixed .sky paints it. A background here would sit on
   top of the world and hide it. */
/* No overflow-x here, deliberately. `overflow-x: hidden` on the body makes it a scroll
   container, which can re-parent `position: fixed` children away from the viewport and
   upset the sticky nav. Nothing needs it either: .sky and .shards are the only things
   that overhang the viewport and both already clip themselves. */
body {
  margin: 0; background: transparent; color: var(--body);
  font-family: var(--font); font-size: 17px; line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { color: var(--ink); line-height: 1.2; margin: 0 0 .5em; letter-spacing: -.015em; }
h1 { font-size: clamp(2rem, 5vw, 3.1rem); font-weight: 800; }
h2 { font-size: clamp(1.5rem, 3.2vw, 2.1rem); font-weight: 750; }
h3 { font-size: 1.1rem; font-weight: 700; }
p { margin: 0 0 1.1em; }
a { color: var(--accent); }
img { max-width: 100%; height: auto; display: block; }
.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 1.25rem; }
.lede { font-size: 1.12rem; }
.center { text-align: center; }
.muted { color: var(--muted); }
.vh {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── the world ────────────────────────────────────────────────────────
   Fixed, full-viewport, and deliberately at z-index 0 rather than -1. An element with
   backdrop-filter establishes a containing block and will not sample a NEGATIVE
   z-index sibling behind it in several engines: the glass panes come out flat grey and
   it reads as a broken blur. Sky 0, content 1, veils 5, nav 20.

   Every gradient here is authored in CSS, not JS, so a visitor with scripting off still
   gets sky and ground; site.js only adds the ridge and cloud detail and moves the camera. */
.sky {
  position: fixed; inset: 0; z-index: 0; overflow: hidden;
  pointer-events: none; contain: layout paint;
}
.sky-grad { position: absolute; inset: -2px; }
/* The sky stays BLUE all the way down. An earlier version tinted the lower stops green
   to blend into the ridges, and the result was green mountains on a green sky — the
   silhouettes vanished. All the green in this world comes from the land. */
.sky-grad--ground {
  background: linear-gradient(180deg, #74b4dd 0%, #9fd0e9 28%, #c6e3f0 56%, #e0eff5 80%, #eef6f7 100%);
}
/* Cross-faded in as the camera climbs: thinner air, brighter haze, deeper blue overhead. */
.sky-grad--high {
  opacity: 0;
  background: linear-gradient(180deg, #5aa8d8 0%, #8ec8e8 24%, #c2e2f4 50%, #e6f4fb 76%, #ffffff 100%);
}
/* Above the clouds. The climb does not stop at cloud level — keep going and the air thins
   out into the deep blue you get at altitude, and eventually stars. */
.sky-grad--space {
  opacity: 0;
  background: linear-gradient(180deg, #0b1f45 0%, #16386e 22%, #2d63a4 48%, #6ba3d4 74%, #b8d9ee 100%);
}
.sky-stars { position: absolute; inset: 0; opacity: 0; }
.sky-stars svg { display: block; width: 100%; height: 100%; }
/* An actual sun, not just a warm patch. The first version was a 78vmax glow with no core,
   sitting at 74% height — which put it behind the mountains and made it read as nothing at
   all. It now has a defined disc and sits well above the ridgeline at rest. */
.sky-sun {
  position: absolute; left: 74%; top: 21%; width: 66vmax; height: 66vmax;
  margin: -33vmax 0 0 -33vmax; will-change: transform, opacity;
  background: radial-gradient(circle,
    rgba(255,253,246,1)    0%,
    rgba(255,251,232,.98)  2.6%,
    rgba(255,246,208,.78)  3.9%,
    rgba(255,240,190,.36)  6.5%,
    rgba(255,236,180,.17) 13%,
    rgba(255,235,178,.07) 27%,
    rgba(255,235,178,0)   50%);
}
.sky-layers { position: absolute; inset: 0; }
.sky-layer { position: absolute; will-change: transform; }
.sky-layer svg { display: block; width: 100%; height: auto; }

/* The lit edge where sky meets rock. Without it each ridge is a flat paper cut-out. */
.sky-layer path.crest { stroke-width: 1.5; stroke-linejoin: round; }
.sky-layer--far  path.crest { stroke: #d8ecf5; opacity: .50; }
.sky-layer--mid  path.crest { stroke: #b6e2cd; opacity: .40; }
.sky-layer--near path.crest { stroke: #6fd3a6; opacity: .34; }

/* Clouds drift sideways forever, independently of the camera.

   The band is rendered twice inside a doubly-wide element, so sliding it left by exactly
   half itself puts the copy where the original was: the loop is seamless and the motion
   is one-directional. An earlier version eased back and forth over ±5% across 300s, which
   was both unnatural — clouds do not change their minds — and far too slow to notice.
   These are slow enough to be calm and fast enough to be visible.

   linear, not ease: any easing makes the loop point obvious. */
@keyframes cloud-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
.sky-drift { will-change: transform; }
.sky-layer--cloudfar  .sky-drift { animation: cloud-drift 150s linear infinite; }
.sky-layer--cloudnear .sky-drift { animation: cloud-drift  90s linear infinite; }

/* The blur needs somewhere to fall off. Clipped at the band edge it leaves a straight
   cut across a cloud, which is exactly what a soft edge should never have. .sky already
   clips everything to the viewport, so letting these overflow costs nothing. */
.sky-layer svg { overflow: visible; }

/* Valley haze. It sits BEHIND the ridge layers, not over them — painted on top it just
   swallows the mountains and the whole world goes flat. Kept light for the same reason:
   nothing on the page depends on it for contrast — the panes carry their own. */
.sky-ground {
  position: absolute; left: -5%; right: -5%; bottom: 0; height: 100vh;
  will-change: transform;
  background: linear-gradient(180deg,
    rgba(226,241,247,0) 0%, rgba(222,238,246,.42) 42%, rgba(214,233,243,.62) 100%);
}

/* The plugin's backdrop separates its four layers by opacity alone (admin.css:320-323),
   which is right behind a chart: it is one flat tint at four strengths and never competes
   with the data. At full-sky scale that reads as mush, so these separate by HUE as well —
   real aerial perspective, distant ridges lighter and bluer, the near slope saturated and
   dark. Same ladder idea, more air in it. */
/* OPAQUE fills, pre-blended toward the sky rather than blended by opacity.

   Aerial perspective is a question of colour — distant things are lighter and bluer — not
   of transparency, and using opacity for it had a real cost: the ridges did not occlude.
   The glass towers standing behind the near slope showed straight through it, so nothing
   in the scene ever looked solid or in front of anything else. These are the same colours
   the blended versions resolved to, painted opaquely, so each landform now hides what is
   behind it the way a landform should. */
/* THE CLOUD LAYERS ARE AT FULL OPACITY AND CARRY THEIR ALPHA IN THE FILL INSTEAD.
   These were opacity .60 and .92. A composited layer with opacity < 1 needs its own
   render surface so the group can be blended as a unit, and the frame trace duly showed
   "opacity animation" among the reasons the compositor was making extra passes. site.js
   now folds each layer's multiplier into its own radial-gradient stops (it already emits
   one gradient per layer), so the ink is the same and the surface is not needed.

   Honesty about the size of this: measured ALONE the change is worth nothing, +0.2pp
   ±4.0 — the .sky-drift child carries will-change:transform and is composited either way,
   so the parent's opacity was only ever a cheap multiply at draw time. It is kept because
   in combination with the shorter veils it measured -6.9pp against -3.3pp for the veils
   on their own, and because a layer that does not need a render surface should not ask
   for one. Do not expect it to buy frames by itself. */
.sky-layer--cloudfar,
.sky-layer--cloudnear { opacity: 1; }
.sky-layer--far  path:not(.crest) { fill: #9cc3d6; }
.sky-layer--mid  path:not(.crest) { fill: #5b977e; }
.sky-layer--near path:not(.crest) { fill: #1c5741; }

/* ── the focus band ───────────────────────────────────────────────────
   Two fixed slabs of glass, top and bottom, mask-faded so there is no hard edge. What
   you are reading, the middle ~70% of the viewport, is never touched; only what is
   entering or leaving softens. Exactly two backdrop-filter elements exist on the page,
   which is the whole reason this is affordable: panes drifting THROUGH the document
   would multiply that cost by however many are on screen at once.

   THE HEIGHTS ARE A PERFORMANCE BUDGET, NOT A TASTE. Profiled against the live site on a
   241 Hz display (4.2 ms frame budget), these two elements were the most expensive thing
   on the page after the sky itself: hiding them took the dropped-frame rate from 32.4% to
   23.8%. They were 26vh + 22vh, so nearly HALF the viewport was being sampled and
   re-blurred every frame of every scroll.

   What the profile also showed is which knob matters. Dropping the blur radius from 7px
   to 4px changed nothing at all (+0.2pp ±2.7) — the cost of a backdrop-filter here is
   paying for the render surface and the backdrop copy, and is essentially flat in the
   radius. Removing the mask was likewise nothing (-0.8pp). AREA is the only lever that
   pays: 16vh + 14vh measured -2.9pp on its own and -3.3pp with the saturate dropped.

   So: keep the 7px blur, which is what the effect actually looks like, and buy the frames
   back by blurring less of the screen. */
.veil { position: fixed; left: 0; right: 0; z-index: 5; pointer-events: none; }
.veil--top    { top: 0;    height: 16vh; }
.veil--bottom { bottom: 0; height: 14vh; }

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .veil {
    background: rgba(255,255,255,.10);
    /* saturate() removed: a 1.08 boost is not visible on a band this soft, and it
       measured a consistent -1.0pp ±0.5 on its own. */
    -webkit-backdrop-filter: blur(7px);
            backdrop-filter: blur(7px);
  }
  .veil--top {
    -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0,0,0,.72) 46%, transparent 100%);
            mask-image: linear-gradient(180deg, #000 0%, rgba(0,0,0,.72) 46%, transparent 100%);
  }
  .veil--bottom {
    -webkit-mask-image: linear-gradient(0deg, #000 0%, rgba(0,0,0,.72) 46%, transparent 100%);
            mask-image: linear-gradient(0deg, #000 0%, rgba(0,0,0,.72) 46%, transparent 100%);
  }
}

/* ── drifting glass shards ────────────────────────────────────────────
   The 3D pane language, for free: gradient, border and specular only, never a
   backdrop-filter. Behind the content, so they cannot obscure a word of it.

   The host is created by site.js INSIDE .sky-layers, between the mid and near ridges, so
   the near slope passes in front of the shards and the far ones sit behind. As a fixed
   overlay on top of the whole world they read as smudges on the lens; slotted into the
   layer stack they read as glass hanging in the valley, which is the depth they exist
   to show. Absolute, not fixed — .sky is already fixed and clips them. */
.shards { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
/* Towers, so: rounded at the top, square-footed, and planted below the horizon. The near
   ridge paints after them and swallows their feet, which is what sells them as standing
   in the landscape rather than floating over it. */
.shard {
  position: absolute; will-change: transform;
  transform-origin: 50% 100%;
  border: 1px solid rgba(255,255,255,.26);
  border-bottom: 0;
  border-radius: 10px 10px 0 0;
  background: linear-gradient(100deg,
    rgba(255,255,255,.16) 0%, rgba(255,255,255,.04) 38%,
    rgba(255,255,255,.11) 62%, rgba(255,255,255,.03) 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.34), inset -1px 0 0 rgba(255,255,255,.10);
}
/* A vertical highlight down one face, which is what gives a flat rectangle an edge and
   a side rather than reading as a pane lying flat against the screen. */
.shard::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  background: linear-gradient(90deg, rgba(255,255,255,.20) 0%, transparent 22%, transparent 76%, rgba(255,255,255,.13) 100%);
}

/* ── nav ─────────────────────────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: 20;
  background: rgba(255,255,255,.82);
  border-bottom: 1px solid rgba(255,255,255,.55);
  box-shadow: 0 1px 20px -8px rgba(4,44,28,.30);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav {
    background: rgba(255,255,255,.62);
    -webkit-backdrop-filter: blur(14px) saturate(1.5);
            backdrop-filter: blur(14px) saturate(1.5);
  }
}
.nav .wrap { display: flex; align-items: center; gap: 1.5rem; height: 62px; }
/* The full lockup carries the name itself, so there is no text beside it to go stale or
   to disagree with the artwork. Sized by height; width follows the 3.33:1 master. */
.brand { display: flex; align-items: center; text-decoration: none; }
.brand img { height: 38px; width: auto; display: block; }
.foot .brand img { height: 30px; }
.nav-links { display: flex; gap: 1.35rem; margin-left: auto; align-items: center; }
.nav-links a { color: var(--body); text-decoration: none; font-size: .95rem; font-weight: 500; }
.nav-links a:hover { color: var(--accent); }
@media (max-width: 760px) { .nav-links a.hide-sm { display: none; } }

/* The altitude rail: how high the camera has climbed, ground to cloud. It teaches the
   scroll metaphor without spending a word of copy on it. Scaled on the compositor,
   never re-laid out. */
.altitude { position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: rgba(9,167,108,.14); }
.altitude i {
  display: block; height: 100%; width: 100%;
  transform: scaleX(0); transform-origin: 0 50%; will-change: transform;
  background: linear-gradient(90deg, var(--deep), var(--mid) 55%, var(--decor));
}

/* ── buttons ─────────────────────────────────────────────────────── */
.btn {
  display: inline-block; padding: .72rem 1.35rem; border-radius: 10px;
  font-weight: 650; font-size: 1rem; text-decoration: none; border: 1.5px solid transparent;
  transition: transform .12s ease, box-shadow .12s ease, background .12s ease;
  cursor: pointer; font-family: inherit;
}
.btn:hover { transform: translateY(-1px); }
.btn-primary { background: var(--accent); color: #fff; box-shadow: 0 2px 10px rgba(6,131,85,.28); }
.btn-primary:hover { background: var(--deep); }
.btn-ghost { border-color: rgba(255,255,255,.75); color: var(--ink); background: rgba(255,255,255,.58); }
.btn-ghost:hover { border-color: var(--mid); color: var(--accent); }
.btn-on-dark { background: rgba(255,255,255,.16); color: #fff; border-color: rgba(255,255,255,.42); }
.btn-on-dark:hover { background: rgba(255,255,255,.26); }
.btn-light { background: #fff; color: var(--deep); }
.btn-sm { padding: .5rem .95rem; font-size: .9rem; border-radius: 8px; }
/* ── hero ────────────────────────────────────────────────────────── */
/* At rest the camera sits on the ground, and the sky down there is BRIGHT. Two earlier
   attempts set white type straight onto it behind a dark scrim; both fought the sky and
   both lost. Aerial perspective puts the PALE distant ridges at the top of the frame and
   the dark ones at the bottom — so the one band a headline sits in is the one band that
   is never dark, and no overlay tuned for one viewport height survives another.

   So the hero is a pane, like every other block on the page. Dark type on glass reads
   over whatever sky the camera happens to be showing, needs no overlay at all, and is
   made of the same material as the rest of the site. */
.hero { position: relative; z-index: 1; padding: 5rem 0 0; text-align: center; }
.hero > * { position: relative; z-index: 1; }
.hero-pane { max-width: 730px; margin: 0 auto; padding: 2.9rem 2.6rem 2.4rem; }
.hero h1 { max-width: 18ch; margin-inline: auto; }
.hero p  { color: var(--body); font-size: 1.18rem; max-width: 52ch; margin: 0 auto 1.9rem; }
.hero-actions { display: flex; gap: .8rem; justify-content: center; flex-wrap: wrap; margin-bottom: .9rem; }
.hero-note { font-size: .9rem; color: var(--muted); margin-bottom: 0; }

/* Per-word rise-in. Transform and opacity only; the words hold their layout slots from
   first paint, so nothing reflows as they arrive. */
/* Fails SAFE. An earlier version parked the words at opacity 0 and transitioned them in
   when JS added a class — so any hitch in that handoff left the most important sentence
   on the site invisible, which is exactly what happened. There is no hidden default
   state now: the words are visible, and the animation is pure enhancement layered on
   top. If the class never lands, the headline simply does not animate. */
.hero h1 .w { display: inline-block; }
.js .hero h1.is-in .w { animation: word-rise .62s cubic-bezier(.2,.7,.3,1) both; }
@keyframes word-rise {
  from { opacity: 0; transform: translate3d(0, .5em, 0); }
  to   { opacity: 1; transform: none; }
}

/* The screenshot used to be bottom-cropped and bled straight into a solid section below,
   which is why it had no bottom edge and no gap above it. There is no band under it any
   more — it floats on the sky — so it gets air above and a finished frame all the way
   round, like every other pane on the page. */
.hero-shot { max-width: 940px; margin: 3.25rem auto 0; padding: 0 1.25rem; }
.hero-shot img {
  border-radius: 16px; border: 1px solid rgba(255,255,255,.55);
  box-shadow: var(--pane-inset), 0 30px 70px -26px rgba(0,30,18,.50);
  object-fit: cover; object-position: top;
  max-height: 440px;
}

/* ── sections ────────────────────────────────────────────────────── */
section { padding: 4.75rem 0; position: relative; z-index: 1; }
.section-head { max-width: 62ch; margin: 0 auto 2.75rem; text-align: center; }
.section-head p { font-size: 1.08rem; margin-bottom: 0; }

/* ── the pane system ──────────────────────────────────────────────────
   One opaque background and nothing else. There is no backdrop-filter on a pane any more,
   on any browser — the fill IS the glass. That used to be the fallback for engines with
   no backdrop-filter support; it is now the only path, so there is one panel to reason
   about instead of two and no way to land in the unreadable-text-on-a-mountain case.

   The 3D read comes from layering — a diagonal fill, a light edge, an inner top highlight
   and a long soft drop — not from an expensive filter, and that was always where most of
   the depth came from. See the long note further down for the measurements that removed
   the filter and why the fill is .92 rather than anything lower. */
.pane {
  position: relative;
  background: var(--pane-bg);
  border: 1px solid var(--pane-edge);
  border-radius: 18px;
  box-shadow: var(--pane-inset), var(--pane-lift);
  padding: 2rem;
  contain: paint;
}
.pane > * { position: relative; z-index: 1; }

/* Specular streak across the face. */
.pane::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 0;
  background: linear-gradient(108deg, transparent 28%, rgba(255,255,255,.42) 43%, rgba(255,255,255,.08) 51%, transparent 64%);
}
/* Pointer-tracked sheen. Fine pointers only: on a touch screen it would either never
   fire or stick wherever the last tap landed. */
.pane::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 0;
  background: radial-gradient(360px circle at var(--mx, 50%) var(--my, 0%), rgba(255,255,255,.55), transparent 62%);
  opacity: 0; transition: opacity .4s ease;
}
@media (hover: hover) and (pointer: fine) {
  .pane:hover::after { opacity: 1; }
}

/*
 * THE PANES NO LONGER USE A BACKDROP-FILTER. THEY GET THEIR GLASS FROM OPACITY.
 *
 * There used to be a gate here: 25 panes, .is-near added by site.js as each approached
 * the viewport, so only the near ones paid for a backdrop-filter. It worked, and it is
 * gone anyway, because the measurements said the whole technique was the wrong trade on
 * this page.
 *
 * WHAT THE PAGE AVERAGE HID. Over the whole scroll, turning every pane's blur off was
 * worth only 2.0pp of dropped frames — a rounding error, and the reason this survived an
 * earlier perf pass. Over the two regions that actually drop frames it was the single
 * biggest cost on the page: -11.8pp across the hero, -8.2pp across Free-and-Pro to the
 * CTA. The average is diluted by the entire middle of the document, where the number of
 * blurring panes is literally zero and there is nothing to save. Never tune this page on
 * a whole-page number; measure the region where the judder actually is.
 *
 * The gate was also not doing what its own comment claimed. It said "only the two or
 * three you can actually see" were blurring. Counted directly: .pane.is-near peaked at
 * NINE and concurrent backdrop-filters in the viewport at TEN. A 250px rootMargin top and
 * bottom over an 806px viewport is a 1306px band, and the plans and CTA panes are short
 * enough to stack nine deep inside it.
 *
 * WHY .92 AND NOT LESS. The blur was not decoration — it removed the high-frequency
 * detail so the sliver of sky showing through arrived as a wash. Drop the filter at the
 * old .90 fill and the ridgeline and one of the glass towers read straight through the
 * hero pane as hard-edged shapes. .92 is where that stops. Going further (.97 was tried)
 * hides them just as well but flattens the pane to a plain white card and throws away the
 * landscape tint, which is the thing worth keeping.
 *
 * This is also what mobile and touch have always done — see the .94 override in the media
 * query near the end of this file. Desktop has now converged on it.
 *
 * Backdrop-filter cost on this page, measured four ways, is FIXED PER ELEMENT: changing
 * the blur radius did nothing (+0.2pp for 7px -> 4px), removing the mask did nothing, and
 * freezing the per-frame opacity write did nothing. Only area and count move it. If a
 * backdrop-filter is ever reintroduced here, that is the rule to design against.
 */

/* MEASURED, not guessed. The worst case is the weakest stop of the blurred pane over the
   TOP of the deep-altitude sky (#0b1f45) — which only became the worst case once the
   camera was allowed to climb past the clouds. Composited against it:

   Recomputed for the flat .92 fill that replaced the blurred gradient:

       --body  #3f5349   7.05:1   (needs 4.5)   was 5.58:1
       --muted #4c5c54   6.05:1   (needs 4.5)   was 4.78:1
       --ink   #10201a  14.42:1   (headings)    was 11.41:1

   Dropping the backdrop-filter IMPROVED all three, and by a wide margin. The old worst
   case was the weakest stop of the blurred gradient, .81; a flat .92 is far more opaque
   than that, so less of the dark sky reaches the text. The perf change and the
   accessibility of this page pull in the same direction here, which is not usually how
   this goes — do not read it as licence to thin the panel back down.

   An earlier draft used .60/.68/.74 stops and a lighter --muted, which measured 2.68:1
   for captions — comfortably illegible. Glass is allowed to cost some transparency and
   is not allowed to cost readability.

   Do not lighten these or thin the panel without re-measuring against the DARKEST sky the
   camera can reach, not against the average one. The plugin holds itself to 4.5:1 and the
   storefront must not undercut it. The mobile/touch value (.94) is more opaque still and
   passes with room to spare. */

.pane--lead { max-width: 64ch; margin: 0 auto; font-size: 1.14rem; line-height: 1.7; padding: 2.5rem; }
.pane--lead strong { color: var(--ink); }
.pane--lead p:last-child { margin-bottom: 0; }

/* ── steps ───────────────────────────────────────────────────────── */
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.5rem; }
.step-n {
  width: 34px; height: 34px; border-radius: 50%; background: var(--accent); color: #fff;
  display: grid; place-items: center; font-weight: 700; font-size: .95rem; margin-bottom: .9rem;
  box-shadow: 0 4px 12px -4px rgba(6,131,85,.6);
}
.step p:last-child { margin-bottom: 0; }

/* ── tags & callout ──────────────────────────────────────────────── */
.tag {
  display: inline-block; font-size: .72rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--accent); background: rgba(6,131,85,.11);
  border-radius: 99px; padding: .25rem .6rem; margin-bottom: .7rem;
}
.callout { border-left: 4px solid var(--mid); max-width: 70ch; margin: 2.25rem auto 0; font-size: .98rem; padding: 1.35rem 1.6rem; }
.callout p:last-child { margin-bottom: 0; }

/* ── follow your $50 ──────────────────────────────────────────────────
   A sticky stage beside scrolling prose. Step progress is derived from the section's
   own rect in the shared rAF (site.js), so this adds no scroll listener of its own. */
.story-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3.5rem; align-items: start; }
.story-stage { position: sticky; top: 16vh; height: 68vh; min-height: 420px; }
.story-stage svg { width: 100%; height: 100%; display: block; overflow: visible; }
.story-step { min-height: 76vh; display: flex; align-items: center; }
.story-step:last-child { min-height: 52vh; }
.story-step .pane { width: 100%; }
.story-step h3 { font-size: 1.25rem; }
.story-step p:last-child { margin-bottom: 0; }
.story-kicker { font-size: .78rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--mid); display: block; margin-bottom: .4rem; }

/* Stage parts. Everything moves on transform and opacity only.

   NO transition here, deliberately. These values are already driven continuously from
   the scroll position, so a transition on top of them just adds several hundred
   milliseconds of lag between the scrollbar and the picture — the stage arrives after
   you have stopped scrolling, which reads as jank rather than smoothness. */
.st-part { will-change: opacity; }
.st-coin-face { fill: #f6c744; }
.st-coin-rim  { fill: none; stroke: #c99a10; stroke-width: 2; }
.st-coin-txt  { fill: #7a5a04; font: 700 15px var(--font); text-anchor: middle; }
.st-pool-bg   { fill: rgba(255,255,255,.55); stroke: rgba(4,97,63,.18); }
.st-pool-fill { fill: var(--mid); }
.st-label     { fill: var(--ink); font: 650 13px var(--font); }
.st-sub       { fill: var(--muted); font: 400 11.5px var(--font); }
.st-amt       { fill: var(--deep); font: 700 13px var(--font); text-anchor: end; }
.st-card      { fill: rgba(255,255,255,.82); stroke: rgba(255,255,255,.75); }
.st-receipt   { fill: #fffdf6; stroke: rgba(4,97,63,.22); }
.st-receipt-l { stroke: rgba(4,97,63,.28); stroke-width: 1.5; stroke-linecap: round; }
.st-thread    { stroke: var(--mid); stroke-width: 2; fill: none; stroke-linecap: round; }
.st-tick      { fill: none; stroke: #fff; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.st-tick-bg   { fill: var(--mid); }

/* No-JS and reduced motion: the stage renders its finished state, and the steps simply
   read as three paragraphs. Nothing is hidden behind an animation that never runs. */
/* The stage is drawn by JS and is decoration only — the three steps carry every word of
   the story themselves. With scripting off it would be an empty 68vh hole, so it goes. */
.no-js .story-stage { display: none; }
.no-js .story-grid { grid-template-columns: 1fr; }
.no-js .story-step { min-height: 0; margin-bottom: 1.25rem; }
/* With scripting off the amounts stay as disabled fields carrying their real values, and
   the hash lines stay as the static text the markup ships with. The ledger still reads. */
.no-js .chain-actions { display: none; }

/* ── break the chain ──────────────────────────────────────────────────
   Real HMAC-SHA256 in the browser, matching the plugin's own hash_input() field order
   (created_at|event|entity_type|entity_id|actor_id|payload|prev_hash). */
.chain { display: grid; gap: .8rem; max-width: 900px; margin: 0 auto; }
.block {
  display: grid; grid-template-columns: 2.2rem 1fr auto; gap: .9rem; align-items: center;
  padding: .95rem 1.1rem; border-radius: 14px;
  transition: border-color .3s ease, background .3s ease, box-shadow .3s ease;
}
.block-seq {
  width: 2.2rem; height: 2.2rem; border-radius: 8px; display: grid; place-items: center;
  background: rgba(6,131,85,.10); color: var(--accent); font-weight: 700; font-size: .85rem;
}
.block-main { min-width: 0; }
.block-ev { font-weight: 650; color: var(--ink); font-size: .95rem; }
.block-hash {
  font-family: var(--mono); font-size: .72rem; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: .15rem;
}
.block-hash b { font-weight: 400; color: var(--accent); }
.block-amt { display: flex; align-items: center; gap: .4rem; }
.block-amt span { color: var(--muted); font-size: .95rem; }
.block-amt input {
  font: inherit; font-size: .95rem; font-weight: 650; color: var(--ink); width: 6.5rem;
  padding: .35rem .5rem; border: 1.5px solid var(--border); border-radius: 8px;
  background: rgba(255,255,255,.85); text-align: right;
}
.block-amt input:focus { border-color: var(--mid); outline: none; box-shadow: 0 0 0 3px rgba(9,167,108,.18); }
.block-amt input:disabled { background: rgba(255,255,255,.4); color: var(--muted); }

/* The seam between blocks. Green while the chain holds, red from the break down. */
.block-link { display: grid; grid-template-columns: 2.2rem 1fr; gap: .9rem; align-items: center; height: .8rem; margin: -.8rem 0; }
.block-link i { display: block; width: 2px; height: 100%; margin: 0 auto; background: var(--mid); opacity: .5; transition: background .3s ease, opacity .3s ease; }

.block.is-broken { border-color: var(--danger-edge); background: var(--danger-bg); }
.block.is-broken .block-seq { background: rgba(216,96,47,.16); color: var(--danger); }
.block.is-broken .block-hash b { color: var(--danger); }
.block-link.is-broken i { background: var(--danger); opacity: 1; }
.block.is-edited { box-shadow: var(--pane-inset), 0 0 0 2px rgba(216,96,47,.5), var(--pane-lift); }

/* Everything past the break. The real verifier returns at the first bad entry, so these
   rows were genuinely never checked — greying them is the honest picture, and painting
   them red would not be. */
.block.is-unreached { opacity: .5; }
.block.is-unreached .block-hash b { color: var(--muted); font-style: italic; }

.chain-verdict {
  display: flex; align-items: center; gap: .7rem; justify-content: center;
  margin: 1.6rem auto 0; padding: .9rem 1.3rem; border-radius: 12px; max-width: 620px;
  font-weight: 650; font-size: 1rem; text-align: center;
  background: rgba(9,167,108,.12); color: var(--deep); border: 1px solid rgba(9,167,108,.35);
  transition: background .3s ease, color .3s ease, border-color .3s ease;
}
.chain-verdict.is-broken { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-edge); }
.chain-verdict-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--mid); flex: none; }
.chain-verdict.is-broken .chain-verdict-dot { background: var(--danger); }
.chain-actions { display: flex; gap: .7rem; justify-content: center; flex-wrap: wrap; margin-top: 1.2rem; }
.chain-hint { text-align: center; font-size: .92rem; color: var(--muted); max-width: 60ch; margin: 1.4rem auto 0; }

/* ── screens, as a stack of glass ─────────────────────────────────────
   Panes settle to flat as they enter view (one-shot IntersectionObserver) and take a
   small pointer tilt on fine pointers. will-change is applied on hover and REMOVED
   after: pinning six layers into GPU memory permanently is its own performance bug. */
.stage { display: grid; gap: 3rem; perspective: 1600px; }
/* position:relative so .shot is reliably the frame's offsetParent — site.js derives the
   frame's untransformed centre from offsetTop/offsetLeft against this box. */
.shot { transform-style: preserve-3d; position: relative; }
.shot figcaption { font-size: .95rem; color: var(--muted); margin-top: .8rem; max-width: 70ch; }
.shot h3 { margin-bottom: .3rem; }
.shot-frame {
  position: relative; border-radius: 16px; overflow: hidden; margin-top: 1.1rem;
  border: 1px solid rgba(255,255,255,.6);
  box-shadow: var(--pane-inset), 0 28px 60px -24px rgba(4,44,28,.42);
  transition: transform .45s cubic-bezier(.2,.7,.3,1), box-shadow .4s ease;
  /*
   * THE PERSPECTIVE IS ON THE TRANSFORM ITSELF, NOT INHERITED FROM .stage.
   *
   * .stage sets perspective: 1600px and .shot sets transform-style: preserve-3d, which
   * should carry it down to this element. It does not, and the tilt was rendering as a
   * flat horizontal squash: measured across the whole cursor range the frame's edge moved
   * 0.58px, and identically for -2.81deg and +2.81deg — symmetric, which is the signature
   * of an orthographic rotation with no perspective at all. Visually that is nothing,
   * which is exactly how it looked.
   *
   * The cause is the entrance animation. `.js .shot.is-in` runs shot-in with
   * animation-fill-mode: both, so the animation stays applied for the life of the page,
   * and it animates opacity. An element with an active opacity animation has its
   * preserve-3d forced to flat — so .shot is permanently flattened and .stage's
   * perspective stops there, one level above the element that needed it. Proven by
   * ablation: disabling the shot-in animation restores the perspective, while forcing
   * preserve-3d back onto .shot or clearing this element's overflow does nothing.
   *
   * Declaring perspective() as part of this element's own transform sidesteps the whole
   * chain. It cannot be flattened by a grouping property on an ancestor, so the tilt no
   * longer depends on nothing above it ever gaining an opacity animation, a filter or a
   * containment. Same 1600px, so the geometry is what the original design intended: the
   * edge now travels 7.5px across the cursor range instead of 0.58px.
   */
  transform: perspective(1600px) rotateY(var(--ry, 0deg)) rotateX(var(--rx, 0deg));
}
/* While the pointer is tracking, the frame follows in ~70ms. The long settle curve above
   is for arriving and for returning to rest; used for tracking it makes every mouse move
   restart a 450ms animation, so the frame lags and rubber-bands behind the cursor. */
.shot-frame.is-tilt { transition: transform 70ms linear; }
.shot-frame img { border-radius: 15px; }
.shot-frame::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background: linear-gradient(112deg, transparent 30%, rgba(255,255,255,.30) 45%, transparent 58%);
  opacity: 0; transition: opacity .45s ease;
}
@media (hover: hover) and (pointer: fine) {
  .shot-frame:hover::after { opacity: 1; }
}
/* Fails SAFE, for the same reason the hero headline does. Parking these at opacity 0 and
   waiting for an IntersectionObserver to grant them visibility means any environment that
   throttles or never delivers IO callbacks shows six invisible screenshots — the section
   the page exists to show. There is no hidden default state: the entrance is an animation
   layered on top, and if the class never lands the screens are simply there. */
.js .shot.is-in { animation: shot-in .75s cubic-bezier(.2,.7,.3,1) both; }
@keyframes shot-in {
  from { opacity: 0; transform: translate3d(0, 46px, 0); }
  to   { opacity: 1; transform: none; }
}

/* ── plans ───────────────────────────────────────────────────────── */
.plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; align-items: start; }
.plan--pro { border-color: rgba(9,167,108,.7); box-shadow: var(--pane-inset), 0 0 0 1px rgba(9,167,108,.35), 0 26px 60px -20px rgba(4,44,28,.34); }
.plan h3 { font-size: 1.35rem; margin-bottom: .15rem; }
.plan .price { font-size: 1rem; color: var(--muted); margin-bottom: 1.2rem; }
.plan ul { list-style: none; padding: 0; margin: 0 0 1.6rem; }
.plan li { padding-left: 1.6rem; position: relative; margin-bottom: .65rem; font-size: .97rem; }
.plan li::before {
  content: ""; position: absolute; left: 0; top: .55em;
  width: 7px; height: 7px; border-radius: 50%; background: var(--mid);
}
.plan .btn { width: 100%; text-align: center; }

/* ── faq ─────────────────────────────────────────────────────────── */
.faq { max-width: 72ch; margin: 0 auto; display: grid; gap: .7rem; }
.faq details { padding: 1.15rem 1.5rem; }
.faq summary {
  cursor: pointer; font-weight: 650; color: var(--ink); font-size: 1.03rem;
  list-style: none; display: flex; justify-content: space-between; gap: 1rem; align-items: center;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; color: var(--mid); font-weight: 400; font-size: 1.5rem; line-height: 1; transition: transform .25s ease; }
.faq details[open] summary::after { transform: rotate(45deg); }
.faq details p { margin: .85rem 0 0; }
.faq details p:last-child { margin-bottom: 0; }

/* ── cta + footer ────────────────────────────────────────────────── */
/* The camera is at its highest here, so the CTA is a pane of clear air rather than a
   solid band: the sky behind it is the point. */
.cta { text-align: center; }
.cta .pane { max-width: 720px; margin: 0 auto; padding: 3rem 2.5rem; }
.cta p { max-width: 52ch; margin: 0 auto 1.9rem; font-size: 1.08rem; }
.cta-actions { display: flex; gap: .8rem; justify-content: center; flex-wrap: wrap; }

footer { position: relative; z-index: 1; padding: 2.5rem 0 3rem; font-size: .92rem; color: var(--muted); }
footer a { color: var(--body); text-decoration: none; }
footer a:hover { color: var(--accent); }
.foot { display: flex; flex-wrap: wrap; gap: 1rem 1.75rem; align-items: center; padding: 1.4rem 1.75rem; border-radius: 16px; }
.foot .spacer { margin-left: auto; }

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

/* ── about ───────────────────────────────────────────────────────── */
.prose { max-width: 65ch; margin: 0 auto; font-size: 1.08rem; padding: 2.75rem; }
.prose h2 { margin-top: 2.25rem; }
.prose h2:first-child { margin-top: 0; }
.prose p:last-child { margin-bottom: 0; }
.signature { margin-top: 2rem; padding-top: 1.75rem; border-top: 1px solid rgba(4,97,63,.14); }
.signature p { margin: 0; }
.signature .who { font-weight: 700; color: var(--ink); }

/* ── contact form ────────────────────────────────────────────────── */
.form { display: grid; gap: 1.1rem; }
.field { display: grid; gap: .35rem; }
.field label { font-weight: 600; color: var(--ink); font-size: .95rem; }
.field input, .field textarea {
  font: inherit; font-size: 1rem; color: var(--ink);
  padding: .7rem .85rem; border: 1.5px solid var(--border); border-radius: 9px;
  background: rgba(255,255,255,.9); width: 100%;
}
.field input:focus, .field textarea:focus { border-color: var(--mid); outline: none; box-shadow: 0 0 0 3px rgba(9,167,108,.18); }
.field textarea { min-height: 150px; resize: vertical; }
.field .hint { font-size: .85rem; color: var(--muted); }
/* The honeypot. Hidden from people and from screen readers; bots fill it anyway. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
/* Turnstile renders into an iframe that arrives after the page does. Reserving its
   height keeps the send button from jumping down under the cursor mid-click. 65px is the
   widget's own fixed height; the extra covers the error state, which is one line taller. */
.cf-turnstile { min-height: 65px; }

.form-status { font-size: .95rem; padding: .85rem 1rem; border-radius: 9px; display: none; }
.form-status.is-ok  { display: block; background: rgba(9,167,108,.12); color: var(--deep); border: 1px solid rgba(9,167,108,.35); }
.form-status.is-bad { display: block; background: var(--danger-bg); color: var(--danger); border: 1px solid var(--danger-edge); }
.contact-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 2rem; align-items: start; }
.contact-aside h3 { margin-bottom: .3rem; }
.contact-aside h3:not(:first-child) { margin-top: 1.4rem; }
.contact-aside p { font-size: .97rem; }
.contact-aside p:last-child { margin-bottom: 0; }

/* Short hero for the interior pages: the same ground and sky, without the screenshot
   hanging off the bottom. */
.hero--short { padding: 4rem 0 3.5rem; }
.hero--short h1 { max-width: 24ch; font-size: clamp(1.8rem, 4vw, 2.6rem); }
.hero--short p { margin-bottom: 0; font-size: 1.08rem; }

/* ── responsive ──────────────────────────────────────────────────── */
@media (max-width: 900px) {
  .story-grid { grid-template-columns: 1fr; gap: 1rem; }
  .story-stage { position: sticky; top: 12vh; height: 46vh; min-height: 300px; }
  .story-step { min-height: 60vh; }
  .contact-grid { grid-template-columns: 1fr; }
}

/* Mobile and touch: drop every backdrop-filter for an opaque tint. This is where the
   technique actually costs frames, and a slightly flatter card is a fair trade for a
   scroll that does not stutter. */
@media (max-width: 760px), (pointer: coarse) {
  .veil { display: none; }
  .shards { display: none; }

  /* The single most expensive thing on a phone: a large, Gaussian-blurred raster being
     translated every frame, forever, whether or not anything else is happening. It keeps
     the compositor awake for the life of the page and competes with the scroll for the
     same budget. The clouds are still there and still parallax with the camera — they
     simply stop drifting sideways. */
  .sky-drift { animation: none !important; }

  /* Nothing here needs a permanently promoted layer: these are transformed on scroll, and
     translate3d already promotes them for the duration. Declaring will-change as well
     pins ~13 layers into GPU memory for the whole session, which is exactly the wrong
     trade on the device with the least of it. */
  .sky-layer, .sky-ground, .sky-sun, .sky-drift, .altitude i, .st-part { will-change: auto; }
  .pane, .nav {
    -webkit-backdrop-filter: none !important;
            backdrop-filter: none !important;
    background: rgba(255,255,255,.94) !important;
  }
  .pane::before { opacity: .5; }
  .pane { padding: 1.5rem; }
  .pane--lead, .prose { padding: 1.75rem; }
  .block { grid-template-columns: 1.9rem 1fr; row-gap: .6rem; }
  .block-amt { grid-column: 1 / -1; justify-content: flex-end; }
  .block-link { grid-template-columns: 1.9rem 1fr; }
  section { padding: 3.25rem 0; }

  /* The nav has to stay one row. With the full lockup plus About, Contact and Live demo,
     375px is tight enough that "Live demo" wrapped and doubled the bar's height, pushing
     the whole page down. Nowrap and a smaller mark, rather than dropping a link — both
     About and Contact are meant to be reachable from every page. */
  .nav .wrap { gap: .75rem; }
  .nav-links { gap: .8rem; }
  .nav-links a { white-space: nowrap; font-size: .86rem; }
  .brand img { height: 28px; }
}

@media (max-width: 400px) {
  .nav-links { gap: .6rem; }
  .nav-links a { font-size: .8rem; }
  .brand img { height: 24px; }
}

/* prefers-reduced-motion is binding, not advisory: the plugin already treats it that
   way (its dashboard paints final totals instead of counting up to them). The camera
   parks, the veils go, and every widget shows its finished state. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .veil { display: none; }
  .sky-layer, .sky-ground, .sky-sun, .shard { transition: none !important; }
  /* Explicit, so the clouds come to rest centred rather than snapping to whichever end
     of the drift the blanket animation-duration override happens to land on. */
  .sky-drift { animation: none !important; transform: none !important; }
  .js .hero h1.is-in .w { animation: none; }
  .js .shot { opacity: 1; transform: none; }
  .shot-frame { --ry: 0deg !important; --rx: 0deg !important; }
  .st-part { opacity: 1 !important; transform: none !important; }
  .btn:hover { transform: none; }
  * { animation-duration: .001ms !important; animation-iteration-count: 1 !important; }
}

/* The page you are on, marked in the nav. Both About and Contact now appear on every
   page — previously each page omitted its own link, so the pair was never visible at
   once and the nav looked like it was missing an item. */
.nav-links a[aria-current="page"] { color: var(--accent); font-weight: 650; }
